(std fmt)

Reference

Overview

Formatting in two layers: interpolation of scalars, and a composable layout of blocks.

(import (std prelude))
(import (std fmt))

;; Layer 1: interpolation
(println* ["Hello, " 42 ", ok? " #t])

;; Layer 2: layout
(show ["Padding: [" (padded/left 10 ["foo"]) "]\n"])
(show ["Number: " (numeric/comma 1234567.89) "\n"])
(show ["Columns:\n"
       (columnar [["first long text string" "second text string"]
                  ["col 2" "more text for col 2"]])])

Interpolation combines scalars of mixed types into text. It works on a (Vec Disp): a vec literal whose elements are strings, ints, doubles, chars and bools, each elaborated into the Disp case that holds it. println* prints one.

Layout is a block algebra, for padding, aligning, wrapping paragraphs and laying out tables. The unit is a Block: a string, int or char, which needs no wrapping in a vec literal, or a BRun, a function that writes to an output sink and threads the current column through. Every combinator takes its content as a (Vec Block) and answers a Block, so they nest. Nothing is written until the blocks are run by show, render or render->string.

A combinator of your own is a BRun and the two functions that run blocks, run-block and run-blocks. It is the same kind of thing as the ones in this module:

(: boxed (-> (Vec Block) Block))
(defun (boxed bs)
  (BRun (fun (out col)
          (run-blocks out col ["[" (render->string bs) "]"]))))

Settings are parameters, so they are dynamically scoped: with-fmt binds one around some blocks while they run, and it is restored when they end. The ones here are:

ParameterDefaultWhat it sets
current-pad#\spaceThe character padding is made of.
current-width78The right margin of wrapped, justified and columnar.
current-ellipsis""What replaces the part a trim cuts off.
current-string-widththe character countHow wide a string is, in columns.
current-precision-1Digits after the decimal separator; negative is as many as needed.
current-comma0Digits per group; zero is no grouping.
current-comma-sep#\,The character between groups.
current-decimal-sep#\.The decimal separator.
current-signed#fWhether a positive number gets a leading +.

A setting of your own works the same way: with-fmt takes the parameter as an argument. Read it with parameter-ref inside the BRun’s function, when the block runs. Read where the block is built, it is read before any enclosing with-fmt has bound it.

See also: println*, show, render->string, with-fmt, Block

Reference

Types

Functions

Values

Types

Disp

union

A scalar to interpolate: a string, int, double, char or bool.

DStr
A string, as it is.
DInt
An int, by int->string.
DDouble
A double, by double->string.
DChar
A char, as one character.
DBool
A bool, as #t or #f.

A vec literal of mixed scalars, such as ["n = " 42 ", ok? " #t], elaborates each element into the case that can hold it, so the cases are rarely written.

See also: println*

Sink

type alias

Where a block writes its text: a function given each piece of it.

See also: Block, run-block

Block

union

A piece of layout: a scalar, or a function that writes and answers the column it left.

BStr
A string, written as it is.
BInt
An int, written in decimal.
BChar
A char.
BRun
A function of the sink and the column it starts at, which writes and answers the column it ends at.
(render->string ["x = " 7 #\!])   ; "x = 7!": the leaves need no wrapping

The column is counted in characters since the last newline written. It is the one thing threaded through every block, and what lets fl, space-to and wrapped know where the cursor is.

See also: run-block, each, captured

Functions

println*

function

(: println* (-> (Vec Disp) void))
(println* ds)

Prints scalars of mixed types, then a newline.

ds
The scalars, written as a vec literal.
(println* ["n = " 42 ", ok? " #t ", " #\x ", " 1.5])

See also: Disp, show

render

function

(: render (-> TextOutputPort (Vec Block) void))
(render port bs)

Writes blocks to a port.

port
The port to write to.
bs
The blocks, run from column 0.

It is #:sync, and cannot be otherwise: the sink is stored in a BRun, and a stored function’s colour is its type, so no suspending sink can go there. To render from a fiber, use render->string and write the string yourself.

See also: show, render->string

show

function

(: show (-> (Vec Block) void))
(show bs)

Writes blocks to the current output port.

bs
The blocks, run from column 0.
(show ["Padding: [" (padded/left 10 ["foo"]) "]\n"])

render to current-output-port, and #:sync for the same reason.

See also: render, render->string

render->string

function

(: render->string (-> (Vec Block) string))
(render->string bs)

The text blocks write, as a string.

bs
The blocks, run from column 0.
returns
Everything the blocks wrote.
(render->string [(padded/left 5 ["7"])])   ; "    7"

See also: show, render, captured

run-block

function

(: run-block (-> Sink int Block int))
(run-block out col b)

Runs one block: the engine’s step, for writing combinators.

out
The sink to write to.
col
The column the block starts at.
b
The block.
returns
The column it ends at.

See also: run-blocks, Block

run-blocks

function

(: run-blocks (-> Sink int (Vec Block) int))
(run-blocks out col bs)

Runs blocks one after another, threading the column.

out
The sink to write to.
col
The column the first block starts at.
bs
The blocks.
returns
The column the last one ends at.

See also: run-block

with-fmt

function

(: with-fmt (-> (Param %a) %a (Vec Block) Block))
(with-fmt p v bs)

Runs blocks with a parameter bound to a value.

%a
The parameter's type.
p
The parameter: one of this module's settings, or one of your own.
v
Its value while the blocks run.
bs
The blocks.
returns
A block. The parameter is bound when it runs, not when it is built, and restored when it ends.
(show [(with-fmt current-precision 2 [(numeric 3.14159)])])   ; 3.14

See also: with-pad, current-width

with-pad

function

(: with-pad (-> char (Vec Block) Block))
(with-pad c bs)

Runs blocks with another padding character.

c
The padding character.
bs
The blocks.
returns
A block: (with-fmt current-pad c bs).
(show [(with-pad #\. ["ab" (space-to 6) "c"])])   ; ab....c, a dot leader

See also: current-pad, space-to

each

function

(: each (-> (Vec Block) Block))
(each bs)

Many blocks as one.

bs
The blocks.
returns
A block that runs them in order.

See also: nothing, joined

captured

function

(: captured (-> (-> string Block) (Vec Block) Block))
(captured f bs)

Renders blocks to a string and hands it to a function that answers a block.

f
Given the text the blocks wrote; its block is run in their place.
bs
The blocks to render.
returns
A block.
(captured (fun (s) (BInt (string-count s))) ["abcd"])   ; writes 4

How a combinator that needs the whole text, rather than writing as it goes, is made: padding, trimming and wrapping are all written with it. The blocks are rendered from column 0, and f’s block runs at the current column.

See also: render->string

width-of

function

(: width-of (-> string int))
(width-of s)

How many columns a string takes, by current-string-width.

s
The string.
returns
Its width.

See also: current-string-width

space-to

function

(: space-to (-> int Block))
(space-to goal)

Pads to a column.

goal
The column to move to.
returns
A block that writes padding up to goal, and nothing if the cursor is already at or past it.
(render->string ["ab" (space-to 5) "c"])   ; "ab   c"

It fills with current-pad, so under with-pad it is a leader. It never moves backwards.

See also: tab-to, with-pad

tab-to

function

(: tab-to (-> (#:width int) Block))
(tab-to #:width 8)

Pads to the next tab stop.

#:width
The distance between tab stops. 8 unless given.
returns
A block that pads to the next multiple of width.
(render->string ["ab" (tab-to #:width 4) "c"])     ; "ab  c"
(render->string ["abcd" (tab-to #:width 4) "c"])   ; "abcd    c"

It always moves: at a tab stop already, it pads to the next one.

See also: space-to

padded/left

function

(: padded/left (-> int (Vec Block) Block))
(padded/left w bs)

Pads content on the left to a width: aligned right.

w
The width, in columns.
bs
The content.
returns
A block. Content wider than w is left as it is: padding never cuts.
(render->string [(padded/left 6 ["x=" 7])])   ; "   x=7"

See also: padded/right, padded/both, fitted/left, current-pad

padded/right

function

(: padded/right (-> int (Vec Block) Block))
(padded/right w bs)

Pads content on the right to a width: aligned left.

w
The width, in columns.
bs
The content.
returns
A block. Content wider than w is left as it is.

See also: padded/left, padded/both, fitted/right

padded/both

function

(: padded/both (-> int (Vec Block) Block))
(padded/both w bs)

Pads content on both sides to a width: centred.

w
The width, in columns.
bs
The content.
returns
A block, with the odd column, if there is one, on the right. Content wider than w is left as it is.

See also: padded/left, padded/right, fitted/both

trimmed/left

function

(: trimmed/left (-> int (Vec Block) Block))
(trimmed/left w bs)

Cuts content to a width, keeping its end.

w
The width, in columns.
bs
The content.
returns
A block. Content that fits is left as it is; otherwise current-ellipsis stands at the start, where the cut was.
(render->string [(trimmed/left 3 ["abcdef"])])   ; "def"

See also: trimmed/right, trimmed/both, fitted/left, current-ellipsis

trimmed/right

function

(: trimmed/right (-> int (Vec Block) Block))
(trimmed/right w bs)

Cuts content to a width, keeping its start.

w
The width, in columns.
bs
The content.
returns
A block. Content that fits is left as it is; otherwise current-ellipsis stands at the end.
(render->string [(trimmed/right 3 ["abcdef"])])   ; "abc"

See also: trimmed/left, trimmed/both, fitted/right, current-ellipsis

trimmed/both

function

(: trimmed/both (-> int (Vec Block) Block))
(trimmed/both w bs)

Cuts content to a width, keeping its middle.

w
The width, in columns.
bs
The content.
returns
A block. Content that fits is left as it is; otherwise current-ellipsis stands at both ends.
(render->string [(trimmed/both 4 ["abcdefgh"])])   ; "cdef"

See also: trimmed/left, trimmed/right, fitted/both

fitted/left

function

(: fitted/left (-> int (Vec Block) Block))
(fitted/left w bs)

Content at exactly a width, aligned right: padded if short, cut if long.

w
The width, in columns.
bs
The content.
returns
A block: trimmed/left, then padded/left.

See also: padded/left, trimmed/left

fitted/right

function

(: fitted/right (-> int (Vec Block) Block))
(fitted/right w bs)

Content at exactly a width, aligned left: padded if short, cut if long.

w
The width, in columns.
bs
The content.
returns
A block: trimmed/right, then padded/right.
(render->string [(fitted/right 5 ["ab"]) "|"])       ; "ab   |"
(render->string [(fitted/right 3 ["abcdef"]) "|"])   ; "abc|"

See also: padded/right, trimmed/right

fitted/both

function

(: fitted/both (-> int (Vec Block) Block))
(fitted/both w bs)

Content at exactly a width, centred: padded if short, cut in the middle if long.

w
The width, in columns.
bs
The content.
returns
A block: trimmed/both, then padded/both.

See also: padded/both, trimmed/both

joined

function

(: joined (-> (-> %a Block) (List %a) (Vec Block) Block))
(joined f xs sep)

Every element of a list, formatted, with a separator between them.

%a
The elements' type.
f
Formats one element.
xs
The elements.
sep
The separator, itself blocks.
returns
A block. One element has no separator, and none is nothing.
(show [(joined (fun (n) (BInt n)) (list 1 2 3) [", "])])   ; 1, 2, 3

See also: joined/prefix, joined/suffix, joined/last

joined/prefix

function

(: joined/prefix (-> (-> %a Block) (List %a) (Vec Block) Block))
(joined/prefix f xs sep)

Every element of a list, formatted, with a separator before each, the first included.

%a
The elements' type.
f
Formats one element.
xs
The elements.
sep
The separator, itself blocks.
returns
A block.
(show [(joined/prefix (fun (s) (BStr s)) (list "a" "b") [","])])   ; ,a,b

See also: joined, joined/suffix

joined/suffix

function

(: joined/suffix (-> (-> %a Block) (List %a) (Vec Block) Block))
(joined/suffix f xs sep)

Every element of a list, formatted, with a separator after each, the last included.

%a
The elements' type.
f
Formats one element.
xs
The elements.
sep
The separator, itself blocks.
returns
A block.
(show [(joined/suffix (fun (s) (BStr s)) (list "a" "b") [","])])   ; a,b,

See also: joined, joined/prefix

joined/last

function

(: joined/last (-> (-> %a Block) (-> %a Block) (List %a) (Vec Block) Block))
(joined/last f last-f xs sep)

Like joined, with the last element formatted by a function of its own.

%a
The elements' type.
f
Formats every element but the last.
last-f
Formats the last element.
xs
The elements.
sep
The separator, itself blocks.
returns
A block. A list of one is formatted by last-f alone.
(show [(joined/last (fun (s) (BStr s))
                    (fun (s) (each ["and " (BStr s)]))
                    (list "a" "b" "c")
                    [", "])])
;; a, b, and c

The separator is written before the last element too, so what last-f adds is a word, not a comma.

See also: joined

numeric

function

(: numeric (-> double (#:precision int) (#:comma int) (#:comma-sep char) (#:decimal-sep char) (#:signed bool) Block))
(numeric x #:precision -1 #:comma -1 #:comma-sep #\nul
                #:decimal-sep #\nul #:signed #f)

A number, with a precision, grouped digits and separators of your choosing.

x
The number. An int is formatted as (cast double n).
#:precision
Digits after the decimal separator. Negative, the default, is current-precision.
#:comma
Digits per group; 0 is no grouping. Negative, the default, is current-comma.
#:comma-sep
The character between groups. #\nul, the default, is current-comma-sep.
#:decimal-sep
The decimal separator. #\nul, the default, is current-decimal-sep.
#:signed
#t gives a positive number a leading +. #f, the default, is current-signed.
returns
A block.
(numeric 2.0 #:precision 2)                  ; 2.00
(numeric -3.14159 #:precision 3)             ; -3.142
(numeric 1234567.891 #:precision 2 #:comma 3)  ; 1,234,567.89
(numeric 42.0 #:signed #t)                   ; +42
(numeric 1234.5 #:comma 3 #:comma-sep #\space #:decimal-sep #\,)  ; 1 234,5

The digits are the invariant culture’s and the separators are settings, so the machine’s locale never shows. A keyword not given defaults to its parameter when the block runs. Since a keyword’s default is what stands for the parameter, a keyword cannot ask for that default against a parameter bound otherwise: #:signed #f under a current-signed of #t still signs, and a negative #:precision still reads current-precision.

See also: numeric/comma, current-precision, current-comma

numeric/comma

function

(: numeric/comma (-> double Block))
(numeric/comma x)

A number with its digits grouped in thousands: the common case.

x
The number.
returns
A block: (numeric x #:comma 3).
(numeric/comma 1234567.0)   ; 1,234,567
(numeric/comma 42.0)        ; 42

See also: numeric

wrapped

function

(: wrapped (-> (Vec Block) Block))
(wrapped bs)

Content filled to the right margin, ragged on the right.

bs
The content. Every run of whitespace in it, newlines too, is a break between words.
returns
A block.
(with-fmt current-width 12 [(wrapped ["the quick brown fox jumps"])])
;; the quick
;; brown fox
;; jumps

The first line starts wherever the cursor is, and has only the room left before current-width. Words are separated by one current-pad. A word wider than a line gets a line of its own rather than being cut.

See also: justified, current-width

justified

function

(: justified (-> (Vec Block) Block))
(justified bs)

Content filled to the right margin and flush against both, the last line excepted.

bs
The content. Every run of whitespace in it is a break between words.
returns
A block.
(with-fmt current-width 12 [(justified ["the quick brown fox jumps"])])
;; the    quick
;; brown    fox
;; jumps

Lines are broken as wrapped breaks them, then the gaps are widened to fill the line, the leftmost ones taking any extra column. The last line, and a line of one word, are not stretched.

See also: wrapped, current-width

columnar

function

(: columnar (-> (Vec (Vec Block)) (#:sep string) Block))
(columnar cols #:sep " ")

Columns side by side, dividing current-width evenly and cutting what does not fit.

cols
The columns, each a vec of blocks; a newline in one starts its next row.
#:sep
Written between the columns. A space unless given.
returns
A block.
(show [(with-fmt current-width 11
         [(columnar [["abc" nl "def"]
                     ["123" nl "456"]])])])
;; abc   123
;; def   456

Each column is rendered on its own and the lines are laid out together. The width left once the separators are taken out is shared equally, and every cell is cut and padded to its share, the last column cut but not padded. A column with fewer lines than the others contributes blanks. The table starts on a fresh line, and every row ends with a newline.

See also: tabular, current-width

tabular

function

(: tabular (-> (Vec (Vec Block)) (#:sep string) Block))
(tabular cols #:sep " ")

Columns side by side, each as wide as its widest line, cutting nothing.

cols
The columns, each a vec of blocks; a newline in one starts its next row.
#:sep
Written between the columns. A space unless given.
returns
A block.
(show [(tabular [["a" nl "bbb"]
                 ["1" nl "2"]])])
;; a   1
;; bbb 2

(show [(tabular [["a" nl "bbb"]
                 ["1" nl "2"]]
                #:sep " | ")])
;; a   | 1
;; bbb | 2

Laid out as columnar lays them out, but with each column padded to the width its own content asks for, the last not padded at all. A column with fewer lines than the others contributes blanks. The table starts on a fresh line, and every row ends with a newline.

See also: columnar

Values

current-pad

value

(: current-pad (Param char))

The character padding is made of. A space unless set.

Padding that is not a whole number of pad characters wide is as many as fit. Words in wrapped and justified are separated by it too.

See also: with-pad, padded/left, space-to

current-width

value

(: current-width (Param int))

The right margin: what wrapped, justified and columnar fill up to. 78 unless set.

See also: wrapped, justified, columnar

current-ellipsis

value

(: current-ellipsis (Param string))

What replaces the part a trim cuts off. Empty, which cuts silently, unless set.

(with-fmt current-ellipsis "..." [(trimmed/right 6 ["abcdefgh"])])   ; abc...

It takes its room out of the content, so a trimmed text is never wider than asked for. An ellipsis wider than that is cut itself.

See also: trimmed/right

current-string-width

value

(: current-string-width (Param (-> string int)))

How many columns a string takes. Its character count unless set.

;; An astral character takes two columns on a terminal.
(defun (terminal-width s)
  (string-fold (fun (ch acc) (+ acc (if (> (char->integer ch) 65535) 2 1))) 0 s))

(with-fmt current-string-width terminal-width [(padded/left 5 ["😀"])])

Everything that measures or cuts goes through it: padding, trimming, wrapping and tables. The column a BRun is handed is not: it counts characters.

See also: width-of

current-precision

value

(: current-precision (Param int))

Digits after the decimal separator. Negative, the default, is as many as the number needs.

As many as it needs is what double->string would give. A precision rounds.

See also: numeric

current-comma

value

(: current-comma (Param int))

Digits per group in the integer part. Zero, the default, is no grouping.

See also: numeric, numeric/comma, current-comma-sep

current-comma-sep

value

(: current-comma-sep (Param char))

The character between groups of digits. A comma unless set.

See also: current-comma, numeric

current-decimal-sep

value

(: current-decimal-sep (Param char))

The decimal separator. A full stop unless set.

See also: numeric, current-comma-sep

current-signed

value

(: current-signed (Param bool))

Whether a number with no sign of its own gets a leading +. #f unless set.

See also: numeric

nothing

value

(: nothing Block)

A block that writes nothing: what a conditional arm answers when it has nothing to say.

(render->string ["a" (if verbose? nl nothing) "b"])

See also: each

nl

value

(: nl Block)

A newline.

See also: fl

fl

value

(: fl Block)

A fresh line: a newline, unless the cursor is already at the start of one.

(render->string ["a" nl fl "b"])   ; "a\nb"
(render->string [fl "a"])          ; "a"

See also: nl