pretty-compact 1.0 → 3.1
raw patch · 5 files changed
Files
- Setup.hs +0/−2
- Text/PrettyPrint/Compact.hs +185/−317
- Text/PrettyPrint/Compact/Core.hs +220/−112
- bench/Benchmark.hs +85/−0
- pretty-compact.cabal +35/−9
− Setup.hs
@@ -1,2 +0,0 @@-import Distribution.Simple-main = defaultMain
Text/PrettyPrint/Compact.hs view
@@ -1,35 +1,83 @@+{-# LANGUAGE TupleSections #-} {-# LANGUAGE OverloadedStrings #-}-+-- | Compact pretty-printer.+--+-- == Examples+--+-- Assume that we want to pretty print S-Expressions, which can either be atom or a list of S-Expressions.+--+-- >>> data SExpr = SExpr [SExpr] | Atom String deriving Show+-- >>> let pretty :: SExpr -> Doc (); pretty (Atom s) = text s; pretty (SExpr xs) = text "(" <> (sep $ map pretty xs) <> text ")"+--+-- Using the above representation, the S-Expression @(a b c d)@ has the following encoding:+--+-- >>> let abcd = SExpr [Atom "a",Atom "b",Atom "c",Atom "d"]+--+-- The legible layouts of the @abcd@ S-Expression defined above would be either+--+-- >>> putStrLn $ render $ pretty abcd+-- (a b c d)+--+-- or+--+-- >>> putStrLn $ renderWith defaultOptions { optsPageWidth = 5 } $ pretty abcd+-- (a+-- b+-- c+-- d)+--+-- The @testData@ S-Expression is specially crafted to+-- demonstrate general shortcomings of both Hughes and Wadler libraries.+--+-- >>> let abcd4 = SExpr [abcd,abcd,abcd,abcd]+-- >>> let testData = SExpr [ SExpr [Atom "abcde", abcd4], SExpr [Atom "abcdefgh", abcd4]]+-- >>> putStrLn $ render $ pretty testData+-- ((abcde ((a b c d) (a b c d) (a b c d) (a b c d)))+-- (abcdefgh ((a b c d) (a b c d) (a b c d) (a b c d))))+--+-- on 20-column-wide page+--+-- >>> putStrLn $ renderWith defaultOptions { optsPageWidth = 20 } $ pretty testData+-- ((abcde ((a b c d)+-- (a b c d)+-- (a b c d)+-- (a b c d)))+-- (abcdefgh+-- ((a b c d)+-- (a b c d)+-- (a b c d)+-- (a b c d))))+--+-- Yet, neither Hughes' nor Wadler's library can deliver those results.+--+-- === Annotations+--+-- For example we can annotate every /car/ element of S-Expressions,+-- and in the rendering phase emphasise them by rendering them in uppercase.+--+-- >>> let pretty' :: SExpr -> Doc Any; pretty' (Atom s) = text s; pretty' (SExpr []) = text "()"; pretty' (SExpr (x:xs)) = text "(" <> (sep $ annotate (Any True) (pretty' x) : map pretty' xs) <> text ")"+-- >>> let render' = renderWith defaultOptions { optsAnnotate = \a x -> if a == Any True then map toUpper x else x }+-- >>> putStrLn $ render' $ pretty' testData+-- ((ABCDE ((A B C D) (A B C D) (A B C D) (A B C D)))+-- (ABCDEFGH ((A B C D) (A b c d) (A b c d) (A b c d))))+-- module Text.PrettyPrint.Compact ( -- * Documents- Doc, + Doc, -- * Basic combinators- mempty, char, text, (<>), nest, line, linebreak, group, softline,- softbreak,+ module Data.Monoid, text, flush, char, - -- * Alignment- --- -- The combinators in this section can not be described by Wadler's- -- original combinators. They align their output relative to the- -- current output position - in contrast to @nest@ which always- -- aligns to the current nesting level. This deprives these- -- combinators from being \`optimal\'. In practice however they- -- prove to be very useful. The combinators in this section should- -- be used with care, since they are more expensive than the other- -- combinators. For example, @align@ shouldn't be used to pretty- -- print all top-level declarations of a language, but using @hang@- -- for let expressions is fine.- align, hang, indent, encloseSep, list, tupled, semiBraces,+ hang, hangWith, encloseSep, list, tupled, semiBraces, -- * Operators- (<+>), (<$>), (</>), (<$$>), (<//>),+ (<+>), ($$), (</>), (<//>), (<$$>), -- * List combinators- hsep, vsep, fillSep, sep, hcat, vcat, fillCat, cat, punctuate,+ hsep, sep, hcat, vcat, cat, punctuate, - -- * Fillers- fill, fillBreak,+ -- * Fill combiantors+ -- fillSep, fillCat, -- * Bracketing combinators enclose, squotes, dquotes, parens, angles, braces, brackets,@@ -43,34 +91,44 @@ bool, -- * Rendering+ renderWith, render,+ Options(..),+ defaultOptions, + -- * Annotations+ annotate,+ -- * Undocumented -- column, nesting, width ) where -import Control.Applicative import Data.Monoid import Text.PrettyPrint.Compact.Core as Text.PrettyPrint.Compact -type Indentation = Int+-- | Render the 'Doc' into 'String' omitting all annotations.+render :: Annotation a => Doc a -> String+render = renderWith defaultOptions -infixr 5 </>,<//>,<$$$>,<$$>-infixr 6 <+>+defaultOptions :: Options a String+defaultOptions = Options+ { optsAnnotate = \_ s -> s+ , optsPageWidth = 80+ } -- | The document @(list xs)@ comma separates the documents @xs@ and -- encloses them in square brackets. The documents are rendered -- horizontally if that fits the page. Otherwise they are aligned -- vertically. All comma separators are put in front of the elements.-list :: [Doc] -> Doc+list :: Annotation a => [Doc a] -> Doc a list = encloseSep lbracket rbracket comma -- | The document @(tupled xs)@ comma separates the documents @xs@ and -- encloses them in parenthesis. The documents are rendered -- horizontally if that fits the page. Otherwise they are aligned -- vertically. All comma separators are put in front of the elements.-tupled :: [Doc] -> Doc+tupled :: Annotation a => [Doc a] -> Doc a tupled = encloseSep lparen rparen comma @@ -78,7 +136,7 @@ -- semi colons and encloses them in braces. The documents are rendered -- horizontally if that fits the page. Otherwise they are aligned -- vertically. All semi colons are put in front of the elements.-semiBraces :: [Doc] -> Doc+semiBraces :: Annotation a => [Doc a] -> Doc a semiBraces = encloseSep lbrace rbrace semi -- | The document @(enclosure l r sep xs)@ concatenates the documents@@ -104,12 +162,12 @@ -- ,200 -- ,3000] -- @-encloseSep :: Doc -> Doc -> Doc -> [Doc] -> Doc-encloseSep left right sep ds- = case ds of- [] -> left <> right- [d] -> left <> d <> right- _ -> align (cat (zipWith (<>) (left : repeat sep) ds) <> right)+encloseSep :: Annotation a => Doc a -> Doc a -> Doc a -> [Doc a] -> Doc a+encloseSep left right separator ds+ = (<> right) $ case ds of+ [] -> left+ [d] -> left <> d+ (d:ds') -> cat (left <> d:map (separator <>) ds') ----------------------------------------------------------- -- punctuate p [d1,d2,...,dn] => [d1 <> p,d2 <> p, ... ,dn]@@ -139,9 +197,9 @@ -- -- (If you want put the commas in front of their elements instead of -- at the end, you should use 'tupled' or, in general, 'encloseSep'.)-punctuate :: Doc -> [Doc] -> [Doc]-punctuate p [] = []-punctuate p [d] = [d]+punctuate :: Annotation a => Doc a -> [Doc a] -> [Doc a]+punctuate _p [] = []+punctuate _p [d] = [d] punctuate p (d:ds) = (d <> p) : punctuate p ds @@ -151,222 +209,166 @@ -- | The document @(sep xs)@ concatenates all documents @xs@ either--- horizontally with @(\<+\>)@, if it fits the page, or vertically with--- @(\<$\>)@.+-- horizontally with @(\<+\>)@, if it fits the page, or vertically+-- with @(\<$$\>)@. Documents on the left of horizontal concatenation+-- must fit on a single line. ----- > sep xs = group (vsep xs)-sep :: [Doc] -> Doc-sep = group . vsep+sep :: Annotation a => [Doc a] -> Doc a+sep xs = groupingBy " " (map (0,) xs) --- | The document @(fillSep xs)@ concatenates documents @xs@--- horizontally with @(\<+\>)@ as long as its fits the page, than--- inserts a @line@ and continues doing that for all documents in--- @xs@.------ > fillSep xs = foldr (\<\/\>) empty xs-fillSep :: [Doc] -> Doc-fillSep = foldDoc (</>) +-- -- | The document @(fillSep xs)@ concatenates documents @xs@+-- -- horizontally with @(\<+\>)@ as long as its fits the page, than+-- -- inserts a @line@ and continues doing that for all documents in+-- -- @xs@.+-- --+-- -- > fillSep xs = foldr (\<\/\>) empty xs+-- fillSep :: Annotation a => [Doc a] -> Doc a+-- fillSep = foldDoc (</>)+ -- | The document @(hsep xs)@ concatenates all documents @xs@ -- horizontally with @(\<+\>)@.-hsep :: [Doc] -> Doc+hsep :: Annotation a => [Doc a] -> Doc a hsep = foldDoc (<+>) ---- | The document @(vsep xs)@ concatenates all documents @xs@--- vertically with @(\<$\>)@. If a 'group' undoes the line breaks--- inserted by @vsep@, all documents are separated with a space.------ > someText = map text (words ("text to lay out"))--- >--- > test = text "some" <+> vsep someText------ This is layed out as:------ @--- some text--- to--- lay--- out--- @------ The 'align' combinator can be used to align the documents under--- their first element------ > test = text "some" <+> align (vsep someText)------ Which is printed as:------ @--- some text--- to--- lay--- out--- @-vsep :: [Doc] -> Doc-vsep = foldDoc (<$$$>)- -- | The document @(cat xs)@ concatenates all documents @xs@ either -- horizontally with @(\<\>)@, if it fits the page, or vertically with -- @(\<$$\>)@. ----- > cat xs = group (vcat xs)-cat :: [Doc] -> Doc-cat = group . vcat+cat :: Annotation a => [Doc a] -> Doc a+cat xs = groupingBy "" (map (0,) xs) --- | The document @(fillCat xs)@ concatenates documents @xs@--- horizontally with @(\<\>)@ as long as its fits the page, than inserts--- a @linebreak@ and continues doing that for all documents in @xs@.------ > fillCat xs = foldr (\<\/\/\>) empty xs-fillCat :: [Doc] -> Doc-fillCat = foldDoc (<//>)+-- -- | The document @(fillCat xs)@ concatenates documents @xs@+-- -- horizontally with @(\<\>)@ as long as its fits the page, than inserts+-- -- a @linebreak@ and continues doing that for all documents in @xs@.+-- --+-- -- > fillCat xs = foldr (\<\/\/\>) empty xs+-- fillCat :: Annotation a => [Doc a] -> Doc a+-- fillCat = foldDoc (<//>) -- | The document @(hcat xs)@ concatenates all documents @xs@ -- horizontally with @(\<\>)@.-hcat :: [Doc] -> Doc+hcat :: Annotation a => [Doc a] -> Doc a hcat = foldDoc (<>) -- | The document @(vcat xs)@ concatenates all documents @xs@--- vertically with @(\<$$\>)@. If a 'group' undoes the line breaks--- inserted by @vcat@, all documents are directly concatenated.-vcat :: [Doc] -> Doc-vcat = foldDoc (<$$>)+-- vertically with @($$)@.+vcat :: Annotation a => [Doc a] -> Doc a+vcat = foldDoc ($$) -foldDoc :: (Doc -> Doc -> Doc) -> [Doc] -> Doc-foldDoc f [] = mempty+foldDoc :: Annotation a => (Doc a -> Doc a -> Doc a) -> [Doc a] -> Doc a+foldDoc _ [] = mempty foldDoc f ds = foldr1 f ds -- | The document @(x \<+\> y)@ concatenates document @x@ and @y@ with a -- @space@ in between. (infixr 6)-(<+>) :: Doc -> Doc -> Doc+(<+>) :: Annotation a => Doc a -> Doc a -> Doc a x <+> y = x <> space <> y --- | The document @(x \<\/\> y)@ concatenates document @x@ and @y@ with a--- 'softline' in between. This effectively puts @x@ and @y@ either--- next to each other (with a @space@ in between) or underneath each--- other. (infixr 5)-(</>) :: Doc -> Doc -> Doc-x </> y = x <> softline <> y---- | The document @(x \<\/\/\> y)@ concatenates document @x@ and @y@ with--- a 'softbreak' in between. This effectively puts @x@ and @y@ either--- right next to each other or underneath each other. (infixr 5)-(<//>) :: Doc -> Doc -> Doc-x <//> y = x <> softbreak <> y+-- | The document @(x \<\/\> y)@ puts @x@ and @y@ either next to each other+-- (with a @space@ in between) or underneath each other. (infixr 5)+(</>) :: Annotation a => Doc a -> Doc a -> Doc a+x </> y = hang 0 x y --- | The document @(x \<$\> y)@ concatenates document @x@ and @y@ with a--- 'line' in between. (infixr 5)-(<$$$>) :: Doc -> Doc -> Doc-x <$$$> y = x <> line <> y+-- | The document @(x \<\/\/\> y)@ puts @x@ and @y@ either right next+-- to each other (if @x@ fits on a single line) or underneath each+-- other. (infixr 5)+(<//>) :: Annotation a => Doc a -> Doc a -> Doc a+x <//> y = hangWith "" 0 x y -- | The document @(x \<$$\> y)@ concatenates document @x@ and @y@ with--- a @linebreak@ in between. (infixr 5)-(<$$>) :: Doc -> Doc -> Doc-x <$$> y = x <> linebreak <> y---- | The document @softline@ behaves like 'space' if the resulting--- output fits the page, otherwise it behaves like 'line'.------ > softline = group line-softline :: Doc-softline = group line---- | The document @softbreak@ behaves like 'empty' if the resulting--- output fits the page, otherwise it behaves like 'line'.------ > softbreak = group linebreak-softbreak :: Doc-softbreak = group linebreak+-- a linebreak in between. (infixr 5)+(<$$>) :: Annotation a => Doc a -> Doc a -> Doc a+(<$$>) = ($$) -- | Document @(squotes x)@ encloses document @x@ with single quotes -- \"'\".-squotes :: Doc -> Doc+squotes :: Annotation a => Doc a -> Doc a squotes = enclose squote squote -- | Document @(dquotes x)@ encloses document @x@ with double quotes -- '\"'.-dquotes :: Doc -> Doc+dquotes :: Annotation a => Doc a -> Doc a dquotes = enclose dquote dquote -- | Document @(braces x)@ encloses document @x@ in braces, \"{\" and -- \"}\".-braces :: Doc -> Doc+braces :: Annotation a => Doc a -> Doc a braces = enclose lbrace rbrace -- | Document @(parens x)@ encloses document @x@ in parenthesis, \"(\" -- and \")\".-parens :: Doc -> Doc+parens :: Annotation a => Doc a -> Doc a parens = enclose lparen rparen -- | Document @(angles x)@ encloses document @x@ in angles, \"\<\" and -- \"\>\".-angles :: Doc -> Doc+angles :: Annotation a => Doc a -> Doc a angles = enclose langle rangle -- | Document @(brackets x)@ encloses document @x@ in square brackets, -- \"[\" and \"]\".-brackets :: Doc -> Doc+brackets :: Annotation a => Doc a -> Doc a brackets = enclose lbracket rbracket -- | The document @(enclose l r x)@ encloses document @x@ between -- documents @l@ and @r@ using @(\<\>)@.-enclose :: Doc -> Doc -> Doc -> Doc+enclose :: Annotation a => Doc a -> Doc a -> Doc a -> Doc a enclose l r x = l <> x <> r -char :: Char -> Doc+char :: Annotation a => Char -> Doc a char x = text [x] -- | The document @lparen@ contains a left parenthesis, \"(\".-lparen :: Doc+lparen :: Annotation a => Doc a lparen = char '(' -- | The document @rparen@ contains a right parenthesis, \")\".-rparen :: Doc+rparen :: Annotation a => Doc a rparen = char ')' -- | The document @langle@ contains a left angle, \"\<\".-langle :: Doc+langle :: Annotation a => Doc a langle = char '<' -- | The document @rangle@ contains a right angle, \">\".-rangle :: Doc+rangle :: Annotation a => Doc a rangle = char '>' -- | The document @lbrace@ contains a left brace, \"{\".-lbrace :: Doc+lbrace :: Annotation a => Doc a lbrace = char '{' -- | The document @rbrace@ contains a right brace, \"}\".-rbrace :: Doc+rbrace :: Annotation a => Doc a rbrace = char '}' -- | The document @lbracket@ contains a left square bracket, \"[\".-lbracket :: Doc+lbracket :: Annotation a => Doc a lbracket = char '[' -- | The document @rbracket@ contains a right square bracket, \"]\".-rbracket :: Doc+rbracket :: Annotation a => Doc a rbracket = char ']' -- | The document @squote@ contains a single quote, \"'\".-squote :: Doc+squote :: Annotation a => Doc a squote = char '\'' -- | The document @dquote@ contains a double quote, '\"'.-dquote :: Doc+dquote :: Annotation a => Doc a dquote = char '"' -- | The document @semi@ contains a semi colon, \";\".-semi :: Doc+semi :: Annotation a => Doc a semi = char ';' -- | The document @colon@ contains a colon, \":\".-colon :: Doc+colon :: Annotation a => Doc a colon = char ':' -- | The document @comma@ contains a comma, \",\".-comma :: Doc+comma :: Annotation a => Doc a comma = char ',' -- | The document @dot@ contains a single dot, \".\".-dot :: Doc+dot :: Annotation a => Doc a dot = char '.' -- | The document @backslash@ contains a back slash, \"\\\".-backslash :: Doc+backslash :: Annotation a => Doc a backslash = char '\\' -- | The document @equals@ contains an equal sign, \"=\".-equals :: Doc+equals :: Annotation a => Doc a equals = char '=' -----------------------------------------------------------@@ -379,191 +381,57 @@ -- using @line@ for newline characters and @char@ for all other -- characters. It is used instead of 'text' whenever the text contains -- newline characters.-string :: String -> Doc-string "" = mempty-string ('\n':s) = line <> string s-string s = case (span (/='\n') s) of- (xs,ys) -> text xs <> string ys+string :: Annotation a => String -> Doc a+string = vcat . map text . lines -bool :: Bool -> Doc+bool :: Annotation a => Bool -> Doc a bool b = text (show b) -- | The document @(int i)@ shows the literal integer @i@ using -- 'text'.-int :: Int -> Doc+int :: Annotation a => Int -> Doc a int i = text (show i) -- | The document @(integer i)@ shows the literal integer @i@ using -- 'text'.-integer :: Integer -> Doc+integer :: Annotation a => Integer -> Doc a integer i = text (show i) -- | The document @(float f)@ shows the literal float @f@ using -- 'text'.-float :: Float -> Doc+float :: Annotation a => Float -> Doc a float f = text (show f) -- | The document @(double d)@ shows the literal double @d@ using -- 'text'.-double :: Double -> Doc+double :: Annotation a => Double -> Doc a double d = text (show d) -- | The document @(rational r)@ shows the literal rational @r@ using -- 'text'.-rational :: Rational -> Doc+rational :: Annotation a => Rational -> Doc a rational r = text (show r) --------------------------------------------------------------- semi primitive: fill and fillBreak------------------------------------------------------------ --- | The document @(fillBreak i x)@ first renders document @x@. It--- than appends @space@s until the width is equal to @i@. If the--- width of @x@ is already larger than @i@, the nesting level is--- increased by @i@ and a @line@ is appended. When we redefine @ptype@--- in the previous example to use @fillBreak@, we get a useful--- variation of the previous output:------ > ptype (name,tp)--- > = fillBreak 6 (text name) <+> text "::" <+> text tp------ The output will now be:------ @--- let empty :: Doc--- nest :: Indentation -> Doc -> Doc--- linebreak--- :: Doc--- @-fillBreak :: Indentation -> Doc -> Doc-fillBreak f x = width x (\w ->- if (w > f) then nest f linebreak- else spacing (f - w)) ---- | The document @(fill i x)@ renders document @x@. It than appends--- @space@s until the width is equal to @i@. If the width of @x@ is--- already larger, nothing is appended. This combinator is quite--- useful in practice to output a list of bindings. The following--- example demonstrates this.------ > types = [("empty","Doc")--- > ,("nest","Indentation -> Doc -> Doc")--- > ,("linebreak","Doc")]--- >--- > ptype (name,tp)--- > = fill 6 (text name) <+> text "::" <+> text tp--- >--- > test = text "let" <+> align (vcat (map ptype types))------ Which is layed out as:------ @--- let empty :: Doc--- nest :: Indentation -> Doc -> Doc--- linebreak :: Doc--- @-fill :: Indentation -> Doc -> Doc-fill f d = width d (\w ->- if (w >= f) then mempty- else spacing (f - w))--width :: Doc -> (Indentation -> Doc) -> Doc-width d f = column (\k1 -> d <> column (\k2 -> f (k2 - k1)))----------------------------------------------------------------- semi primitive: Alignment and indentation---------------------------------------------------------------- | The document @(indent i x)@ indents document @x@ with @i@ spaces.------ > test = indent 4 (fillSep (map text--- > (words "the indent combinator indents these words !")))------ Which lays out with a page width of 20 as:------ @--- the indent--- combinator--- indents these--- words !--- @-indent :: Indentation -> Doc -> Doc-indent i d = hang i (spacing i <> d)- -- | The hang combinator implements hanging indentation. The document--- @(hang i x)@ renders document @x@ with a nesting level set to the--- current column plus @i@. The following example uses hanging--- indentation for some text:------ > test = hang 4 (fillSep (map text--- > (words "the hang combinator indents these words !")))------ Which lays out on a page with a width of 20 characters as:------ @--- the hang combinator--- indents these--- words !--- @------ The @hang@ combinator is implemented as:------ > hang i x = align (nest i x)-hang :: Indentation -> Doc -> Doc-hang i d = align (nest i d)+-- @(hang i x y)@ either @x@ and @y@ concatenated with @\<+\>@ or @y@+-- below @x@ with an additional indentation of @i@. --- | The document @(align x)@ renders document @x@ with the nesting--- level set to the current column. It is used for example to--- implement 'hang'.------ As an example, we will put a document right above another one,--- regardless of the current nesting level:------ > x $$ y = align (x <$$$> y)------ > test = text "hi" <+> (text "nice" $$ text "world")------ which will be layed out as:------ @--- hi nice--- world--- @-align :: Doc -> Doc-align d = column (\k ->- nesting (\i -> nest (k - i) d)) --nesting might be negative :-)+hang :: Annotation a => Int -> Doc a -> Doc a -> Doc a+hang = hangWith " " --- | The @line@ document advances to the next line and indents to the--- current nesting level. Document @line@ behaves like @(text \" \")@--- if the line break is undone by 'group'.-line :: Doc-line = Line False---- | The @linebreak@ document advances to the next line and indents to--- the current nesting level. Document @linebreak@ behaves like--- 'empty' if the line break is undone by 'group'.-linebreak :: Doc-linebreak = Line True+-- | The hang combinator implements hanging indentation. The document+-- @(hang separator i x y)@ either @x@ and @y@ concatenated with @\<\>+-- text separator \<\>@ or @y@ below @x@ with an additional+-- indentation of @i@.+hangWith :: Annotation a => String -> Int -> Doc a -> Doc a -> Doc a+hangWith separator n x y = groupingBy separator [(0,x), (n,y)] --- | The document @(nest i x)@ renders document @x@ with the current--- indentation level increased by i (See also 'hang', 'align' and--- 'indent').------ > nest 2 (text "hello" <$$$> text "world") <$$$> text "!"------ outputs as:------ @--- hello--- world--- !--- @-nest :: Indentation -> Doc -> Doc-nest i x = Nest i x+space :: Annotation a => Doc a+space = text " " -column, nesting :: (Indentation -> Doc) -> Doc-column f = Column f-nesting f = Nesting f+-- $setup+-- >>> import Data.Monoid+-- >>> import Data.Char
Text/PrettyPrint/Compact/Core.hs view
@@ -1,142 +1,250 @@+{-# LANGUAGE TupleSections #-}+{-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE RecordWildCards #-}-module Text.PrettyPrint.Compact.Core(Doc(..),render,group,flatten,space,spacing,text) where+{-# LANGUAGE ScopedTypeVariables, TypeSynonymInstances, FlexibleContexts, FlexibleInstances, GeneralizedNewtypeDeriving, ViewPatterns, DeriveFunctor, DeriveFoldable, DeriveTraversable, LambdaCase #-}+module Text.PrettyPrint.Compact.Core(Annotation,Layout(..),renderWith,Options(..),groupingBy,Doc,($$)) where -import Data.Monoid+import Prelude ()+import Prelude.Compat as P++import Data.List.Compat (sortOn,groupBy,minimumBy) import Data.Function (on)-import Data.List (partition,minimumBy,sort)-import Data.Either (partitionEithers)+import Data.Semigroup+import Data.Sequence (singleton, Seq, viewl, viewr, ViewL(..), ViewR(..), (|>)) import Data.String+import Data.Foldable (toList)+import Control.Applicative (liftA2)+-- | Annotated string, which consists of segments with separate (or no) annotations.+--+-- We keep annotated segments in a container (list).+-- The annotation is @Maybe a@, because the no-annotation case is common.+--+-- /Note:/ with @Last x@ annotation, the 'annotate' will overwrite all annotations.+--+-- /Note:/ if the list is changed into `Seq` or similar structure+-- allowing fast viewr and viewl, then we can impose an additional+-- invariant that there aren't two consequtive non-annotated segments;+-- yet there is no performance reason to do so.+--+data AS a = AS !Int [(a, String)]+ deriving (Eq,Ord,Show,Functor,Foldable,Traversable) -type Indentation = Int+-- | Tests the invariants of 'AS'+_validAs :: AS a -> Bool+_validAs (AS i s) = lengthInvariant && noNewlineInvariant+ where+ lengthInvariant = i == sum (map (length . snd) s)+ noNewlineInvariant = all (notElem '\n' . snd) s -data Box = Str String | Spacing Indentation | NewLine {-^ not allowed in the input -}+asLength :: AS a -> Int+asLength (AS l _) = l -instance IsString Doc where- fromString = text+-- | Make a non-annotated 'AS'.+mkAS :: Monoid a => String -> AS a+mkAS s = AS (length s) [(mempty, s)] -data Doc = Empty- | Text Box- | Line !Bool -- True <=> when undone by group, do not insert a space- | Cat Doc Doc- | Nest Indentation Doc- | Union Doc Doc -- invariant: first lines of first doc longer than the first lines of the second doc- | Column (Indentation -> Doc)- | Nesting (Indentation -> Doc)+instance Semigroup (AS a) where+ AS i xs <> AS j ys = AS (i + j) (xs <> ys) +newtype L a = L (Seq (AS a)) -- non-empty sequence+ deriving (Eq,Ord,Show,Functor,Foldable,Traversable) -instance Monoid Doc where- mempty = Empty- mappend = Cat+instance Monoid a => Semigroup (L a) where+ L (viewr -> xs :> x) <> L (viewl -> y :< ys) =+ L (xs <> singleton (x <> y) <> indent ys) where -text :: String -> Doc-text = Text . Str+ n = asLength x+ pad = mkAS (P.replicate n ' ')+ indent = if n == 0 then id else fmap (pad <>) -spacing = Text . Spacing+ L _ <> L _ = error "<> @L: invariant violated, Seq is empty" -space = spacing 1+instance Monoid a => Monoid (L a) where+ mempty = L (singleton (mkAS "")) -group :: Doc -> Doc-group x = Union (flatten x) x+instance Layout L where+ text = L . singleton . mkAS+ flush (L xs) = L (xs |> mkAS "")+ annotate a (L s') = L (fmap annotateAS s')+ where annotateAS (AS i s) = AS i (fmap annotatePart s)+ annotatePart (b, s) = (b `mappend` a, s) -flatten :: Doc -> Doc-flatten (Cat x y) = Cat (flatten x) (flatten y)-flatten (Nest i x) = Nest i (flatten x)-flatten (Line noSpace) = if noSpace then Empty else space-flatten (Union x _y) = flatten x-flatten (Column f) = Column (flatten . f)-flatten (Nesting f) = Nesting (flatten . f)-flatten other = other --Empty,Char,Text+renderWithL :: (Monoid a, Monoid r) => Options a r -> L a -> r+renderWithL opts (L xs) = intercalate (toList xs)+ where+ f = optsAnnotate opts+ f' (AS _ s) = foldMap (uncurry f) s+ sep = f mempty "\n" -len :: Box -> Int-len (Str s) = length s-len (Spacing x) = x-len NewLine = 0+ intercalate [] = mempty+ intercalate (y:ys) = f' y `mappend` foldMap (mappend sep . f') ys -data Docs = Nil- | Cons !Indentation Doc Docs+data Options a r = Options+ { optsPageWidth :: !Int -- ^ maximum page width+ , optsAnnotate :: a -> String -> r -- ^ how to annotate the string. /Note:/ the annotation should preserve the visible length of the string.+ } -data Process = Process {overflow :: Indentation- ,curIndent :: !Indentation -- current indentation- ,numToks :: Int -- total number of non-space tokens produced- ,tokens :: [Box] -- tokens produced, in reverse order- ,rest :: !Docs -- rest of the input document to process- }--- The ⟨filtering⟩ step takes all process states starting at the current--- line, and discards those that are dominated. A process state--- dominates another one if it was able to produce more tokens while--- having less indentation. This is implemented by first sorting the--- process states by following 'measure', and then applying the--- 'filtering' function.+class Layout d where+ text :: Monoid a => String -> d a+ flush :: Monoid a => d a -> d a+ -- | `<>` new annotation to the 'Doc'.+ --+ -- Example: 'Any True' annotation will transform the rendered 'Doc' into uppercase:+ --+ -- >>> let r = putStrLn . renderWith defaultOptions { optsAnnotate = \a x -> if a == Any True then map toUpper x else x }+ -- >>> r $ text "hello" <$$> annotate (Any True) (text "world")+ -- hello+ -- WORLD+ --+ annotate :: forall a. Monoid a => a -> d a -> d a -measure :: Process -> (Indentation, Int)-measure Process{..} = (curIndent, negate numToks)+-- type parameter is phantom.+data M a = M {height :: Int,+ lastWidth :: Int,+ maxWidth :: Int+ }+ deriving (Show,Eq,Ord,Functor,Foldable,Traversable) -instance Eq Process where- (==) = (==) `on` measure-instance Ord Process where- compare = compare `on` measure+instance Semigroup (M a) where+ a <> b =+ M {maxWidth = max (maxWidth a) (maxWidth b + lastWidth a),+ height = height a + height b,+ lastWidth = lastWidth a + lastWidth b} -filtering :: [Process] -> [Process]-filtering (x:y:xs) | numToks x >= numToks y = filtering (x:xs)- | otherwise = x:filtering (y:xs)-filtering xs = xs+instance Monoid a => Monoid (M a) where+ mempty = text ""+ mappend = (<>) -renderAll :: Double -> Indentation -> Doc -> [Box]-renderAll rfrac w doc = reverse $ loop [Process 0 0 0 [] $ Cons 0 doc Nil]- where- loop ps = case dones of- ((_,done):_) -> done -- here we could attempt to do a better choice. Seems to be fine for now.- [] -> case conts of- (_:_) -> loop $ filtering $ sort $ conts -- See the comment ⟨filtering⟩ above- [] -> case conts'over of- (_:_) -> loop [minimumBy (compare `on` overflow) conts'over]- [] -> case dones'over of- ((_,done):_) -> done- [] -> [Str "Pretty print: Panic"]+instance Layout M where+ text s = M {height = 0, maxWidth = length s, lastWidth = length s}+ flush a = M {maxWidth = maxWidth a,+ height = height a + 1,+ lastWidth = 0}+ annotate _ M{..} = M{..}+class Poset a where+ (≺) :: a -> a -> Bool - where- -- advance all processes by one line (if possible)- ps' = concatMap (\Process{..} -> rall numToks tokens curIndent curIndent rest) ps- -- Have some processes reached the end? With some overflow?- (dones0,conts0) = partitionEithers ps'- (conts,conts'over) = partition (\p -> overflow p <= 0) conts0- (dones,dones'over) = partition (\(o,_) -> o <= 0) dones0 - -- r :: the ribbon width in characters- r = max 0 (min w (round (fromIntegral w * rfrac)))+instance Poset (M a) where+ M c1 l1 s1 ≺ M c2 l2 s2 = c1 <= c2 && l1 <= l2 && s1 <= s2 - -- Automatically inserted spacing does not count as doing more production.- count (Spacing _) = 0- count (Str _) = 1+mergeOn :: Ord b => (a -> b) -> [a] -> [a] -> [a]+mergeOn m = go+ where+ go [] xs = xs+ go xs [] = xs+ go (x:xs) (y:ys)+ | m x <= m y = x:go xs (y:ys)+ | otherwise = y:go (x:xs) ys - -- Compute the state(s) after reaching the end of line.- -- rall :: n = indentation of current line- -- k = current column- -- (ie. (k >= n) && (k - n == count of inserted characters)- rall :: Int -> [Box] -> Indentation -> Indentation -> Docs -> [Either (Indentation,[Box]) Process]- rall nts ts n k ds0 = case ds0 of- Nil -> [Left $ (overflow,ts)] -- Done!- Cons i d ds -> case d of- Empty -> rall nts ts n k ds- Text s -> let k' = k+len s in seq k' (rall (nts+count s) (s:ts) n k' ds)- Line _ -> [Right $ Process overflow i nts (Spacing i:NewLine:ts) ds] -- "yield" when the end of line is reached- Cat x y -> rall nts ts n k (Cons i x (Cons i y ds))- Nest j x -> let i' = i+j in seq i' (rall nts ts n k (Cons i' x ds))- Union x y -> rall nts ts n k (Cons i x ds) ++ rall nts ts n k (Cons i y ds)- Column f -> rall nts ts n k (Cons i (f k) ds)- Nesting f -> rall nts ts n k (Cons i (f i) ds)- where overflow = negate $ min (w - k) (r - k + n)+mergeAllOn :: Ord b => (a -> b) -> [[a]] -> [a]+mergeAllOn _ [] = []+mergeAllOn m (x:xs) = mergeOn m x (mergeAllOn m xs) +bestsOn :: forall a b. (Poset b, Ord b)+ => (a -> b) -- ^ measure+ -> [[a]] -> [a]+bestsOn m = paretoOn' m [] . mergeAllOn m -layout :: [Box] -> String-layout [] = []-layout (Str s:xs) = s ++ layout xs-layout (NewLine:xs) = '\n' : layout xs-layout (Spacing x:xs) = replicate x ' ' ++ layout xs+-- | @paretoOn m = paretoOn' m []@+paretoOn' :: Poset b => (a -> b) -> [a] -> [a] -> [a]+paretoOn' _ acc [] = P.reverse acc+paretoOn' m acc (x:xs) = if any ((≺ m x) . m) acc+ then paretoOn' m acc xs+ else paretoOn' m (x:acc) xs+ -- because of the ordering, we have that+ -- for all y ∈ acc, y <= x, and thus x ≺ y+ -- is false. No need to refilter acc. -render :: Double -> Indentation -> Doc -> String-render rfrac w d = do- layout $ renderAll rfrac w d+-- list sorted by lexicographic order for the first component+-- function argument is the page width+newtype ODoc a = MkDoc {fromDoc :: Int -> [(Pair M L a)]}+ deriving Functor -instance Show Doc where- show = render 0.8 80+instance Monoid a => Semigroup (ODoc a) where+ MkDoc xs <> MkDoc ys = MkDoc $ \w -> bestsOn frst [ discardInvalid w [x <> y | y <- ys w] | x <- xs w]++discardInvalid w = quasifilter (fits w . frst)++quasifilter _ [] = []+quasifilter p zs = let fzs = filter p zs+ in if null fzs -- in case that there are no valid layouts, we take a narrow one.+ then [minimumBy (compare `on` (maxWidth . frst)) zs]+ else fzs++instance Monoid a => Monoid (ODoc a) where+ mempty = text ""+ mappend = (<>)++fits :: Int -> M a -> Bool+fits w x = maxWidth x <= w++instance Layout ODoc where+ text s = MkDoc $ \_ -> [text s]+ flush (MkDoc xs) = MkDoc $ \w -> fmap flush (xs w)+ annotate a (MkDoc xs) = MkDoc $ \w -> fmap (annotate a) (xs w)++renderWith :: (Monoid r, Annotation a)+ => Options a r -- ^ rendering options+ -> ODoc a -- ^ renderable+ -> r+renderWith opts d = case xs of+ [] -> error "No suitable layout found."+ ((_ :-: x):_) -> renderWithL opts x+ where+ pageWidth = optsPageWidth opts+ xs = discardInvalid pageWidth (fromDoc d pageWidth)++onlySingleLine :: [Pair M L a] -> [Pair M L a]+onlySingleLine = takeWhile (\(M{..} :-: _) -> height == 0)++spaces :: (Monoid a,Layout l) => Int -> l a+spaces n = text $ replicate n ' '+++-- | The document @(x \$$> y)@ concatenates document @x@ and @y@ with+-- a linebreak in between. (infixr 5)+($$) :: (Layout d, Monoid a, Semigroup (d a)) => d a -> d a -> d a+a $$ b = flush a <> b++second f (a,b) = (a, f b)++groupingBy :: Monoid a => String -> [(Int,Doc a)] -> Doc a+groupingBy _ [] = mempty+groupingBy separator ms = MkDoc $ \w ->+ let mws = map (second (($ w) . fromDoc)) ms+ (_,lastMw) = last mws+ hcatElems = map (onlySingleLine . snd) (init mws) ++ [lastMw] -- all the elements except the first must fit on a single line+ vcatElems = map (\(indent,x) -> map (spaces indent <>) x) mws+ horizontal = discardInvalid w $ foldr1 (liftA2 (\x y -> x <> text separator <> y)) hcatElems+ vertical = foldr1 (\xs ys -> bestsOn frst [[x $$ y | y <- ys] | x <- xs]) vcatElems+ in bestsOn frst [horizontal,vertical]++data Pair f g a = (:-:) {frst :: f a, scnd :: g a}+ deriving (Functor,Foldable,Traversable)++instance (Semigroup (f a), Semigroup (g a)) => Semigroup (Pair f g a) where+ (x :-: y) <> (x' :-: y') = (x <> x') :-: (y <> y')+instance (Monoid (f a), Monoid (g a)) => Monoid (Pair f g a) where+ mempty = mempty :-: mempty++instance (Layout a, Layout b) => Layout (Pair a b) where+ text s = text s :-: text s+ flush (a:-:b) = (flush a:-: flush b)+ annotate x (a:-:b) = (annotate x a:-:annotate x b)++instance Monoid a => IsString (Doc a) where+ fromString = text++type Annotation a = (Monoid a)+type Doc = ODoc++-- tt :: Doc ()+-- tt = groupingBy " " $ map (4,) $ +-- ((replicate 4 $ groupingBy " " (map (4,) (map text ["fw"]))) +++-- [groupingBy " " (map (0,) (map text ["fw","arstnwfyut","arstin","arstaruf"]))])++-- $setup+-- >>> import Text.PrettyPrint.Compact+-- >>> import Data.Monoid+-- >>> import Data.Char
+ bench/Benchmark.hs view
@@ -0,0 +1,85 @@+module Main (main) where++import Prelude ()+import Prelude.Compat++import Control.DeepSeq (force)+import Control.Exception (evaluate)+import Data.Aeson (Value (..), decode)+import Data.Foldable (toList)++import qualified Criterion.Main as C+import qualified Data.HashMap.Lazy as H+import qualified Data.ByteString.Lazy as BSL+import qualified Data.Text.Lazy as TL+import qualified Data.Text.Lazy.Builder as TLB++import qualified Text.PrettyPrint.Compact as PC+import qualified Text.PrettyPrint.HughesPJ as HPJ+import qualified Text.PrettyPrint.Leijen as WL++prettiestJSON :: Value -> PC.Doc ()+prettiestJSON (Bool True) = PC.text "true"+prettiestJSON (Bool False) = PC.text "false"+prettiestJSON (Object o) = PC.encloseSep (PC.text "{") (PC.text "}") (PC.text ",") (map prettyKV $ H.toList o)+ where prettyKV (k,v) = PC.text (show k) PC.<> PC.text ":" PC.<+> prettiestJSON v+prettiestJSON (String s) = PC.string (show s)+prettiestJSON (Array a) = PC.encloseSep (PC.text "[") (PC.text "]") (PC.text ",") (map prettiestJSON $ toList a)+prettiestJSON Null = PC.mempty+prettiestJSON (Number n) = PC.text (show n)++pcRenderText :: Monoid a => PC.Doc a -> TL.Text+pcRenderText = TLB.toLazyText . PC.renderWith PC.defaultOptions+ { PC.optsAnnotate = \_ -> TLB.fromString }++wlJSON :: Value -> WL.Doc+wlJSON (Bool True) = WL.text "true"+wlJSON (Bool False) = WL.text "false"+wlJSON (Object o) = WL.encloseSep (WL.text "{") (WL.text "}") (WL.text ",") (map prettyKV $ H.toList o)+ where prettyKV (k,v) = WL.text (show k) WL.<> WL.text ":" WL.<+> wlJSON v+wlJSON (String s) = WL.string (show s)+wlJSON (Array a) = WL.encloseSep (WL.text "[") (WL.text "]") (WL.text ",") (map wlJSON $ toList a)+wlJSON Null = WL.empty+wlJSON (Number n) = WL.text (show n)++wlRender :: WL.Doc -> String+wlRender d = WL.displayS (WL.renderPretty 1 80 d) ""++hpjEncloseSep :: HPJ.Doc -> HPJ.Doc -> HPJ.Doc -> [HPJ.Doc] -> HPJ.Doc+hpjEncloseSep open close sep list = open HPJ.<> HPJ.sep (HPJ.punctuate sep list) HPJ.<> close++hpjJSON :: Value -> HPJ.Doc+hpjJSON (Bool True) = HPJ.text "true"+hpjJSON (Bool False) = HPJ.text "false"+hpjJSON (Object o) = hpjEncloseSep (HPJ.text "{") (HPJ.text "}") (HPJ.text ",") (map prettyKV $ H.toList o)+ where prettyKV (k,v) = HPJ.text (show k) HPJ.<> HPJ.text ":" HPJ.<+> hpjJSON v+hpjJSON (String s) = HPJ.text (show s)+hpjJSON (Array a) = hpjEncloseSep (HPJ.text "[") (HPJ.text "]") (HPJ.text ",") (map hpjJSON $ toList a)+hpjJSON Null = HPJ.empty+hpjJSON (Number n) = HPJ.text (show n)++readJSONValue :: FilePath -> IO Value+readJSONValue fname = do+ contents <- BSL.readFile fname+ let Just inpJson = decode contents+ return inpJson++main :: IO ()+main = do+ smallValue <- evaluate . force =<< readJSONValue "bench/small.json"+ bigValue <- evaluate . force =<< readJSONValue "bench/big.json"++ C.defaultMain+ [ C.bgroup "small"+ [ C.bench "pretty-compact" $ C.nf (PC.render . prettiestJSON) smallValue+ , C.bench "pretty-compact Text" $ C.nf (pcRenderText . prettiestJSON) smallValue+ , C.bench "pretty" $ C.nf (HPJ.render . hpjJSON) smallValue+ , C.bench "wl-pprint" $ C.nf (wlRender . wlJSON) smallValue+ ]+ , C.bgroup "big"+ [ C.bench "pretty-compact" $ C.nf (PC.render . prettiestJSON) bigValue+ , C.bench "pretty-compact Text" $ C.nf (pcRenderText . prettiestJSON) bigValue+ , C.bench "pretty" $ C.nf (HPJ.render . hpjJSON) bigValue + , C.bench "wl-pprint" $ C.nf (wlRender . wlJSON) bigValue+ ]+ ]
pretty-compact.cabal view
@@ -1,5 +1,6 @@+Cabal-Version: 3.4 name: pretty-compact-version: 1.0+version: 3.1 synopsis: Pretty-printing library description: This package contains a pretty-printing library, a set of API's@@ -9,21 +10,46 @@ . This library produces more compact outputs than both Wadler-Leijen or Hughes-PJ algorithms, at the expense of computational ressources.- The API is the same as Wadler-Leijen's.-license: GPL+ The core API is based on Hughes-PJ, but some combinators of the Leijen API are implemented as well.+license: LGPL-3.0-only license-file: LICENSE category: Text maintainer: Jean-Philippe Bernardy <jeanphilippe.bernardy@gmail.com> build-type: Simple-Cabal-Version: >= 1.8 source-repository head type: git location: http://github.com/jyp/prettiest.git + Library- exposed-modules:- Text.PrettyPrint.Compact- Text.PrettyPrint.Compact.Core- build-depends:- base >= 3 && < 5+ exposed-modules:+ Text.PrettyPrint.Compact+ Text.PrettyPrint.Compact.Core+ build-depends:+ base >= 4.6 && < 5,+ base-compat >= 0.9.3 && <0.12,+ containers >= 0 && <666++ if !impl(ghc >= 8.0)+ build-depends: semigroups >= 0 && <666++ other-extensions:+ LambdaCase++benchmark pretty-comparison+ type: exitcode-stdio-1.0+ hs-source-dirs: bench+ main-is: Benchmark.hs+ build-depends:+ aeson,+ base,+ base-compat,+ bytestring,+ criterion,+ deepseq,+ pretty,+ pretty-compact,+ text,+ unordered-containers,+ wl-pprint