Towards The Prettiest Printer

Last updated on May 1, 2015 by Jean-Philippe Bernardy
Tags: Haskell

Popular Haskell pretty printers have given me less-than-optimal results. This is especially disappointing, as they seem to be the epitome of functional programs, blessed with the correct-by-construction methodology of program development. In this note I review why I find the current solutions sub-optimal, and propose a satisfactory alternative.

The state of the art.

Even today, pretty printing in Haskell is mostly backed by two classic libraries, either:

  1. The Hughes-Peyton Jones library. The design is described by Hughes in The Design of a Pretty-printing Library. It has then been adopted (and modified) by Peyton Jones, and was distributed with GHC for a long time, making it the de-facto standard pretty printer. It is now available on Hackage in the pretty package. I believe that this remains the dominant design, perhaps disputed by…

  2. The Wadler-Leijen library. In the penultimate chapter of The Fun of Programming, Wadler re-constructs a pretty printing library from scratch. Keeping true to Hughes in particular and the general functional programming tradition in general, Wadler starts by specifying his library using equational laws, and derives an implementation. Leijen took Wadler’s implementation and modified it to increase its expressivity (but more on that later). The result is available in the eponymous wl-pprint package.

Not. Pretty. Enough.

As it happens, I am dissatisfied with the outputs produced by either libraries. At the risk of appearing ungrateful to the masters, I’ll spend some effort to back this claim.

Hughes

Let us assume we want to pretty print S-Expressions:

data SExpr where
  SExpr :: [SExpr] -> SExpr
  Atom :: String -> SExpr

We’d like to allow to pretty print an S-Expr either horizontally, like so:

(a b c d)

or vertically, like so:

(a
 b
 c
 d)

(I’ll refrain the urge to be more specific at this point).

For the sake of the argument, let’s pretend we want to pretty print the following s-expr:

abcd = SExpr $ map (Atom . (:[])) "abcd"
abcd4 = SExpr [abcd,abcd,abcd,abcd]
testData = SExpr [Atom "axbxcxd", abcd4] 

Printed on a wide page, we’d get:

(axbxcxd ((a b c d) (a b c d) (a b c d) (a b c d) (a b c d)))

In a narrow page (whose width is indicated by the row of hashes), what we’d get from Hughes’ library is the following output:

###############
(axbxcxd ((a
           b
           c
           d)
          (a
           b
           c
           d)
          (a
           b
           c
           d)
          (a
           b
           c
           d)
          (a
           b
           c
           d)))
###############

This does not quite cut it. Our senses of aesthetics are tingling… this is not pretty enough. Why can’t we get the following?

###############
(axbxcxd
 ((a b c d)
  (a b c d)
  (a b c d)
  (a b c d)
  (a b c d))

The thing is, Hughes believes that “it would be unreasonably inefficient for a pretty-printer do decide whether or not to split the first line of a document on the basis of the content of the last.” (sec. 7.4 of his paper). Therefore, he chooses a greedy algorithm, which tries to fit as much as possible on a single line, without regard for what comes next. In our example, the algorithm fits (axbxcxd ((a, but then it has committed to a very deep indentation level, which forces a less-than-pretty outcome for the remainder of the document.

Wadler

Wadler’s design fares somewhat better. It does not suffer from the above problem… by default. That is, it lacks the capability to express that sub-documents should be vertically aligned — compositionally.

Let us illustrate. Using Wadler’s library, one might specify pretty printing of s-exprs as follows (see Wadler’s paper is there is any doubt on the meaning of the following):

x </> y = x <> line <> y
sep = foldr empty (</>)
pretty (SExpr xs) = group $ "(" <> nest 1 (sep $ map pretty xs) <> ")"
pretty (Atom x) = text x

which appears to do the trick. Indeed, we get:

###############
(axbxcxd
 ((a b c d)
  (a b c d)
  (a b c d)
  (a b c d)
  (a b c d))

However, the group combinator does not quite behave as I’d like. What group does is to allow its argument to be laid out on a single line, instead of multiple ones. Hence, we can put two elements next to each other only if they are flattened. This means that if we typeset the same s-expr, but in a slightly wider page, we get the same output:

#####################
(axbxcxd
 ((a b c d)
  (a b c d)
  (a b c d)
  (a b c d)
  (a b c d))

whereas I crave something more pleasing to the eye:

#####################
(axbxcxd ((a b c d)
          (a b c d)
          (a b c d)
          (a b c d)
          (a b c d))

At this point, the reader may raise two objections:

The search for the prettiest output

API

Before discussing possible algorithms, we need to chose wisely the the document-description language that we accept. Daringly standing on Phil’s strong shoulders, I propose the following set of combinators:

We can represent the above API in a data type, as follows:

data Doc where
  Line :: Doc
  Nil :: Doc
  (:<>) :: Doc -> Doc -> Doc
  Text :: String -> Doc
  Nest :: Int -> Doc -> Doc
  Align :: Doc -> Doc
  (:<|>) :: Doc -> Doc -> Doc -- ^ Attn: INVARIANT
  Spacing :: String -> Doc

Compared to Wadler (and a-fortiori Hughes) the above API is very liberal in the typesetting strategies that it can express. Indeed, the user can use a fully-general disjunction operator (<|>), which accepts arbitrary documents as arguments. A downside is that it leaves the user responsible to give two documents that differ only in layout: they must have the same contents.

contents :: Doc -> [String]
contents (Spacing _) = []
contents Nil = []
contents Line = []
contents (d1 :<> d2) = contents d1 <> contents d2
contents (Text x) = [x]
contents (Align x) = contents x
contents (x :<|> y) = contents x

(Note that the contents function relies on the invariant being verified.)

Other invariants include that text and spacing may not contain any newline, and nesting may not be negative.

Example

Using the above combinators, we can pretty print s-exprs as follows:

x <+> y = x <> Spacing " " <> y
x </> y = x <> Line <> y
sep [] = mempty
sep xs = foldr1 (<+>) xs :<|> Align (foldr1 (</>) xs)
pretty (Atom s) = Text s
pretty (SExpr xs) = Text "(" <> (sep $ map pretty xs) <> Text ")"

The sep combinator now precisely expresses what I was after at the beginning: either all the elements are glued horizontally, or they are aligned vertically.

Semantics

We have our API and an intuition of what it means. Let us make the intuition formal, by specifying how to render documents. I could do as Hughes or Wadler and start by stating a few laws on the API (in particular all laws stated by Wadler should hold). Instead I’ll give the semantics directly, using a compositional interpretation. I will interpret documents as a non-deterministic function from the current indentation level (1st argument) and current column (2nd argument) to a text and a final column.

Using lists for non-determinism, we have:

type Semantics = Int -> Int -> [(String,Int)]

The interpretation function is then the following.

eval :: Doc -> Semantics
eval (Text s) i c = return (s, c + length s)
eval (Spacing s) i c = return (s, c + length s)
eval Nil i c = return ("",c)
eval (Align d) i c = eval d c c
eval (Nest j d) i c = eval d (i+j) c
eval Line i c = return ('\n' : replicate i ' ', i)
eval (d1 :<> d2) i c = do
  (t1,c1) <- eval d1 i c
  (t2,c2) <- eval d2 i c1
  return (t1 ++ t2, c2)
eval (d1 :<|> d2) i c = eval d1 i c ++ eval d2 i c

Given the use of monadic syntax to handle list-non-determinism, the interpretation of text, spacing, empty, <>, and even <|> reserve no particular surprise. The interesting bit is the interplay between line, nest and align.

The indentation level is implemented by inserting a certain number of spaces after moving to the next Line (which also resets the current column). Nest-ing is defined by increasing the indentation level. Align-ing means setting the indentation level to the current column. (Exercise: verify that, at all times, c >= i.)

Finally, we can define the prettiest rendering as that which

(This is not quite the ideal definition: sometimes no layout fits the page, and we want to pick that with the least overflow. But we’ll leave such details to the implementer and stick to the simpler definition given above.)

Fitting the page means that the line width is less than the page width:

maxWidth = maximum . map length . lines
fits w s = maxWidth s <= w

The final renderer is thus:

height = length . lines
render w d = minimumBy (compare `on` height) $ filter (fits w) $ map fst $ eval d 0 0

The above renderer satisfies our needs: it finds the prettiest layout. Yet, we should not expect to get results quickly. A document may contain hundreds of disjunctions, and if we exhaustively search a space that big, even the legendary long-lasting batteries of our iPads(tm) will die before anything can be printed.

Implementation

Fortunately, there is a way out of this tar-pit. The trick is to explore the search space line by line. That is, every time we find the Line combinator, we stash the current partial result for later examination. Eventually, all pending states will be stashed. We can then prune out useless, dominated states, and resume the search. There remains to define when a state is dominated:

For each state t, we define:

Definition: t dominates u iff. i(t) < i(u) and p(t) >= p(u).

Heuristic: If t dominates u, and t is a valid state, then u does not generate the prettiest output. The idea is the following: if u is at a higher indentation level, it has less space to print the rest of the document (remember that indentation is always positive). Therefore, if it is also late in the production of tokens, there is no hope for u to catch up with t. There are some pathological cases where things do not go as I outline above, however they are pretty rare: they never occured in the examples that I’ve tried1.

Consequently, if there is a finite number l of indentation levels (traditionally l=79), then we have only to consider at worst l solutions after each line break. There is no exponential blow up.

For completeness, here is the code implementing the above idea.

type Docs = [(Int,Doc)]
data Process = Process {curIndent :: Int -- current indentation
                       ,progress :: Int
                       ,tokens :: [String] -- tokens produced, in reverse order
                       ,rest :: Docs  -- rest of the input document to process
                       }
measure :: Process -> (Int, Int)
measure Process{..} = (curIndent, negate progress)

filtering :: [Process] -> [Process]
filtering (x:y:xs) | progress x >= progress y = filtering (x:xs)
                   | otherwise = x:filtering (y:xs)
filtering xs = xs

renderFast :: Int -> Doc -> String
renderFast w doc = concat $ reverse $ loop [Process 0 0 [] $ [(0,doc)]]
    where
      loop ps = case dones of
        (done:_) -> done
        [] -> case conts of
          (_:_) -> loop $ filtering $ sortBy (compare `on` measure) $ conts
          [] -> error "overflow"
        where
          ps' = concatMap (\Process{..} -> rall progress tokens curIndent rest) ps
          (dones,conts) = partitionEithers ps'

      rall :: Int -> [String] -> Int -> Docs -> [Either [String] Process]
      rall p ts k ds0 | k > w = []
      rall p ts k ds0 = case ds0 of
         [] ->  [Left ts] -- Done!
         (i,d):ds -> case d of
            Nil       -> rall p ts k ds
            Text s    -> rall (p+1) (s:ts) (k+length s) ds
            Spacing s -> rall (p  ) (s:ts) (k+length s) ds
            Line      -> [Right $ Process i p (('\n':replicate i ' '):ts) ds]
            x :<> y   -> rall p ts k ((i,x):(i,y):ds)
            Nest j x  -> rall p ts k ((i+j,x):ds)
            x :<|> y  -> rall p ts k ((i,x):ds) ++ rall p ts k ((i,y):ds)
            Align x   -> rall p ts k ((k,x):ds)

Coda

The above has been inspired by two implementations of pretty printers that I’ve made. The first one is a regular pretty printing library, available on hackage which is a drop-in replacement for the print-wl package, except for a few unsafe functions which I’ve hidden. It implements exaclty the above idea, but handles overflow satisfactorily.

The second one is part of the marxup package, which is a Haskell layer on top of the Latex document-preparation system. To see an example of a paper typeset with this technology, follow this link.

Happy pretty printing!


  1. I originally thought this heurisitic to be a theorem, as shown in earlier versions of this document.