This has been mostly adapted from https://hackage.haskell.org/package/prettyprinter-1.5.1/docs/Data-Text-Prettyprint-Doc.html.
Lists functions that operator on and/or return a Doc<A>
Primitives and simple combinators to create and alter documents:
fun nil(): Doc<Nothing>
Creates an empty document. The resulting document still has a height of 1. So in combination with e.g. vCat it can still render as a newline.
listOf("hello".text(), nil(), "world".text()).vCat()
// hello
//
// world
fun String.text(): Doc<Nothing>
Create a document which contains the string as text.
The string should never contain newlines as that would lead to unwanted behaviour. If it can contain newlines, use doc instead.
fun String.doc(): Doc<Nothing>
Create a document which contains the string as text and also handles newlines by replacing them with hardLine()
fun line(): Doc<Nothing>
This document advances the layout to the next line and sets the indentation to the current nesting level.
"This is rendered in".text() + line() + "two lines".text()
// This is rendered in
// two lines
If inside a group the newline can be undone and replaced by a space.
fun lineBreak(): Doc<Nothing>
This document advances the layout to the next line and sets the indentation to the current nesting level.
"This is rendered in".text() + lineBreak() + "two lines".text()
// This is rendered in
// two lines
If inside a group the newline can be undone and replaced by nil.
fun softLine(): Doc<Nothing>
softLine behaves like a space if the layout fits, otherwise like a newline.
("This is rendered in".text() + softLine() + "one line".text()).pretty(maxWidth = 80)
// This is rendered in one line
("This is rendered in".text() + softLine() + "two lines".text()).pretty(maxWidth = 20)
// This is rendered in
// two lines
softLine() == line().group()
fun softLineBreak(): Doc<Nothing>
softLineBreak behavious like nil if the layout fits, otherwise like a newline.
("This is rendered in".text() + softLineBreak() + "one line".text()).pretty(maxWidth = 80)
// This is rendered inone line
("This is rendered in".text() + softLineBreak() + "two lines".text()).pretty(maxWidth = 20)
// This is rendered in
// two lines
softLineBreak() == lineBreak().group()
fun hardLine(): Doc<Nothing>
Insert a new line which cannot be flattened by group.
fun <A> Doc<A>.nest(i: Int): Doc<A>
Change the layout of a document by increasing the indentation level (of the following lines) by i
. Negative values are allowed and will decrease the nesting level.
listOf(listOf("Hello".text(), "World".text()).vCat().nest(4), "!".text()).vCat()
// Hello
// World
// !
See also:
fun <A> Doc<A>.group(): Doc<A>
Group tries to flatten a document to a single line. If this fails (on hardLine's) or does not fit, the document will be layed out unchanged. This function is the key to building adapting layouts.
See vCat, line, flatAlt for examples.
fun <A> Doc<A>.flatAlt(fallback: Doc<A>): Doc<A>
Renders a document as is by default, but when inside a group it will render the fallback
. TODO note on how fallbacks affect the rendering and what invariants it should hold.
line() == hardLine().flatAlt(space())
lineBreak() == hardLine().flatAlt(nil())
This section describes functions that can align their output relative to the current cursor position as opposed to nest which always aligns to the current nesting level. This means that in terms of the Wadler-Leijen algorithm (which is what is used to layout the document), these functions do not produce an optimal result. They are however immensly useful in practice. Some of these functions are also more expensive to use at the top level, but should be fine in most cases.
fun <A> Doc<A>.align(): Doc<A>
Align will lay out the document with the current nesting level set to the current column.
"Hello".text() spaced listOf("World".text(), "there".text()).vSep()
// Hello World
// there
"Hello".text() spaced listOf("World".text(), "there".text()).vSep().align()
// Hello World
// there
fun <A> Doc<A>.hang(i: Int): Doc<A>
Hang lays out the document x with a nesting level set to the current column plus i. Negative values are allowed, and decrease the nesting level.
val doc = "Indenting these words with hang".reflow()
("prefix".text() spaced doc.hang(4)).pretty(maxWidth = 24)
// prefix Indenting
// these
// words with
// hang
val doc = "Indenting these words with nest".reflow()
("prefix".text() spaced doc.nest(4)).pretty(maxWidth = 24)
// prefix Indenting
// these
// words with
// nest
hang(i) == nest(i).align()
fun <A> Doc<A>.indent(i: Int): Doc<A>
Indents the document with i
spaces, starting from the current column.
val doc = "Indent these words using indent".reflow()
("prefix".text() spaced doc.indent(4)).pretty(maxWidth = 24)
// prefix Indent
// these
// words
// using
// indent
fun <A> List<Doc<A>>.encloseSep(left: Doc<A>, right: Doc<A>, sep: Doc<A>): Doc<A>
Concatenate the documents between left
and right
and add sep
in between.
val encloseSep = "list".text() spaced listOf(1.doc(), 2.doc(), 10.doc(), 1698.doc())
.encloseSep(lBracket(), rBracket(), comma())
.align()
encloseSep.pretty(maxWidth = 80)
// list [1,2,10,1698]
encloseSep.pretty(maxWidth = 10)
// list [1
// ,2
// ,10
// ,1698]
If you want to put the separator as a suffix, take a look at puncuate.
fun <A> List<Doc<A>>.list(): Doc<A>
Variant of encloseSep for list-like output.
val listDoc = listOf(1.doc(), 10.doc(), 100.doc(), 1000.doc(), 10000.doc()).list()
listDoc.pretty(maxWidth = 80)
// [1, 10, 100, 1000, 10000]
listDoc.pretty(maxWidth = 10)
// [ 1
// , 10
// , 100
// , 1000
// , 10000]
fun <A> List<Doc<A>>.tupled(): Doc<A>
Variant of encloseSep for tuple-like output.
val tupleDoc = listOf(1.doc(), 10.doc(), 100.doc(), 1000.doc(), 10000.doc()).tupled()
tupleDoc.pretty(maxWidth = 80)
// (1, 10, 100, 1000, 10000)
tupleDoc.pretty(maxWidth = 10)
// ( 1
// , 10
// , 100
// , 1000
// , 10000)
fun <A> List<Doc<A>>.semiBraces(): Doc<A>
Variant of encloseSep for braced output.
val bracedDoc = listOf(1.doc(), 10.doc(), 100.doc(), 1000.doc(), 10000.doc()).semiBraces()
bracedDoc.pretty(maxWidth = 80)
// {1, 10, 100, 1000, 10000}
bracedDoc.pretty(maxWidth = 10)
// { 1
// , 10
// , 100
// , 1000
// , 10000}
fun <A> Doc<A>.plus(other: Doc<A>): Doc<A>
Concatenate two documents.
"Hello".text() + space() + "world".text()
// Hello world
fun <A> Doc<A>.spaced(other: Doc<A>): Doc<A>
Concatenate two documents with a space in between.
"Hello".text() spaced "world".text()
// Hello world
fun <A> Doc<A>.line(other: Doc<A>): Doc<A>
Concatenate two documents with a line in between.
"Hello".text() line "world".text()
// Hello
// world
fun <A> Doc<A>.lineBreak(other: Doc<A>): Doc<A>
Concatenate two documents with a lineBreak in between.
"Hello".text() lineBreak "world".text()
// Hello
// world
fun <A> Doc<A>.softLine(other: Doc<A>): Doc<A>
Concatenate two documents with a softLine in between.
"Hello".text() softLine "world".text()
// Hello world
fun <A> Doc<A>.softLineBreak(other: Doc<A>): Doc<A>
Concatenate two documents with a softLineBreak in between.
"Hello ".text() softLineBreak "world".text()
// Hello world
These functions generalize working over lists of documents. They are separated into two families: sep
and cat
where sep
uses line and cat
uses lineBreak to separate content.
fun <A> List<Doc<A>>.foldDoc(f: (Doc<A>, Doc<A>) -> Doc<A>): Doc<A>
Concatenate the documents in a list given a binary function f
. Almost all other operators on lists are implemented using this function.
fun <F, A> Kind<F, Doc<A>>.foldDoc(FF: Foldable<F>, f: (Doc<A>, Doc<A>) -> Doc<A>): Doc<A>
Kind polymorphic version of foldDoc that allows folding any foldable structure.
Functions from this list will use line to insert linebreaks. This means it when used with group they will be replaced by spaces.
fun <A> List<Doc<A>>.hSep(): Doc<A>
Concatenate all documents using spaced. This never introduces newlines on its own.
val hSepDoc = listOf("Hello".text(), "there!".text(), "- Kenobi".text()).hSep()
hSepDoc.pretty(maxWidth = 80)
// Hello there! - Kenobi
hSepDoc.pretty(maxWidth = 10)
// Hello there! - Kenobi
For a layout that automatically adds linebreaks, consider fillSep instead.
fun <A> List<Doc<A>>.vSep(): Doc<A>
Concatenate all documents vertically using line.
val vSepDoc = listOf("Text".text(), "to".text(), "lay".text(), "out".text()).vSep()
"prefix".text() spaced vSepDoc
// prefix Text
// to
// lay
// out
"prefix".text() spaced vSepDoc.align()
// prefix Text
// to
// lay
// out
Because later grouping a vSep
is so common, sep is a built-in which does that.
fun <A> List<Doc<A>>.fillSep(): Doc<A>
Concatenate all documents with softLine. The resulting document will be as wide as possible, but introduce newlines if it no longer fits.
val fillSepDoc = listOf(
"Very".text(), "long".text(), "text".text(),
"to".text(), "lay".text(), "out.".text(),
"Hello".text(), "world".text(), "example!".text()
).fillSep()
fillSepDoc.pretty(maxWidth = 80)
// Very long text to lay out. Hello
// world example!
fillSepDoc.pretty(maxWidth = 40)
// Very long text
// to lay out.
// Hello world
// example!
fun <A> List<Doc<A>>.sep(): Doc<A>
Concatenate all documents with softLine, but calls group on the resulting document to flatten it. This is different from [vSep] because it tries to lay out horizontally first, instead of just vertically. This is also different from fillSep, because it will also flatten the individual documents instead of just the separator.
val sepDoc = listOf("Text".text() line "that".text(), "spans".text(), "multiple".text(), "lines".text()).sep()
sepDoc.pretty(maxWidth = 80)
// Text that spans multiple lines
sepDoc.pretty(maxWidth = 20)
// Text
// that
// spans
// multiple
// lines
Functions from this list will use lineBreak to insert linebreaks. This means it when used with group they will be replaced by nil.
fun <A> List<Doc<A>>.hCat(): Doc<A>
Concatenate all documents using plus. This never introduces newlines on its own.
val hCatDoc = listOf("Hello".text(), "there!".text(), "- Kenobi".text()).hCat()
hCatDoc.pretty(maxWidth = 80)
// Hellothere!- Kenobi
hCatDoc.pretty(maxWidth = 10)
// Hellothere!- Kenobi
For a layout that automatically adds linebreaks, consider fillCat instead.
fun <A> List<Doc<A>>.vCat(): Doc<A>
Concatenate all documents vertically using lineBreak.
val vCatDoc = listOf("Text".text(), "to".text(), "lay".text(), "out".text()).vCat()
"prefix".text() spaced vCatDoc
// prefix Text
// to
// lay
// out
"prefix".text() spaced vCatDoc.align()
// prefix Text
// to
// lay
// out
Because later grouping a vCat
is so common, cat is a built-in which does that.
fun <A> List<Doc<A>>.fillCat(): Doc<A>
Concatenate all documents with softLineBreak. The resulting document will be as wide as possible, but introduce newlines if it no longer fits.
val fillCatDoc = listOf(
"Very".text(), "long".text(), "text".text(),
"to".text(), "lay".text(), "out.".text(),
"Hello".text(), "world".text(), "example!".text()
).fillCat()
fillCatDoc.pretty(maxWidth = 80)
// Verylongtexttolayout.Helloworld
// example!
fillCatDoc.pretty(maxWidth = 40)
// Verylongtextto
// layout.Hello
// worldexample!
fun <A> List<Doc<A>>.cat(): Doc<A>
Concatenate all documents with softLineBreak, but calls group on the resulting document to flatten it. This is different from vCat because it tries to lay out horizontally first, instead of just vertically. This is also different from fillCat, because it will also flatten the individual documents instead of just the separator.
val catDoc = listOf("Text".text() line "that".text(), "spans".text(), "multiple".text(), "lines".text()).cat()
catDoc.pretty(maxWidth = 80)
// Text thatspansmultiplelines
catDoc.pretty(maxWidth = 20)
// Text
// that
// spans
// multiple
// lines
fun <A> List<Doc<A>>.punctuate(p: Doc<A>): List<Doc<A>>
Append p
to all but the last document.
val punctuateDoc = listOf("Hello".text(), "world".text(), "example".text())
.punctuate(comma())
punctuateDoc.hSep().pretty(maxWidth = 80)
// Hello, world, example
punctuateDoc.vSep().pretty(maxWidth = 20)
// Hello,
// world,
// example
If you want to but elements infront of the documents you should use encloseSep.
Layout documents with access to context such as the current position, the available page width or the current nesting.
fun <A> column(f: (Int) -> Doc<A>): Doc<A>
Layout a document with information on what the current column is. Used to implement align.
column { c -> "Columns are".text() spaced c.doc() + "-based".text() }
// Columns are 0-based
val colDoc = "prefix".text() spaced column { pipe() spaced " <- column".text() spaced it.doc() }
listOf(0, 4, 8).map { colDoc.indent(it) }.vSep()
// prefix | <- column 7
// prefix | <- column 11
// prefix | <- column 15
fun <A> nesting(f: (Int) -> Doc<A>): Doc<A>
Layout a document with information on what the current nesting is. Used to implement align.
val nestingDoc = "prefix".text() spaced nesting { ("Nested:".text() spaced it.doc()).brackets() }
listOf(0, 4, 8).map { nestingDoc.indent(it) }.vSep()
// prefix [Nested: 0]
// prefix [Nested: 4]
// prefix [Nested: 8]
fun <A> Doc<A>.width(f: (Int) -> Doc<A>): Doc<A>
Layout a document with information on what the current column width is.
fun <A> annotate(d: Doc<A>): Doc<A> = d.brackets().width { w -> " <- width:".text() spaced w.doc() }
listOf(
"---".text(), "------".text(), "---".text().indent(3),
listOf("---".text(), "---".text().indent(4)).vSep()
).map(::annotate).vSep().align()
// [---] <- width: 5
// [------] <- width: 8
// [ ---] <- width: 8
// [---
// ---] <- width: 8
fun <A> pageWidth(f: (PageWidth) -> Doc<A>): Doc<A>
Layout a document with information on what the pagewidth settings are.
fun PageWidth.doc(): Doc<Nothing> = when (this) {
PageWidth.Unbounded -> "Unbounded".text()
is PageWidth.Available ->
"Max width:".text() spaced maxWidth.doc() spaced ", ribbonFrac:".text() spaced ribbonFract.doc()
}
val _pwDoc = "prefix".text() spaced pageWidth { pw -> pw.doc().brackets() }
val pwDoc = listOf(0, 4, 8).map { _pwDoc.indent(it) }.vSep()
pwDoc.pretty(maxWidth = 32)
// prefix [Max width: 32 , ribbonFrac: 0.4]
// prefix [Max width: 32 , ribbonFrac: 0.4]
// prefix [Max width: 32 , ribbonFrac: 0.4]
pwDoc.layoutPretty(PageWidth.Unbounded).renderString()
// prefix [Unbounded]
// prefix [Unbounded]
// prefix [Unbounded]
Fill up available space.
fun <A> Doc<A>.fill(i: Int): Doc<A>
Lay out a document and append spaces until it has a width of i
.
val types = listOf("empty" to "Doc", "nest" to "Int -> Doc -> Doc", "fillSep" to "[Doc] -> Doc")
"let".text() spaced types.map { (n, tp) -> n.text().fill(5) spaced "::".text() spaced tp.text() }
.vCat().align()
// let empty :: Doc
// nest :: Int -> Doc -> Doc
// fillSep :: [Doc] -> Doc
fun <A> Doc<A>.fillBreak(i: Int): Doc<A>
Lay out a document and append spaces until it has a width of i
and insert a lineBreak if it exceeds the desired width.
val types = listOf("empty" to "Doc", "nest" to "Int -> Doc -> Doc", "fillSep" to "[Doc] -> Doc")
"let".text() spaced types.map { (n, tp) -> n.text().fillBreak(5) spaced "::".text() spaced tp.text() }
.vCat().align()
// let empty :: Doc
// nest :: Int -> Doc -> Doc
// fillSep
// :: [Doc] -> Doc
fun <A> Doc<A>.enclose(l: Doc<A>, r: Doc<A>): Doc<A>
Surround the document with l
and r
.
"Hello".text().enclose("_".text(), "_".text())
// _Hello_
Common functions to enclose documents.
fun <A> Doc<A>.sQuotes(): Doc<A>
"·".text().sQuotes()
// '·'
fun <A> Doc<A>.dQuotes(): Doc<A>
"·".text().dQuotes()
// "·"
fun <A> Doc<A>.parens(): Doc<A>
"·".text().parens()
// (·)
fun <A> Doc<A>.angles(): Doc<A>
"·".text().angles()
// <·>
fun <A> Doc<A>.braces(): Doc<A>
"·".text().braces()
// {·}
fun <A> Doc<A>.brackets(): Doc<A>
"·".text().brackets()
// [·]
fun <A> Doc<A>.d9966quotes(): Doc<A>
"·".text().d9966quotes()
// „·“
fun <A> Doc<A>.d6699quotes(): Doc<A>
"·".text().d6699quotes()
// “·”
fun <A> Doc<A>.s96quotes(): Doc<A>
"·".text().s96quotes()
// ‚·‘
fun <A> Doc<A>.s69quotes(): Doc<A>
"·".text().s69quotes()
// ‘·’
fun <A> Doc<A>.dGuillemetsOut(): Doc<A>
"·".text().dGuillemetsOut()
// «·»
fun <A> Doc<A>.dGuillemetsIn(): Doc<A>
"·".text().dGuillemetsIn()
// »·«
fun <A> Doc<A>.sGuillemetsOut(): Doc<A>
"·".text().sGuillemetsOut()
// ‹·›
fun <A> Doc<A>.sGuillemetsIn(): Doc<A>
"·".text().sGuillemetsIn()
// ›·‹
Some constants for ascii and unicode characters
Constants for ASCII characters can be found here.
Constants for uncode characters can be found here.
Combinators that deal with annotations. Learn more about annotations here.
fun <A> Doc<A>.annotate(a: A): Doc<A>
Add an annotation to a document. This can be used to supply additional context to the renderer, e.g. color. This is only relevant for documents that are rendered with a custom renderer. renderString and derivates will ignore annotations.
fun <A> Doc<A>.unAnnotate(): Doc<Nothing>
Remove annotations from a document.
fun <A, B> Doc<A>.reAnnotate(f: (A) -> B): Doc<B>
Alter annotations of a document.
fun <A, B> Doc<A>.alterAnnotations(f: (A) -> List<B>): Doc<B>
Alter annotations of a document with the ability to either remove the annotation or add one or more annotations to the document.
–
fun String.words(): List<Doc<Nothing>>
Split a string into single words and convert them to Doc
using doc.
"Hello world string".words().vSep()
// Hello
// world
// string
fun String.reflow(): Doc<Nothing>
Replace spaces in a string with softLine. This attempts to lay out the string as wide as possible, but introduces newlines when it exceeds max-width.
"Hello world string that is slightly longer than normal".reflow()
// Hello world string that is
// slightly longer than normal
fun <A> Doc<A>.fuse(shallow: Boolean): Doc<A>
Fuse text nodes in a document if possible. If you document consists of many "H".text() + "E".text() ...
operations, this will attempt to fuse the text together to reduce the overhead for the layout algorithm. If you use deep fusion shallow => false
, this will also try to fuse documents returned by [column], [nesting] and [pageWidth]. A fused document is always equal to an unfused on when rendered. Benchmark your code to see if this benefits you or not!
Operations that use SimpleDoc
instead of Doc
. To learn more about SimpleDoc
go here.
fun Doc<A>.layoutPretty(pw: PageWidth): SimpleDoc<A>
The default layout algorithm which calculates a layout for a document.
fun Doc<A>.layoutSmart(pw: PageWidth): SimpleDoc<A>
A layout algorithm which uses more lookahead than layoutPretty. It is slightly slower, but can produce nicer results.
fun <A> Doc<A>.f(): Doc<A> = (("fun(".text() softLineBreak this) + rParen()).hang(2)
val lSmartDoc = listOf("exempli".text(), "gratia".text()).list().align().f().f().f().f().f()
val hr = pipe() + "------------------------".text() + pipe()
listOf(hr, lSmartDoc, hr).vSep().layoutPretty(PageWidth.Available(26, 1F)).renderString()
// |------------------------|
// fun(fun(fun(fun(fun(
// [ exempli
// , gratia] )))))
// |------------------------|
listOf(hr, lSmartDoc, hr).vSep().layoutSmart(PageWidth.Available(26, 1F)).renderString()
// |------------------------|
// fun(
// fun(
// fun(
// fun(
// fun(
// [ exempli
// , gratia] )))))
// |------------------------|
fun Doc<A>.layoutCompact(): SimpleDoc<A>
A very simple layout algorithm that lays out a document with no extra newlines or indentation. This is useful to render into machine-readable formats.
fun SimpleDoc<A>.renderString(): String
Render a simple doc as a string. Ignores annotations.
fun <A, B> SimpleDoc<A>.renderDecorated(empty: B, combine: (B, B) -> B, fromString: (String) -> B, annotate: (A, B) -> B): B
Helper to implement basic renders that handle annotations.
For example renderString is defined as:
fun <A> SimpleDoc<A>.renderString(): String =
renderDecorated(
empty = "",
combine = { a, b -> a + b },
fromString = { it },
annotate = { ann, str -> str }
)