DoctorPretty

0.5.0

Wadler's "A prettier printer" embedded pretty-printer DSL for Swift
bkase/DoctorPretty

What's New

10_000x Speedup

2018-06-28T17:34:52Z

Thanks @chriseidhof for the 10_000x speedup with a simple fix!

Doctor Pretty

A Swift implementation of the A prettier printer (Wadler 2003) ported from wl-pprint-annotated

Build Status

What is this

A pretty printer is the dual of a parser -- take some arbitrary AST and output it to a String. This library is a collection of combinators and a primitive called a Doc to describe pretty printing some data much like a parser combinator library provides combinators and primitives to describe parsing test into an AST. Interestingly, this implementation efficiently finds the best pretty print. You encode your knowledge of what the best means with your use of various Doc combinators.

For example: Let's say we have some internal structured representation of this Swift code:

func aLongFunction(foo: String, bar: Int, baz: Long) -> (String, Int, Long) {
    sideEffect()
    return (foo, bar, baz)
}

With this library the description that pretty prints the above at a page width of 120 characters. Also prints:

At a page-width of 40 characters:

func aLongFunction(
    foo: String, bar: Int, baz: Long
) -> (String, Int, Long) {
    sideEffect()
    return (foo, bar, baz)
}

and at a page-width of 20 characters:

func aLongFunction(
    foo: String,
    bar: Int,
    baz: Long
) -> (
    String,
    Int,
    Long
) {
    sideEffect()
    return (
        foo,
        bar,
        baz
    )
}

See the encoding of this particular document in the testSwiftExample test case.

What would I use this for?

If you're outputting text and you care about the width of the page. Serializing to a Doc lets you capture your line-break logic and how your output string looks in one go.

Why would you output text and care about page width?

  1. You're building a gofmt-type tool for Swift (Note: gofmt doesn't pretty-print based on a width, refmt (Reason) and prettier (JavaScript) do)
  2. You're writing some sort of codegen tool to output Swift code
  3. You're building a source-to-source transpiler
  4. You're outputing help messages in a terminal window for some commandline app (I'm planning to use this for https://github.com/bkase/swift-optparse-applicative)

What is this, actually

A Swift implementation of the A prettier printer (Wadler 2003) paper (including generally accepted modern enhancements ala wl-pprint-annotated. This implementation is close to a direct port of wl-pprint-annotated with some influence from scala-optparse-applicative's Doc and a few extra Swiftisms.

Basic Usage

Doc is very composable. First of all it's a monoid with an .empty document and the .concat case which just puts two documents next to each other. We also have a primitive called grouped, which tries this document on a single line, but if it doesn't fit then breaks it up on new-lines. From there we build all high-level combinators up.

x <%> y concats x and y with a space in between if it fits, otherwise puts a line.

.text("foo") <%> .text("bar")

pretty-prints under a large page-width:

foo bar

but when the page-width is set to 5 prints:

foo
bar

Here are a few more combinators:

/// Concats x and y with a space in between
static func <+>(x: Doc, y: Doc) -> Doc

/// Behaves like `space` if the output fits the page
/// Otherwise it behaves like line
static var softline

/// Concats x and y together if it fits
/// Otherwise puts a line in between
static func <%%>(x: Doc, y: Doc) -> Doc

/// Behaves like `zero` if the output fits the page
/// Otherwise it behaves like line
static var softbreak: Doc

/// Puts a line between x and y that can be flattened to a space
static func <&>(x: Doc, y: Doc) -> Doc

/// Puts a line between x and y that can be flattened with no space
static func <&&>(x: Doc, y: Doc) -> Doc

There are also combinators for turning collections of documents into "collection"-like pretty-printed primitives such as a square-bracket separated lists:

.text("let x =") <%> [ "foo", "bar", "baz" ].map(Doc.text).list(indent: 4)

Pretty-prints at page-width 80 to:

let x = [foo, bar, baz]

and at page-width 10 to:

let x = [
    foo,
    bar,
    baz
]

See the source for more documentation, I have included descriptive doc-comments to explain all the operators (mostly taken from wl-pprint-annotated).

How do I actually pretty print my documents?

Doc has two rendering methods for now: renderPrettyDefault prints with a page-width of 100 and renderPretty lets you control the page-width.

These methods don't return Strings directly -- they return SimpleDoc a low-level IR that is close to a string, but high-enough that you can efficiently output to some other output system like stdout or a file.

For now, SimpleDoc has displayString() which outputs a String, and:

func display<M: Monoid>(readString: (String) -> M) -> M

display takes a function that can turn a string into a monoid and then smashes everything together. Because this works for any monoid, you just need to provide a monoid instance for your output formatter (to write to stdout or to a file).

Installation

With Swift Package Manager, put this inside your Package.swift:

.Package(url: "https://github.com/bkase/DoctorPretty.git",
         majorVersion: 0, minor: 3)

How does it work?

Read the A prettier printer (Wadler 2003) paper.

Doc is a recursive enum that captures text and new lines. The interesting case is .union(longerLines: Doc, shorterLines: Doc). This case reifies the notion of "try the longer lines first, then the shorter lines". We can build all sorts of high-level combinators that combine Docs in different ways that eventually reduce to a few strategic .unions.

The renderer keeps a work-list and each rule removes or adds pieces of work to the list and recurses until the list is empty. The best-fit metric proceeds greedily for now, but can be swapped out easily for a more intelligent algorithm in the future.

Description

  • Swift Tools 4.0.0
View More Packages from this Author

Dependencies

Last updated: Thu Oct 17 2024 19:31:05 GMT-0900 (Hawaii-Aleutian Daylight Time)