Prettyprinter: Declarative Structured Data Formatting with Function Composition
When working with structured data, printing it in a clear and adaptable format is a common challenge. This comes up often in debugging, logging, and code generation. For instance, an array literal [a,b,c]
should ideally print on one line if the screen is wide enough, but gracefully wrap and indent when space is limited.
Traditional solutions often rely on manually concatenating strings while tracking indentation levels. This approach is not only tedious, but also error-prone.
A more elegant solution is to use function composition. With this approach, we build a prettyprinter: a system where users combine primitive formatting functions into a Doc
structure that describes the intended layout. Given a maximum width, the prettyprinter automatically chooses the most readable formatting.
This makes the printing process declarative—you specify what the layout should look like under different conditions, and the system figures out how to render it.
SimpleDoc Primitives
We begin with a minimal representation called SimpleDoc
. It consists of just four primitives:
enum SimpleDoc {
SimpleDoc
Empty
SimpleDoc
Line
(String) -> SimpleDoc
Text(String
String)
(SimpleDoc, SimpleDoc) -> SimpleDoc
Cat(enum SimpleDoc {
Empty
Line
Text(String)
Cat(SimpleDoc, SimpleDoc)
}
SimpleDoc, enum SimpleDoc {
Empty
Line
Text(String)
Cat(SimpleDoc, SimpleDoc)
}
SimpleDoc)
}
Empty
: represents an empty stringLine
: represents a newlineText(String)
: plain text without line breaksCat(SimpleDoc, SimpleDoc)
: concatenates twoSimpleDocs
s
Using these primitives, we can implement a simple rendering function. It flattens a SimpleDoc
into a string using a stack-based traversal:
fn enum SimpleDoc {
Empty
Line
Text(String)
Cat(SimpleDoc, SimpleDoc)
}
SimpleDoc::(doc : SimpleDoc) -> String
render(SimpleDoc
doc : enum SimpleDoc {
Empty
Line
Text(String)
Cat(SimpleDoc, SimpleDoc)
}
SimpleDoc) -> String
String {
let StringBuilder
buf = type StringBuilder
StringBuilder::(size_hint? : Int) -> StringBuilder
Creates a new string builder with an optional initial capacity hint.
Parameters:
size_hint
: An optional initial capacity hint for the internal buffer. If
less than 1, a minimum capacity of 1 is used. Defaults to 0. It is the size of bytes,
not the size of characters. size_hint
may be ignored on some platforms, JS for example.
Returns a new StringBuilder
instance with the specified initial capacity.
new()
let Array[SimpleDoc]
stack = [SimpleDoc
doc]
while Array[SimpleDoc]
stack.(self : Array[SimpleDoc]) -> SimpleDoc?
Removes the last element from a array and returns it, or None
if it is empty.
Example
let v = [1, 2, 3]
assert_eq(v.pop(), Some(3))
assert_eq(v, [1, 2])
pop() is (SimpleDoc) -> SimpleDoc?
Some(SimpleDoc
doc) {
match SimpleDoc
doc {
SimpleDoc
Empty => ()
SimpleDoc
Line => {
StringBuilder
buf..(self : StringBuilder, str : String) -> Unit
Writes a string to the StringBuilder.
write_string("\n")
}
(String) -> SimpleDoc
Text(String
text) => {
StringBuilder
buf.(self : StringBuilder, str : String) -> Unit
Writes a string to the StringBuilder.
write_string(String
text)
}
(SimpleDoc, SimpleDoc) -> SimpleDoc
Cat(SimpleDoc
left, SimpleDoc
right) =>
Array[SimpleDoc]
stack..(self : Array[SimpleDoc], value : SimpleDoc) -> Unit
Adds an element to the end of the array.
If the array is at capacity, it will be reallocated.
Example
let v = []
v.push(3)
push(SimpleDoc
right)..(self : Array[SimpleDoc], value : SimpleDoc) -> Unit
Adds an element to the end of the array.
If the array is at capacity, it will be reallocated.
Example
let v = []
v.push(3)
push(SimpleDoc
left)
}
}
StringBuilder
buf.(self : StringBuilder) -> String
Returns the current content of the StringBuilder as a string.
to_string()
}
Here’s a quick test: we can see that the expressiveness of SimpleDoc
is equivalent to String
: Empty
corresponds to ""
, Line
corresponds to "\n"
, Text("a")
corresponds to "a"
, and Cat(Text("a"), Text("b"))
corresponds to "a" + "b"
.
test "simple doc" {
let SimpleDoc
doc : enum SimpleDoc {
Empty
Line
Text(String)
Cat(SimpleDoc, SimpleDoc)
}
SimpleDoc = (SimpleDoc, SimpleDoc) -> SimpleDoc
Cat((String) -> SimpleDoc
Text("hello"), (SimpleDoc, SimpleDoc) -> SimpleDoc
Cat(SimpleDoc
Line, (String) -> SimpleDoc
Text("world")))
(obj : &Show, content~ : String, loc~ : SourceLoc = _, args_loc~ : ArgsLoc = _) -> Unit raise InspectError
Tests if the string representation of an object matches the expected content.
Used primarily in test cases to verify the correctness of Show
implementations and program outputs.
Parameters:
object
: The object to be inspected. Must implement the Show
trait.
content
: The expected string representation of the object. Defaults to
an empty string.
location
: Source code location information for error reporting.
Automatically provided by the compiler.
arguments_location
: Location information for function arguments in
source code. Automatically provided by the compiler.
Throws an InspectError
if the actual string representation of the object
does not match the expected content. The error message includes detailed
information about the mismatch, including source location and both expected
and actual values.
Example:
inspect(42, content="42")
inspect("hello", content="hello")
inspect([1, 2, 3], content="[1, 2, 3]")
inspect(
SimpleDoc
doc.(doc : SimpleDoc) -> String
render(),
String
content=(
#|hello
#|world
),
)
}
At this stage, the SimpleDoc
doesn’t yet handle indentation or layout choices—but we’re about to fix that.
ExtendDoc: Nest, Choice, Group
To handle real-world formatting, we extend SimpleDoc
with three new primitives:
enum ExtendDoc {
ExtendDoc
Empty
ExtendDoc
Line
(String) -> ExtendDoc
Text(String
String)
(ExtendDoc, ExtendDoc) -> ExtendDoc
Cat(enum ExtendDoc {
Empty
Line
Text(String)
Cat(ExtendDoc, ExtendDoc)
Nest(Int, ExtendDoc)
Choice(ExtendDoc, ExtendDoc)
Group(ExtendDoc)
}
ExtendDoc, enum ExtendDoc {
Empty
Line
Text(String)
Cat(ExtendDoc, ExtendDoc)
Nest(Int, ExtendDoc)
Choice(ExtendDoc, ExtendDoc)
Group(ExtendDoc)
}
ExtendDoc)
(Int, ExtendDoc) -> ExtendDoc
Nest(Int
Int, enum ExtendDoc {
Empty
Line
Text(String)
Cat(ExtendDoc, ExtendDoc)
Nest(Int, ExtendDoc)
Choice(ExtendDoc, ExtendDoc)
Group(ExtendDoc)
}
ExtendDoc)
(ExtendDoc, ExtendDoc) -> ExtendDoc
Choice(enum ExtendDoc {
Empty
Line
Text(String)
Cat(ExtendDoc, ExtendDoc)
Nest(Int, ExtendDoc)
Choice(ExtendDoc, ExtendDoc)
Group(ExtendDoc)
}
ExtendDoc, enum ExtendDoc {
Empty
Line
Text(String)
Cat(ExtendDoc, ExtendDoc)
Nest(Int, ExtendDoc)
Choice(ExtendDoc, ExtendDoc)
Group(ExtendDoc)
}
ExtendDoc)
(ExtendDoc) -> ExtendDoc
Group(enum ExtendDoc {
Empty
Line
Text(String)
Cat(ExtendDoc, ExtendDoc)
Nest(Int, ExtendDoc)
Choice(ExtendDoc, ExtendDoc)
Group(ExtendDoc)
}
ExtendDoc)
}
-
Nest
Nest(Int, ExtendDoc)
indents the doc by n spaces after each line break. Nested levels accumulate. -
Choice
Choice(ExtendDoc, ExtendDoc)
stores two alternative layouts. Usually, the first parameter is the more compact layout without line breaks, and the second is the layout withLine
s. The renderer uses the first layout in compact mode and the second otherwise. -
Group
Group(ExtendDoc)
groups anExtendDoc
and decides between compact or non-compact layout based on the available width. If the remaining space is sufficient, it prints compactly; otherwise, it falls back to the layout with line breaks.
Measuring Space
To know whether compact layout fits, we need a way to estimate how many characters a document would require:
let Int
max_space = 9999
fn enum ExtendDoc {
Empty
Line
Text(String)
Cat(ExtendDoc, ExtendDoc)
Nest(Int, ExtendDoc)
Choice(ExtendDoc, ExtendDoc)
Group(ExtendDoc)
}
ExtendDoc::(self : ExtendDoc) -> Int
space(ExtendDoc
self : enum ExtendDoc {
Empty
Line
Text(String)
Cat(ExtendDoc, ExtendDoc)
Nest(Int, ExtendDoc)
Choice(ExtendDoc, ExtendDoc)
Group(ExtendDoc)
}
Self) -> Int
Int {
match ExtendDoc
self {
ExtendDoc
Empty => 0
ExtendDoc
Line => Int
max_space
(String) -> ExtendDoc
Text(String
str) => String
str.(self : String) -> Int
Returns the number of UTF-16 code units in the string. Note that this is not
necessarily equal to the number of Unicode characters (code points) in the
string, as some characters may be represented by multiple UTF-16 code units.
Parameters:
string
: The string whose length is to be determined.
Returns the number of UTF-16 code units in the string.
Example:
inspect("hello".length(), content="5")
inspect("🤣".length(), content="2") // Emoji uses two UTF-16 code units
inspect("".length(), content="0") // Empty string
length()
(ExtendDoc, ExtendDoc) -> ExtendDoc
Cat(ExtendDoc
a, ExtendDoc
b) => ExtendDoc
a.(self : ExtendDoc) -> Int
space() (self : Int, other : Int) -> Int
Adds two 32-bit signed integers. Performs two's complement arithmetic, which
means the operation will wrap around if the result exceeds the range of a
32-bit integer.
Parameters:
self
: The first integer operand.
other
: The second integer operand.
Returns a new integer that is the sum of the two operands. If the
mathematical sum exceeds the range of a 32-bit integer (-2,147,483,648 to
2,147,483,647), the result wraps around according to two's complement rules.
Example:
inspect(42 + 1, content="43")
inspect(2147483647 + 1, content="-2147483648") // Overflow wraps around to minimum value
+ ExtendDoc
b.(self : ExtendDoc) -> Int
space()
(Int, ExtendDoc) -> ExtendDoc
Nest(_, ExtendDoc
a) | (ExtendDoc, ExtendDoc) -> ExtendDoc
Choice(ExtendDoc
a, _) | (ExtendDoc) -> ExtendDoc
Group(ExtendDoc
a) => ExtendDoc
a.(self : ExtendDoc) -> Int
space()
}
}
Here, Line
is treated as requiring “infinite” space. This guarantees that if a Group
contains a line break, it won’t attempt to print compactly.
Rendering ExtendDoc
We extend SimpleDoc::render
to implement ExtendDoc::render
. Since after printing a substructure we need to return to the original indentation level, the stack must also store two states for each pending ExtendDoc
: indentation and whether compact mode is active. We also maintain a column
variable to track the number of characters already used on the current line, in order to calculate remaining space. Finally, the function adds a width
parameter to specify the maximum line width.
fn enum ExtendDoc {
Empty
Line
Text(String)
Cat(ExtendDoc, ExtendDoc)
Nest(Int, ExtendDoc)
Choice(ExtendDoc, ExtendDoc)
Group(ExtendDoc)
}
ExtendDoc::(doc : ExtendDoc, width? : Int) -> String
render(ExtendDoc
doc : enum ExtendDoc {
Empty
Line
Text(String)
Cat(ExtendDoc, ExtendDoc)
Nest(Int, ExtendDoc)
Choice(ExtendDoc, ExtendDoc)
Group(ExtendDoc)
}
ExtendDoc, Int
width~ : Int
Int = 80) -> String
String {
let StringBuilder
buf = type StringBuilder
StringBuilder::(size_hint? : Int) -> StringBuilder
Creates a new string builder with an optional initial capacity hint.
Parameters:
size_hint
: An optional initial capacity hint for the internal buffer. If
less than 1, a minimum capacity of 1 is used. Defaults to 0. It is the size of bytes,
not the size of characters. size_hint
may be ignored on some platforms, JS for example.
Returns a new StringBuilder
instance with the specified initial capacity.
new()
let Array[(Int, Bool, ExtendDoc)]
stack = [(0, false, ExtendDoc
doc)] // default: no indentation, non-compact mode
let mut Int
column = 0
while Array[(Int, Bool, ExtendDoc)]
stack.(self : Array[(Int, Bool, ExtendDoc)]) -> (Int, Bool, ExtendDoc)?
Removes the last element from a array and returns it, or None
if it is empty.
Example
let v = [1, 2, 3]
assert_eq(v.pop(), Some(3))
assert_eq(v, [1, 2])
pop() is ((Int, Bool, ExtendDoc)) -> (Int, Bool, ExtendDoc)?
Some((Int
indent, Bool
fit, ExtendDoc
doc)) {
match ExtendDoc
doc {
ExtendDoc
Empty => ()
ExtendDoc
Line => {
StringBuilder
buf..(self : StringBuilder, str : String) -> Unit
Writes a string to the StringBuilder.
write_string("\n")
for _ in Int
0..<Int
indent {
StringBuilder
buf.(self : StringBuilder, str : String) -> Unit
Writes a string to the StringBuilder.
write_string(" ")
}
Int
column = Int
indent
}
(String) -> ExtendDoc
Text(String
text) => {
StringBuilder
buf.(self : StringBuilder, str : String) -> Unit
Writes a string to the StringBuilder.
write_string(String
text)
Int
column (self : Int, other : Int) -> Int
Adds two 32-bit signed integers. Performs two's complement arithmetic, which
means the operation will wrap around if the result exceeds the range of a
32-bit integer.
Parameters:
self
: The first integer operand.
other
: The second integer operand.
Returns a new integer that is the sum of the two operands. If the
mathematical sum exceeds the range of a 32-bit integer (-2,147,483,648 to
2,147,483,647), the result wraps around according to two's complement rules.
Example:
inspect(42 + 1, content="43")
inspect(2147483647 + 1, content="-2147483648") // Overflow wraps around to minimum value
+= String
text.(self : String) -> Int
Returns the number of UTF-16 code units in the string. Note that this is not
necessarily equal to the number of Unicode characters (code points) in the
string, as some characters may be represented by multiple UTF-16 code units.
Parameters:
string
: The string whose length is to be determined.
Returns the number of UTF-16 code units in the string.
Example:
inspect("hello".length(), content="5")
inspect("🤣".length(), content="2") // Emoji uses two UTF-16 code units
inspect("".length(), content="0") // Empty string
length()
}
(ExtendDoc, ExtendDoc) -> ExtendDoc
Cat(ExtendDoc
left, ExtendDoc
right) =>
Array[(Int, Bool, ExtendDoc)]
stack..(self : Array[(Int, Bool, ExtendDoc)], value : (Int, Bool, ExtendDoc)) -> Unit
Adds an element to the end of the array.
If the array is at capacity, it will be reallocated.
Example
let v = []
v.push(3)
push((Int
indent, Bool
fit, ExtendDoc
right))..(self : Array[(Int, Bool, ExtendDoc)], value : (Int, Bool, ExtendDoc)) -> Unit
Adds an element to the end of the array.
If the array is at capacity, it will be reallocated.
Example
let v = []
v.push(3)
push((Int
indent, Bool
fit, ExtendDoc
left))
(Int, ExtendDoc) -> ExtendDoc
Nest(Int
n, ExtendDoc
doc) => Array[(Int, Bool, ExtendDoc)]
stack..(self : Array[(Int, Bool, ExtendDoc)], value : (Int, Bool, ExtendDoc)) -> Unit
Adds an element to the end of the array.
If the array is at capacity, it will be reallocated.
Example
let v = []
v.push(3)
push((Int
indent (self : Int, other : Int) -> Int
Adds two 32-bit signed integers. Performs two's complement arithmetic, which
means the operation will wrap around if the result exceeds the range of a
32-bit integer.
Parameters:
self
: The first integer operand.
other
: The second integer operand.
Returns a new integer that is the sum of the two operands. If the
mathematical sum exceeds the range of a 32-bit integer (-2,147,483,648 to
2,147,483,647), the result wraps around according to two's complement rules.
Example:
inspect(42 + 1, content="43")
inspect(2147483647 + 1, content="-2147483648") // Overflow wraps around to minimum value
+ Int
n, Bool
fit, ExtendDoc
doc))
(ExtendDoc, ExtendDoc) -> ExtendDoc
Choice(ExtendDoc
a, ExtendDoc
b) =>
Array[(Int, Bool, ExtendDoc)]
stack.(self : Array[(Int, Bool, ExtendDoc)], value : (Int, Bool, ExtendDoc)) -> Unit
Adds an element to the end of the array.
If the array is at capacity, it will be reallocated.
Example
let v = []
v.push(3)
push(if Bool
fit { (Int
indent, Bool
fit, ExtendDoc
a) } else { (Int
indent, Bool
fit, ExtendDoc
b) })
(ExtendDoc) -> ExtendDoc
Group(ExtendDoc
doc) => {
let Bool
fit = Bool
fit (Bool, Bool) -> Bool
|| Int
column (self : Int, other : Int) -> Int
Adds two 32-bit signed integers. Performs two's complement arithmetic, which
means the operation will wrap around if the result exceeds the range of a
32-bit integer.
Parameters:
self
: The first integer operand.
other
: The second integer operand.
Returns a new integer that is the sum of the two operands. If the
mathematical sum exceeds the range of a 32-bit integer (-2,147,483,648 to
2,147,483,647), the result wraps around according to two's complement rules.
Example:
inspect(42 + 1, content="43")
inspect(2147483647 + 1, content="-2147483648") // Overflow wraps around to minimum value
+ ExtendDoc
doc.(self : ExtendDoc) -> Int
space() (self_ : Int, other : Int) -> Bool
<= Int
width
Array[(Int, Bool, ExtendDoc)]
stack.(self : Array[(Int, Bool, ExtendDoc)], value : (Int, Bool, ExtendDoc)) -> Unit
Adds an element to the end of the array.
If the array is at capacity, it will be reallocated.
Example
let v = []
v.push(3)
push((Int
indent, Bool
fit, ExtendDoc
doc))
}
}
}
StringBuilder
buf.(self : StringBuilder) -> String
Returns the current content of the StringBuilder as a string.
to_string()
}
Let’s use ExtendDoc
to describe a (expr)
and print it under different width:
let ExtendDoc
softline : enum ExtendDoc {
Empty
Line
Text(String)
Cat(ExtendDoc, ExtendDoc)
Nest(Int, ExtendDoc)
Choice(ExtendDoc, ExtendDoc)
Group(ExtendDoc)
}
ExtendDoc = (ExtendDoc, ExtendDoc) -> ExtendDoc
Choice(ExtendDoc
Empty, ExtendDoc
Line)
impl trait Add {
add(Self, Self) -> Self
op_add(Self, Self) -> Self
}
types implementing this trait can use the +
operator
Add for enum ExtendDoc {
Empty
Line
Text(String)
Cat(ExtendDoc, ExtendDoc)
Nest(Int, ExtendDoc)
Choice(ExtendDoc, ExtendDoc)
Group(ExtendDoc)
}
ExtendDoc with (a : ExtendDoc, b : ExtendDoc) -> ExtendDoc
op_add(ExtendDoc
a, ExtendDoc
b) {
(ExtendDoc, ExtendDoc) -> ExtendDoc
Cat(ExtendDoc
a, ExtendDoc
b)
}
test "tuple" {
let ExtendDoc
tuple : enum ExtendDoc {
Empty
Line
Text(String)
Cat(ExtendDoc, ExtendDoc)
Nest(Int, ExtendDoc)
Choice(ExtendDoc, ExtendDoc)
Group(ExtendDoc)
}
ExtendDoc = (ExtendDoc) -> ExtendDoc
Group(
(String) -> ExtendDoc
Text("(") (self : ExtendDoc, other : ExtendDoc) -> ExtendDoc
+ (Int, ExtendDoc) -> ExtendDoc
Nest(2, ExtendDoc
softline (self : ExtendDoc, other : ExtendDoc) -> ExtendDoc
+ (String) -> ExtendDoc
Text("expr")) (self : ExtendDoc, other : ExtendDoc) -> ExtendDoc
+ ExtendDoc
softline (self : ExtendDoc, other : ExtendDoc) -> ExtendDoc
+ (String) -> ExtendDoc
Text(")"),
)
(obj : &Show, content~ : String, loc~ : SourceLoc = _, args_loc~ : ArgsLoc = _) -> Unit raise InspectError
Tests if the string representation of an object matches the expected content.
Used primarily in test cases to verify the correctness of Show
implementations and program outputs.
Parameters:
object
: The object to be inspected. Must implement the Show
trait.
content
: The expected string representation of the object. Defaults to
an empty string.
location
: Source code location information for error reporting.
Automatically provided by the compiler.
arguments_location
: Location information for function arguments in
source code. Automatically provided by the compiler.
Throws an InspectError
if the actual string representation of the object
does not match the expected content. The error message includes detailed
information about the mismatch, including source location and both expected
and actual values.
Example:
inspect(42, content="42")
inspect("hello", content="hello")
inspect([1, 2, 3], content="[1, 2, 3]")
inspect(ExtendDoc
tuple.(doc : ExtendDoc, width~ : Int) -> String
render(Int
width=40), String
content="(expr)")
(obj : &Show, content~ : String, loc~ : SourceLoc = _, args_loc~ : ArgsLoc = _) -> Unit raise InspectError
Tests if the string representation of an object matches the expected content.
Used primarily in test cases to verify the correctness of Show
implementations and program outputs.
Parameters:
object
: The object to be inspected. Must implement the Show
trait.
content
: The expected string representation of the object. Defaults to
an empty string.
location
: Source code location information for error reporting.
Automatically provided by the compiler.
arguments_location
: Location information for function arguments in
source code. Automatically provided by the compiler.
Throws an InspectError
if the actual string representation of the object
does not match the expected content. The error message includes detailed
information about the mismatch, including source location and both expected
and actual values.
Example:
inspect(42, content="42")
inspect("hello", content="hello")
inspect([1, 2, 3], content="[1, 2, 3]")
inspect(
ExtendDoc
tuple.(doc : ExtendDoc, width~ : Int) -> String
render(Int
width=5),
String
content=(
#|(
#| expr
#|)
),
)
}
Here, softline
is defined as a choice between Empty
and Line
. Since rendering starts in non-compact mode, we wrap the whole expression with Group
. When the width is sufficient, the entire expression prints on one line; otherwise, it automatically wraps with indentation. To improve readability, we overloaded the +
operator for ExtendDoc
.
Composition Functions
In practice, users rely more on higher-level combinators built from the ExtendDoc
primitives—like the softline
above. Let’s introduce some useful functions for structured printing.
softline & softbreak
let ExtendDoc
softbreak : enum ExtendDoc {
Empty
Line
Text(String)
Cat(ExtendDoc, ExtendDoc)
Nest(Int, ExtendDoc)
Choice(ExtendDoc, ExtendDoc)
Group(ExtendDoc)
}
ExtendDoc = (ExtendDoc, ExtendDoc) -> ExtendDoc
Choice((String) -> ExtendDoc
Text(" "), ExtendDoc
Line)
Similar to softline
, except that in compact mode it inserts a space. Note that within the same Group
, all Choice
s follow the same compact or non-compact decision.
let ExtendDoc
abc : enum ExtendDoc {
Empty
Line
Text(String)
Cat(ExtendDoc, ExtendDoc)
Nest(Int, ExtendDoc)
Choice(ExtendDoc, ExtendDoc)
Group(ExtendDoc)
}
ExtendDoc = (String) -> ExtendDoc
Text("abc")
let ExtendDoc
def : enum ExtendDoc {
Empty
Line
Text(String)
Cat(ExtendDoc, ExtendDoc)
Nest(Int, ExtendDoc)
Choice(ExtendDoc, ExtendDoc)
Group(ExtendDoc)
}
ExtendDoc = (String) -> ExtendDoc
Text("def")
let ExtendDoc
ghi : enum ExtendDoc {
Empty
Line
Text(String)
Cat(ExtendDoc, ExtendDoc)
Nest(Int, ExtendDoc)
Choice(ExtendDoc, ExtendDoc)
Group(ExtendDoc)
}
ExtendDoc = (String) -> ExtendDoc
Text("ghi")
test "softbreak" {
let ExtendDoc
doc : enum ExtendDoc {
Empty
Line
Text(String)
Cat(ExtendDoc, ExtendDoc)
Nest(Int, ExtendDoc)
Choice(ExtendDoc, ExtendDoc)
Group(ExtendDoc)
}
ExtendDoc = (ExtendDoc) -> ExtendDoc
Group(ExtendDoc
abc (self : ExtendDoc, other : ExtendDoc) -> ExtendDoc
+ ExtendDoc
softbreak (self : ExtendDoc, other : ExtendDoc) -> ExtendDoc
+ ExtendDoc
def (self : ExtendDoc, other : ExtendDoc) -> ExtendDoc
+ ExtendDoc
softbreak (self : ExtendDoc, other : ExtendDoc) -> ExtendDoc
+ ExtendDoc
ghi)
(obj : &Show, content~ : String, loc~ : SourceLoc = _, args_loc~ : ArgsLoc = _) -> Unit raise InspectError
Tests if the string representation of an object matches the expected content.
Used primarily in test cases to verify the correctness of Show
implementations and program outputs.
Parameters:
object
: The object to be inspected. Must implement the Show
trait.
content
: The expected string representation of the object. Defaults to
an empty string.
location
: Source code location information for error reporting.
Automatically provided by the compiler.
arguments_location
: Location information for function arguments in
source code. Automatically provided by the compiler.
Throws an InspectError
if the actual string representation of the object
does not match the expected content. The error message includes detailed
information about the mismatch, including source location and both expected
and actual values.
Example:
inspect(42, content="42")
inspect("hello", content="hello")
inspect([1, 2, 3], content="[1, 2, 3]")
inspect(ExtendDoc
doc.(doc : ExtendDoc, width~ : Int) -> String
render(Int
width=20), String
content="abc def ghi")
(obj : &Show, content~ : String, loc~ : SourceLoc = _, args_loc~ : ArgsLoc = _) -> Unit raise InspectError
Tests if the string representation of an object matches the expected content.
Used primarily in test cases to verify the correctness of Show
implementations and program outputs.
Parameters:
object
: The object to be inspected. Must implement the Show
trait.
content
: The expected string representation of the object. Defaults to
an empty string.
location
: Source code location information for error reporting.
Automatically provided by the compiler.
arguments_location
: Location information for function arguments in
source code. Automatically provided by the compiler.
Throws an InspectError
if the actual string representation of the object
does not match the expected content. The error message includes detailed
information about the mismatch, including source location and both expected
and actual values.
Example:
inspect(42, content="42")
inspect("hello", content="hello")
inspect([1, 2, 3], content="[1, 2, 3]")
inspect(
ExtendDoc
doc.(doc : ExtendDoc, width~ : Int) -> String
render(Int
width=10),
String
content=(
#|abc
#|def
#|ghi
),
)
}
autoline & autobreak
let ExtendDoc
autoline : enum ExtendDoc {
Empty
Line
Text(String)
Cat(ExtendDoc, ExtendDoc)
Nest(Int, ExtendDoc)
Choice(ExtendDoc, ExtendDoc)
Group(ExtendDoc)
}
ExtendDoc = (ExtendDoc) -> ExtendDoc
Group(ExtendDoc
softline)
let ExtendDoc
autobreak : enum ExtendDoc {
Empty
Line
Text(String)
Cat(ExtendDoc, ExtendDoc)
Nest(Int, ExtendDoc)
Choice(ExtendDoc, ExtendDoc)
Group(ExtendDoc)
}
ExtendDoc = (ExtendDoc) -> ExtendDoc
Group(ExtendDoc
softbreak)
autoline
and autobreak
make sure the ExtendDoc
s fit as much as possible on one line, like text editors do.
test {
let ExtendDoc
doc : enum ExtendDoc {
Empty
Line
Text(String)
Cat(ExtendDoc, ExtendDoc)
Nest(Int, ExtendDoc)
Choice(ExtendDoc, ExtendDoc)
Group(ExtendDoc)
}
ExtendDoc = (ExtendDoc) -> ExtendDoc
Group(
ExtendDoc
abc (self : ExtendDoc, other : ExtendDoc) -> ExtendDoc
+ ExtendDoc
autobreak (self : ExtendDoc, other : ExtendDoc) -> ExtendDoc
+ ExtendDoc
def (self : ExtendDoc, other : ExtendDoc) -> ExtendDoc
+ ExtendDoc
autobreak (self : ExtendDoc, other : ExtendDoc) -> ExtendDoc
+ ExtendDoc
ghi,
)
(obj : &Show, content~ : String, loc~ : SourceLoc = _, args_loc~ : ArgsLoc = _) -> Unit raise InspectError
Tests if the string representation of an object matches the expected content.
Used primarily in test cases to verify the correctness of Show
implementations and program outputs.
Parameters:
object
: The object to be inspected. Must implement the Show
trait.
content
: The expected string representation of the object. Defaults to
an empty string.
location
: Source code location information for error reporting.
Automatically provided by the compiler.
arguments_location
: Location information for function arguments in
source code. Automatically provided by the compiler.
Throws an InspectError
if the actual string representation of the object
does not match the expected content. The error message includes detailed
information about the mismatch, including source location and both expected
and actual values.
Example:
inspect(42, content="42")
inspect("hello", content="hello")
inspect([1, 2, 3], content="[1, 2, 3]")
inspect(ExtendDoc
doc.(doc : ExtendDoc, width~ : Int) -> String
render(Int
width=10), String
content="abc def ghi")
(obj : &Show, content~ : String, loc~ : SourceLoc = _, args_loc~ : ArgsLoc = _) -> Unit raise InspectError
Tests if the string representation of an object matches the expected content.
Used primarily in test cases to verify the correctness of Show
implementations and program outputs.
Parameters:
object
: The object to be inspected. Must implement the Show
trait.
content
: The expected string representation of the object. Defaults to
an empty string.
location
: Source code location information for error reporting.
Automatically provided by the compiler.
arguments_location
: Location information for function arguments in
source code. Automatically provided by the compiler.
Throws an InspectError
if the actual string representation of the object
does not match the expected content. The error message includes detailed
information about the mismatch, including source location and both expected
and actual values.
Example:
inspect(42, content="42")
inspect("hello", content="hello")
inspect([1, 2, 3], content="[1, 2, 3]")
inspect(
ExtendDoc
doc.(doc : ExtendDoc, width~ : Int) -> String
render(Int
width=5),
String
content=(
#|abc def
#|ghi
),
)
(obj : &Show, content~ : String, loc~ : SourceLoc = _, args_loc~ : ArgsLoc = _) -> Unit raise InspectError
Tests if the string representation of an object matches the expected content.
Used primarily in test cases to verify the correctness of Show
implementations and program outputs.
Parameters:
object
: The object to be inspected. Must implement the Show
trait.
content
: The expected string representation of the object. Defaults to
an empty string.
location
: Source code location information for error reporting.
Automatically provided by the compiler.
arguments_location
: Location information for function arguments in
source code. Automatically provided by the compiler.
Throws an InspectError
if the actual string representation of the object
does not match the expected content. The error message includes detailed
information about the mismatch, including source location and both expected
and actual values.
Example:
inspect(42, content="42")
inspect("hello", content="hello")
inspect([1, 2, 3], content="[1, 2, 3]")
inspect(
ExtendDoc
doc.(doc : ExtendDoc, width~ : Int) -> String
render(Int
width=3),
String
content=(
#|abc
#|def
#|ghi
),
)
}
sepby
fn (xs : Array[ExtendDoc], sep : ExtendDoc) -> ExtendDoc
sepby(Array[ExtendDoc]
xs : type Array[T]
An Array
is a collection of values that supports random access and can
grow in size.
Array[enum ExtendDoc {
Empty
Line
Text(String)
Cat(ExtendDoc, ExtendDoc)
Nest(Int, ExtendDoc)
Choice(ExtendDoc, ExtendDoc)
Group(ExtendDoc)
}
ExtendDoc], ExtendDoc
sep : enum ExtendDoc {
Empty
Line
Text(String)
Cat(ExtendDoc, ExtendDoc)
Nest(Int, ExtendDoc)
Choice(ExtendDoc, ExtendDoc)
Group(ExtendDoc)
}
ExtendDoc) -> enum ExtendDoc {
Empty
Line
Text(String)
Cat(ExtendDoc, ExtendDoc)
Nest(Int, ExtendDoc)
Choice(ExtendDoc, ExtendDoc)
Group(ExtendDoc)
}
ExtendDoc {
match Array[ExtendDoc]
xs {
[] => ExtendDoc
Empty
Array[ExtendDoc]
[ExtendDoc
xArray[ExtendDoc]
, .. xs] => ArrayView[ExtendDoc]
xs.(self : ArrayView[ExtendDoc], init~ : ExtendDoc, f : (ExtendDoc, ExtendDoc) -> ExtendDoc) -> ExtendDoc
Fold out values from an View according to certain rules.
Example
let sum = [1, 2, 3, 4, 5][:].fold(init=0, (sum, elem) => sum + elem)
inspect(sum, content="15")
fold(ExtendDoc
init=ExtendDoc
x, (ExtendDoc
a, ExtendDoc
b) => ExtendDoc
a (self : ExtendDoc, other : ExtendDoc) -> ExtendDoc
+ ExtendDoc
sep (self : ExtendDoc, other : ExtendDoc) -> ExtendDoc
+ ExtendDoc
b)
}
}
sepby
inserts a separator sep
between ExtendDoc
s.
let ExtendDoc
comma : enum ExtendDoc {
Empty
Line
Text(String)
Cat(ExtendDoc, ExtendDoc)
Nest(Int, ExtendDoc)
Choice(ExtendDoc, ExtendDoc)
Group(ExtendDoc)
}
ExtendDoc = (String) -> ExtendDoc
Text(",")
test {
let ExtendDoc
layout = (ExtendDoc) -> ExtendDoc
Group((xs : Array[ExtendDoc], sep : ExtendDoc) -> ExtendDoc
sepby([ExtendDoc
abc, ExtendDoc
def, ExtendDoc
ghi], ExtendDoc
comma (self : ExtendDoc, other : ExtendDoc) -> ExtendDoc
+ ExtendDoc
softbreak))
(obj : &Show, content~ : String, loc~ : SourceLoc = _, args_loc~ : ArgsLoc = _) -> Unit raise InspectError
Tests if the string representation of an object matches the expected content.
Used primarily in test cases to verify the correctness of Show
implementations and program outputs.
Parameters:
object
: The object to be inspected. Must implement the Show
trait.
content
: The expected string representation of the object. Defaults to
an empty string.
location
: Source code location information for error reporting.
Automatically provided by the compiler.
arguments_location
: Location information for function arguments in
source code. Automatically provided by the compiler.
Throws an InspectError
if the actual string representation of the object
does not match the expected content. The error message includes detailed
information about the mismatch, including source location and both expected
and actual values.
Example:
inspect(42, content="42")
inspect("hello", content="hello")
inspect([1, 2, 3], content="[1, 2, 3]")
inspect(ExtendDoc
layout.(doc : ExtendDoc, width~ : Int) -> String
render(Int
width=40), String
content="abc, def, ghi")
(obj : &Show, content~ : String, loc~ : SourceLoc = _, args_loc~ : ArgsLoc = _) -> Unit raise InspectError
Tests if the string representation of an object matches the expected content.
Used primarily in test cases to verify the correctness of Show
implementations and program outputs.
Parameters:
object
: The object to be inspected. Must implement the Show
trait.
content
: The expected string representation of the object. Defaults to
an empty string.
location
: Source code location information for error reporting.
Automatically provided by the compiler.
arguments_location
: Location information for function arguments in
source code. Automatically provided by the compiler.
Throws an InspectError
if the actual string representation of the object
does not match the expected content. The error message includes detailed
information about the mismatch, including source location and both expected
and actual values.
Example:
inspect(42, content="42")
inspect("hello", content="hello")
inspect([1, 2, 3], content="[1, 2, 3]")
inspect(
ExtendDoc
layout.(doc : ExtendDoc, width~ : Int) -> String
render(Int
width=10),
String
content=(
#|abc,
#|def,
#|ghi
),
)
}
surround
fn (m : ExtendDoc, l : ExtendDoc, r : ExtendDoc) -> ExtendDoc
surround(ExtendDoc
m : enum ExtendDoc {
Empty
Line
Text(String)
Cat(ExtendDoc, ExtendDoc)
Nest(Int, ExtendDoc)
Choice(ExtendDoc, ExtendDoc)
Group(ExtendDoc)
}
ExtendDoc, ExtendDoc
l : enum ExtendDoc {
Empty
Line
Text(String)
Cat(ExtendDoc, ExtendDoc)
Nest(Int, ExtendDoc)
Choice(ExtendDoc, ExtendDoc)
Group(ExtendDoc)
}
ExtendDoc, ExtendDoc
r : enum ExtendDoc {
Empty
Line
Text(String)
Cat(ExtendDoc, ExtendDoc)
Nest(Int, ExtendDoc)
Choice(ExtendDoc, ExtendDoc)
Group(ExtendDoc)
}
ExtendDoc) -> enum ExtendDoc {
Empty
Line
Text(String)
Cat(ExtendDoc, ExtendDoc)
Nest(Int, ExtendDoc)
Choice(ExtendDoc, ExtendDoc)
Group(ExtendDoc)
}
ExtendDoc {
ExtendDoc
l (self : ExtendDoc, other : ExtendDoc) -> ExtendDoc
+ ExtendDoc
m (self : ExtendDoc, other : ExtendDoc) -> ExtendDoc
+ ExtendDoc
r
}
surround
wraps an ExtendDoc
with left and right delimiters.
test {
(obj : &Show, content~ : String, loc~ : SourceLoc = _, args_loc~ : ArgsLoc = _) -> Unit raise InspectError
Tests if the string representation of an object matches the expected content.
Used primarily in test cases to verify the correctness of Show
implementations and program outputs.
Parameters:
object
: The object to be inspected. Must implement the Show
trait.
content
: The expected string representation of the object. Defaults to
an empty string.
location
: Source code location information for error reporting.
Automatically provided by the compiler.
arguments_location
: Location information for function arguments in
source code. Automatically provided by the compiler.
Throws an InspectError
if the actual string representation of the object
does not match the expected content. The error message includes detailed
information about the mismatch, including source location and both expected
and actual values.
Example:
inspect(42, content="42")
inspect("hello", content="hello")
inspect([1, 2, 3], content="[1, 2, 3]")
inspect((m : ExtendDoc, l : ExtendDoc, r : ExtendDoc) -> ExtendDoc
surround(ExtendDoc
abc, (String) -> ExtendDoc
Text("("), (String) -> ExtendDoc
Text(")")).(doc : ExtendDoc, width? : Int) -> String
render(), String
content="(abc)")
}
Printing JSON
Using the functions above, we can implement a JSON prettyprinter. This function recursively processes each JSON element and generates the appropriate layout.
fn (x : Json) -> ExtendDoc
pretty(Json
x : enum Json {
Null
True
False
Number(Double, repr~ : String?)
String(String)
Array(Array[Json])
Object(Map[String, Json])
}
Json) -> enum ExtendDoc {
Empty
Line
Text(String)
Cat(ExtendDoc, ExtendDoc)
Nest(Int, ExtendDoc)
Choice(ExtendDoc, ExtendDoc)
Group(ExtendDoc)
}
ExtendDoc {
fn (Array[ExtendDoc], ExtendDoc, ExtendDoc) -> ExtendDoc
comma_list(Array[ExtendDoc]
xs, ExtendDoc
l, ExtendDoc
r) {
((Int, ExtendDoc) -> ExtendDoc
Nest(2, ExtendDoc
softline (self : ExtendDoc, other : ExtendDoc) -> ExtendDoc
+ (xs : Array[ExtendDoc], sep : ExtendDoc) -> ExtendDoc
sepby(Array[ExtendDoc]
xs, ExtendDoc
comma (self : ExtendDoc, other : ExtendDoc) -> ExtendDoc
+ ExtendDoc
softbreak)) (self : ExtendDoc, other : ExtendDoc) -> ExtendDoc
+ ExtendDoc
softline)
|> (m : ExtendDoc, l : ExtendDoc, r : ExtendDoc) -> ExtendDoc
surround(ExtendDoc
l, ExtendDoc
r)
|> (ExtendDoc) -> ExtendDoc
Group
}
match Json
x {
(Array[Json]) -> Json
Array(Array[Json]
elems) => {
let Array[ExtendDoc]
elems = Array[Json]
elems.(self : Array[Json]) -> Iter[Json]
Creates an iterator over the elements of the array.
Parameters:
array
: The array to create an iterator from.
Returns an iterator that yields each element of the array in order.
Example:
let arr = [1, 2, 3]
let mut sum = 0
arr.iter().each((x) => { sum = sum + x })
inspect(sum, content="6")
iter().(self : Iter[Json], f : (Json) -> ExtendDoc) -> Iter[ExtendDoc]
Transforms the elements of the iterator using a mapping function.
Type Parameters
T
: The type of the elements in the iterator.
R
: The type of the transformed elements.
Arguments
self
- The input iterator.
f
- The mapping function that transforms each element of the iterator.
Returns
A new iterator that contains the transformed elements.
map((x : Json) -> ExtendDoc
pretty).(self : Iter[ExtendDoc]) -> Array[ExtendDoc]
Collects the elements of the iterator into an array.
collect()
(Array[ExtendDoc], ExtendDoc, ExtendDoc) -> ExtendDoc
comma_list(Array[ExtendDoc]
elems, (String) -> ExtendDoc
Text("["), (String) -> ExtendDoc
Text("]"))
}
(Map[String, Json]) -> Json
Object(Map[String, Json]
pairs) => {
let Array[ExtendDoc]
pairs = Map[String, Json]
pairs
.(self : Map[String, Json]) -> Iter[(String, Json)]
Returns the iterator of the hash map, provide elements in the order of insertion.
iter()
.(self : Iter[(String, Json)], f : ((String, Json)) -> ExtendDoc) -> Iter[ExtendDoc]
Transforms the elements of the iterator using a mapping function.
Type Parameters
T
: The type of the elements in the iterator.
R
: The type of the transformed elements.
Arguments
self
- The input iterator.
f
- The mapping function that transforms each element of the iterator.
Returns
A new iterator that contains the transformed elements.
map((String, Json)
p => (ExtendDoc) -> ExtendDoc
Group((String) -> ExtendDoc
Text((String, Json)
p.String
0.(self : String) -> String
Returns a valid MoonBit string literal representation of a string,
add quotes and escape special characters.
Examples
let str = "Hello \n"
inspect(str.to_string(), content="Hello \n")
inspect(str.escape(), content="\"Hello \\n\"")
escape()) (self : ExtendDoc, other : ExtendDoc) -> ExtendDoc
+ (String) -> ExtendDoc
Text(": ") (self : ExtendDoc, other : ExtendDoc) -> ExtendDoc
+ (x : Json) -> ExtendDoc
pretty((String, Json)
p.Json
1)))
.(self : Iter[ExtendDoc]) -> Array[ExtendDoc]
Collects the elements of the iterator into an array.
collect()
(Array[ExtendDoc], ExtendDoc, ExtendDoc) -> ExtendDoc
comma_list(Array[ExtendDoc]
pairs, (String) -> ExtendDoc
Text("{"), (String) -> ExtendDoc
Text("}"))
}
(String) -> Json
String(String
s) => (String) -> ExtendDoc
Text(String
s.(self : String) -> String
Returns a valid MoonBit string literal representation of a string,
add quotes and escape special characters.
Examples
let str = "Hello \n"
inspect(str.to_string(), content="Hello \n")
inspect(str.escape(), content="\"Hello \\n\"")
escape())
(Double, repr~ : String?) -> Json
Number(Double
i) => (String) -> ExtendDoc
Text(Double
i.(self : Double) -> String
Converts a double-precision floating-point number to its string
representation.
Parameters:
self
: The double-precision floating-point number to be converted.
Returns a string representation of the double-precision floating-point
number.
Example:
inspect(42.0.to_string(), content="42")
inspect(3.14159.to_string(), content="3.14159")
inspect((-0.0).to_string(), content="0")
inspect(@double.not_a_number.to_string(), content="NaN")
to_string())
Json
False => (String) -> ExtendDoc
Text("false")
Json
True => (String) -> ExtendDoc
Text("true")
Json
Null => (String) -> ExtendDoc
Text("null")
}
}
When rendered, the JSON automatically adapts to different widths:
test {
let Json
json : enum Json {
Null
True
False
Number(Double, repr~ : String?)
String(String)
Array(Array[Json])
Object(Map[String, Json])
}
Json = {
"key1": "string",
"key2": [12345, 67890],
"key3": [
{ "field1": 1, "field2": 2 },
{ "field1": 1, "field2": 2 },
{ "field1": [1, 2], "field2": 2 },
],
}
(obj : &Show, content~ : String, loc~ : SourceLoc = _, args_loc~ : ArgsLoc = _) -> Unit raise InspectError
Tests if the string representation of an object matches the expected content.
Used primarily in test cases to verify the correctness of Show
implementations and program outputs.
Parameters:
object
: The object to be inspected. Must implement the Show
trait.
content
: The expected string representation of the object. Defaults to
an empty string.
location
: Source code location information for error reporting.
Automatically provided by the compiler.
arguments_location
: Location information for function arguments in
source code. Automatically provided by the compiler.
Throws an InspectError
if the actual string representation of the object
does not match the expected content. The error message includes detailed
information about the mismatch, including source location and both expected
and actual values.
Example:
inspect(42, content="42")
inspect("hello", content="hello")
inspect([1, 2, 3], content="[1, 2, 3]")
inspect(
(x : Json) -> ExtendDoc
pretty(Json
json).(doc : ExtendDoc, width~ : Int) -> String
render(Int
width=80),
String
content=(
#|{
#| "key1": "string",
#| "key2": [12345, 67890],
#| "key3": [
#| {"field1": 1, "field2": 2},
#| {"field1": 1, "field2": 2},
#| {"field1": [1, 2], "field2": 2}
#| ]
#|}
),
)
(obj : &Show, content~ : String, loc~ : SourceLoc = _, args_loc~ : ArgsLoc = _) -> Unit raise InspectError
Tests if the string representation of an object matches the expected content.
Used primarily in test cases to verify the correctness of Show
implementations and program outputs.
Parameters:
object
: The object to be inspected. Must implement the Show
trait.
content
: The expected string representation of the object. Defaults to
an empty string.
location
: Source code location information for error reporting.
Automatically provided by the compiler.
arguments_location
: Location information for function arguments in
source code. Automatically provided by the compiler.
Throws an InspectError
if the actual string representation of the object
does not match the expected content. The error message includes detailed
information about the mismatch, including source location and both expected
and actual values.
Example:
inspect(42, content="42")
inspect("hello", content="hello")
inspect([1, 2, 3], content="[1, 2, 3]")
inspect(
(x : Json) -> ExtendDoc
pretty(Json
json).(doc : ExtendDoc, width~ : Int) -> String
render(Int
width=30),
String
content=(
#|{
#| "key1": "string",
#| "key2": [12345, 67890],
#| "key3": [
#| {"field1": 1, "field2": 2},
#| {"field1": 1, "field2": 2},
#| {
#| "field1": [1, 2],
#| "field2": 2
#| }
#| ]
#|}
),
)
(obj : &Show, content~ : String, loc~ : SourceLoc = _, args_loc~ : ArgsLoc = _) -> Unit raise InspectError
Tests if the string representation of an object matches the expected content.
Used primarily in test cases to verify the correctness of Show
implementations and program outputs.
Parameters:
object
: The object to be inspected. Must implement the Show
trait.
content
: The expected string representation of the object. Defaults to
an empty string.
location
: Source code location information for error reporting.
Automatically provided by the compiler.
arguments_location
: Location information for function arguments in
source code. Automatically provided by the compiler.
Throws an InspectError
if the actual string representation of the object
does not match the expected content. The error message includes detailed
information about the mismatch, including source location and both expected
and actual values.
Example:
inspect(42, content="42")
inspect("hello", content="hello")
inspect([1, 2, 3], content="[1, 2, 3]")
inspect(
(x : Json) -> ExtendDoc
pretty(Json
json).(doc : ExtendDoc, width~ : Int) -> String
render(Int
width=20),
String
content=(
#|{
#| "key1": "string",
#| "key2": [
#| 12345,
#| 67890
#| ],
#| "key3": [
#| {
#| "field1": 1,
#| "field2": 2
#| },
#| {
#| "field1": 1,
#| "field2": 2
#| },
#| {
#| "field1": [
#| 1,
#| 2
#| ],
#| "field2": 2
#| }
#| ]
#|}
),
)
}
Conclusion
By combining a small set of primitives with function composition, we can build a flexible, declarative prettyprinter that adapts structured data layouts to the available screen width.
This approach scales well: you describe layout intentions with combinators like sepby
, surround
, or autobreak
, and the rendering engine takes care of indentation, line breaks, and fitting.
The current implementation can be further optimized:
- Memoizing
space
calculations to improve performance. - Adding a
ribbon
parameter to balance whitespace vs. content density - Supporting advanced layouts like hanging indents or mandatory line breaks
For a deeper dive, see Philip Wadler’s classic paper A prettier printer – Philip Wadler, as well as prettyprinter libraries in Haskell, OCaml, and other languages.