diff --git a/_02_.Writing-programs.sz b/_02_.Writing-programs.sz index 0fb0aea..b3b292e 100644 --- a/_02_.Writing-programs.sz +++ b/_02_.Writing-programs.sz @@ -599,6 +599,399 @@ Prefixing an identifier with a comma (e.g. @code{,who}) splices the variable's v All of these functions return a @code{Result} and execute synchronously, blocking the calling thread until completion. When running inside a bjoroutine, non-blocking asynchronous alternatives are available; refer to the @code{(std run)} module documentation for details. +@subsection{Formatting text} + +For one line of output, an interpolated string or @code{println*} is enough. @code{println*} is in @code{(std fmt)}, and takes strings, ints, doubles, chars and bools in one vec: + +@codeblock[#:lang "bjolang"]|{ +(import (std fmt)) + +(println #"${who} has ${points} points") ;; ada has 31 points +(println* [who " has " points " points"]) ;; ada has 31 points +}| + +When the shape of the output matters, such as columns that line up, padding, a paragraph wrapped to a width or a table, use the layout part of @code{(std fmt)}. This section is a guide to it. The module's page has the reference. + +@subsubsection{A block and a vec of blocks} + +Layout is made of two types, and nearly every type error in layout code is one of them where the other is wanted: + +@read-list{ + - A @code{Block} is one piece of output: a string, an int, a char, or a combinator such as @code{(padded/left 5 ["x"])}. + - A @code{(Vec Block)} is pieces of output in order, written as a vec literal: @code{["x = " 7 nl]}. +} + +There is one rule. Every combinator takes its content as a @code{(Vec Block)} and gives back one @code{Block}. @code{show}, @code{render} and @code{render->string}, which write the output, take a @code{(Vec Block)}. So the brackets go around content, and a combinator call is one element in them: + +@codeblock[#:lang "bjolang"]|{ +(show [(padded/left 5 ["x"])]) ;; " x" +(show [(padded/left 5 ["x"]) (padded/left 5 ["y"]) nl]) ;; " x y", then a newline +(show ["Total:" (padded/left 8 [42]) nl]) ;; "Total: 42" +}| + +Read @code{(padded/left 5 ["x"])} as "pad this content to 5 columns". The content is a vec even when it is one thing, as the body of a @code{begin} is a body even when it is one form. These are refused: + +@codeblock[#:lang "bjolang"]|{ +(show (padded/left 5 "x")) ;; "x" is not content, and show wants a vec +(show (padded/left 5 ["x"])) ;; show wants a vec, and this is one block +(show [["x"]]) ;; a vec in a vec: a vec is not a block +}| + +More brackets do not help. A vec of vecs is right in one place only: a table, which is a vec of rows or of columns, each of them a @code{(Vec Block)}. + +Nothing is written until @code{show} runs the blocks. A block is a value: you can bind it, pass it to a function and return it. + +@subsubsection{What goes in the brackets} + +A vec literal is read element by element when it stands where a @code{(Vec Block)} is wanted: as an argument of a function that takes one, as the return value of a function declared to return one, or as the value of a binding whose type is written. Each element is one of these: + +@read-table{ +| Element | Is read as | +|--------------------------------------------------------------------------------+---------------------------------------------| +| A string, an int or a char: a literal, a variable or a call | @code{(BStr s)}, @code{(BInt n)}, @code{(BChar c)} | +| A @code{Block}: @code{nl}, @code{nothing}, a combinator call, a variable of type @code{Block} | itself | +| An @code{if}, @code{cond}, @code{match} or @code{let} whose results are any of these | the result it gives | +} + +So variables and calls go in as they are: + +@codeblock[#:lang "bjolang"]|{ +(def name "Ada") +(: n int) +(def n 3) +(show ["Hello, " name "! You have " n " new messages" #\. nl]) +;; Hello, Ada! You have 3 new messages. + +(show [(string-upcase name) " " (vec-length [1 2 3]) nl]) ;; ADA 3 +(show ["Paid: " (if paid? "yes" "no") nl]) ;; Paid: yes +(show ["Title: " (match title ((Some t) t) (None "-")) nl]) ;; Title: Dr +}| + +Anything else is converted first: + +@read-table{ +| You have | Write | +|-----------------------------------+---------------------------------------------------------------| +| A double @code{x} | @code{(numeric x)}, or @code{(numeric x #:precision 2)} | +| A bool @code{b} | @code{(if b "yes" "no")} | +| A long @code{n} | @code{(long->string n)}, or @code{(bytes n)} for a size | +| A @code{(Vec Block)} @code{bs} | @code{(each bs)} | +| A vec literal | @code{(each [...])}, or its elements in the outer vec | +| A @code{(Vec string)} @code{xs} | @code{(each (vec-map BStr xs))} | +| A @code{(List %a)} @code{xs} | @code{(joined f xs [", "])}, where @code{f} makes the block of one element | +} + +@strong{A number bound by @code{def}.} The @code{(: n int)} above is needed. @code{(def n 3)} alone does not say which kind of number @code{n} is, as it could still become a long or a double, so @code{[n]} cannot read it as a block, and the compiler says so at @code{n}. The same holds for @code{(def xs [1 2 3])} and @code{(def total (+ 1 2))}. A number that comes from a typed parameter, a record field or a function such as @code{vec-length} has its type already, and a number written in the vec itself, as in @code{["n = " 3]}, is an int. + +@strong{A vec bound by @code{def}.} @code{(def parts ["a" "b"])} is a @code{(Vec string)}, which is not a @code{(Vec Block)}: write its type, and it is read as blocks. + +@codeblock[#:lang "bjolang"]|{ +(: parts (Vec Block)) +(def parts ["a" 1 #\b]) +(show parts) ;; a1b +(show ["<" (each parts) ">" nl]) ;; +}| + +@subsubsection{From one type to the other} + +@read-table{ +| You have | You want a @code{Block} | You want a @code{(Vec Block)} | +|-------------------------+-------------------------------------+-------------------------------| +| a string @code{s} | @code{(BStr s)} | @code{[s]} | +| an int @code{n} | @code{(BInt n)} | @code{[n]} | +| a block @code{b} | @code{b} | @code{[b]} | +| blocks @code{bs} | @code{(each bs)} | @code{bs} | +| pieces | @code{(each [name ": " n])} | @code{[name ": " n]} | +} + +Outside a vec literal nothing is converted: a function declared to return a @code{Block} returns @code{(BStr s)} or @code{(each [s])}, not @code{s}. @code{BStr}, @code{BInt} and @code{BChar} are the cases of @code{Block}, and like any constructor they are functions, so @code{(vec-map BStr names)} is a @code{(Vec Block)}. + +@subsubsection{Blocks from your data} + +Most layout code is a function from a value of your own to a block. Give it a signature: the signature is what tells the compiler that the literal inside is blocks. + +@codeblock[#:lang "bjolang"]|{ +(type (: Item (Record (: name string) (: price double) (: stock int)))) + +(: stock (Vec Item)) +(def stock + [(Item (name "Green tea") (price 3.5) (stock 12)) + (Item (name "Gaiwan") (price 1240.0) (stock 2)) + (Item (name "Kettle") (price 89.9) (stock 0))]) + +(: item-line (-> Item Block)) +(defun (item-line i) + (each [(padded/right 10 [(record-ref i name)]) + (padded/left 9 [(numeric (record-ref i price) #:precision 2 #:comma 3)]) + (if (= (record-ref i stock) 0) " sold out" nothing) + nl])) + +(show [(each (vec-map item-line stock))]) +}| + +@codeblock[#:lang "text"]|{ +Green tea 3.50 +Gaiwan 1,240.00 +Kettle 89.90 sold out +}| + +@code{item-line} returns one block, so its pieces are under @code{each}. @code{vec-map} gives a vec of blocks, and @code{each} makes that one block too. A function can return content instead, a @code{(Vec Block)}, and the caller decides where it goes: + +@codeblock[#:lang "bjolang"]|{ +(: labelled (-> string int (Vec Block))) +(defun (labelled label n) + [label ": " n nl]) + +(show (labelled "cups" 3)) ;; cups: 3 +(show [">> " (each (labelled "pots" 1))]) ;; >> pots: 1 +}| + +A lambda has no signature, so a vec literal returned from one is not read as blocks: in @code{(vec-map (fun (i) [(record-ref i name) (record-ref i stock)]) stock)} the vec has a string and an int in it, and is refused. Put the literal under @code{each}, which says that it is content, or write a function with a signature: + +@codeblock[#:lang "bjolang"]|{ +(show [(each (vec-map (fun (i) (each [(record-ref i name) ": " (record-ref i stock) nl])) + stock))]) +;; Green tea: 12 +;; Gaiwan: 2 +;; Kettle: 0 +}| + +@code{nothing} is the block that writes nothing, for the branch that has nothing to say. In a vec literal, what an @code{if}, a @code{match} or a @code{cond} gives is read as any element is, so an arm can be a string. In a function declared to return a @code{Block}, nothing is converted, so there the arms are blocks: + +@codeblock[#:lang "bjolang"]|{ +(: greeting (-> string (Option string) Block)) +(defun (greeting name title) + (each ["Hello, " + (match title + ((Some t) (each [t " "])) + (None nothing)) + name "!"])) + +(: medal (-> int Block)) +(defun (medal place) + (cond ((= place 1) (BStr "gold")) + ((= place 2) (BStr "silver")) + (else nothing))) + +(show [(greeting "Ada" (Some "Dr")) nl (greeting "Bob" None) nl]) +;; Hello, Dr Ada! +;; Hello, Bob! +}| + +A list is written with @code{joined}, which takes a function that makes each element's block and a separator, which is content too: + +@codeblock[#:lang "bjolang"]|{ +(show [(joined BStr (list "tea" "milk" "honey") [", "]) nl]) ;; tea, milk, honey +(show [(joined/last BStr (fun (s) (each ["and " s])) + (list "tea" "milk" "honey") [", "]) nl]) ;; tea, milk, and honey +(show [(joined (fun (s) (each ["* " s])) (list "tea" "milk") [nl]) nl]) +;; * tea +;; * milk +}| + +@subsubsection{Padding, cutting and leaders} + +@code{padded/left}, @code{padded/right} and @code{padded/both} pad content to a width: aligned right, left, or centred. Padding never cuts. @code{trimmed/left}, @code{trimmed/right} and @code{trimmed/both} cut content to a width, and keep its end, its start or its middle. @code{fitted/left}, @code{fitted/right} and @code{fitted/both} do both, so the content is exactly the width. + +@codeblock[#:lang "bjolang"]|{ +(show [(padded/right 8 [who]) (padded/left 4 [points]) nl]) ;; ada 31 +(show [(with-pad #\0 [(padded/left 4 [42])]) nl]) ;; 0042 +(show [(with-pad #\= [(padded/both 20 [" Receipt "])]) nl]) ;; ===== Receipt ====== +(show [(trimmed/left 6 ["/home/ada/scores.txt"]) nl]) ;; es.txt +(show [(with-fmt current-ellipsis "…" + [(trimmed/left 12 ["/home/ada/scores.txt"])]) nl]) ;; …/scores.txt +(show ["[" (fitted/both 6 ["ab"]) "]" nl]) ;; [ ab ] +}| + +@code{space-to} pads to a column, whatever was written before it, and @code{with-pad} changes what the padding is made of. Together they make a leader: + +@codeblock[#:lang "bjolang"]|{ +(show [(with-pad #\. ["Tea" (space-to 12) "3.50"]) nl + (with-pad #\. ["Gaiwan" (space-to 12) "1240.00"]) nl]) +;; Tea.........3.50 +;; Gaiwan......1240.00 +}| + +@code{nl} is a newline, and @code{fl} a newline unless the cursor is already at the start of a line. + +@subsubsection{Numbers} + +An int is a block. A double is not, as there is no one way to write one: @code{numeric} says how, with a precision, digits in groups, and the separators. @code{bytes} and @code{percent} are blocks for two common kinds of number. + +@codeblock[#:lang "bjolang"]|{ +(show [(numeric 1234567.891 #:precision 2 #:comma 3) " " + (numeric 0.5 #:precision 3) " " + (numeric 42.0 #:signed #t) " " + (percent 0.125) " " + (bytes 1500L #:si #t) nl]) +;; 1,234,567.89 0.500 +42 12.5% 1.5 kB +}| + +The digits are always the invariant culture's, so the machine's locale never shows. The separators are settings, described below. + +@subsubsection{Paragraphs} + +@code{wrapped} fills content to @code{current-width}, and @code{justified} makes it flush against both margins. Every run of whitespace in the content is a break between words. + +@codeblock[#:lang "bjolang"]|{ +(show [(with-fmt current-width 20 + [(justified ["Bjolang lays text out with blocks, and blocks nest."])]) + nl]) +;; Bjolang lays text +;; out with blocks, and +;; blocks nest. +}| + +The first line starts where the cursor is, so after a label it has less room. The lines after it start at column 0. + +@subsubsection{Tables} + +A table is the one place where a vec of vecs is right. @code{tabular} takes a vec of columns, and @code{tabular/rows} a vec of rows. Each column or row is a @code{(Vec Block)}, so the inner vecs are read as blocks as any other content is: + +@codeblock[#:lang "bjolang"]|{ +(show [(tabular [["tea" nl "gaiwan" nl "kettle"] + [3 nl 1240 nl 90]] + #:sep " " #:align [AlignLeft AlignRight])]) + +(show [(tabular/rows [["tea" 3] ["gaiwan" 1240] ["kettle" 90]] + #:sep " " #:align [AlignLeft AlignRight])]) +}| + +Both write the same table: + +@codeblock[#:lang "text"]|{ +tea 3 +gaiwan 1240 +kettle 90 +}| + +In @code{tabular} a column's cells are separated by @code{nl}. In @code{tabular/rows} each element of a row is one cell, so a cell of several pieces is under @code{each}, and a newline in a cell gives the row another line: + +@codeblock[#:lang "bjolang"]|{ +(show [(tabular/rows [["tea" (each [3 " cups"])] + ["kettle" (each ["boils in" nl "4 minutes"])]] + #:style StyleLight)]) +}| + +@codeblock[#:lang "text"]|{ +┌────────┬───────────┐ +│ tea │ 3 cups │ +│ kettle │ boils in │ +│ │ 4 minutes │ +└────────┴───────────┘ +}| + +Rows from data come from a function with a signature, for the reason given above. Here is the high-score ledger from "Files and I/O" as a table: + +@codeblock[#:lang "bjolang"]|{ +(: score-row (-> (Tuple string int) (Vec Block))) +(defun (score-row s) + (def (Tuple name points) s) + [name points]) + +(show [(tabular/rows (vec-map score-row (read-scores)) + #:titles ["Player" "Score"] + #:align [AlignLeft AlignRight] + #:style StyleRounded)]) +}| + +@codeblock[#:lang "text"]|{ +╭────────┬───────╮ +│ Player │ Score │ +├────────┼───────┤ +│ ada │ 31 │ +│ bo │ 27 │ +╰────────┴───────╯ +}| + +@code{#:style} takes a @code{TableStyle}: @code{StyleNone}, the default, @code{StyleSimple}, @code{StyleAscii}, @code{StyleLight}, @code{StyleHeavy}, @code{StyleDouble}, @code{StyleRounded}, @code{StyleMarkdown}, or @code{StyleCustom} with pieces of your own. @code{#:align} takes an @code{Align} for each column, from the first: @code{AlignLeft}, @code{AlignRight}, @code{AlignCentre}, or @code{(AlignOn #\.)}, which puts every decimal point of the column in one place. @code{#:widths} and @code{#:overflow} make a table fit @code{current-width}, by cutting or wrapping its cells. + +A table whose rows are not all known yet, such as results that come one at a time, is laid out a row at a time with @code{table-layout} and @code{table-row}. The widths are fixed beforehand, so each row is written when it is known: + +@codeblock[#:lang "bjolang"]|{ +(def layout (table-layout [8 6] #:sep " " #:align [AlignLeft AlignRight])) +(show [(table-row layout ["Player" "Score"])]) +(loop (:for (Tuple name points) (read-scores)) + (:do (show [(table-row layout [name points])]))) +}| + +@codeblock[#:lang "text"]|{ +Player Score +ada 31 +bo 27 +}| + +A table can also be a quoted literal, which the compiler checks where it is written. A column can say what kind of number it holds: + +@codeblock[#:lang "bjolang"]|{ +(show-table '(table (style light) header + (column "Player") + (column "Score" (align right)) + (column "Share" (percent (precision 0))) + (rows (row "ada" 31 0.46) (row "bo" 27 0.40) (row "cy" 9 0.13)))) +}| + +@codeblock[#:lang "text"]|{ +┌────────┬───────┬───────┐ +│ Player │ Score │ Share │ +├────────┼───────┼───────┤ +│ ada │ 31 │ 46% │ +│ bo │ 27 │ 40% │ +│ cy │ 9 │ 13% │ +└────────┴───────┴───────┘ +}| + +The rows can be spliced, @code{(rows ,scores)}, from a @code{(Vec (Vec TableCell))}. See @code{Table} on the module's page. + +@subsubsection{Settings} + +The pad character, the right margin, the ellipsis and the number formats are parameters. @code{with-fmt} binds one around some blocks while they run, and @code{with-pad} is @code{with-fmt} for @code{current-pad}. Settings nest: + +@codeblock[#:lang "bjolang"]|{ +(show [(with-fmt current-comma 3 + [(with-fmt current-comma-sep #\space + [(with-fmt current-decimal-sep #\, + [(numeric 1234567.5)])])]) + nl]) +;; 1 234 567,5 +}| + +The settings are @code{current-pad}, @code{current-width}, @code{current-ellipsis}, @code{current-string-width}, @code{current-precision}, @code{current-comma}, @code{current-comma-sep}, @code{current-decimal-sep} and @code{current-signed}. For text with wide characters or colours in it, which a terminal shows in more or fewer columns than it has characters, set @code{current-string-width} to @code{terminal-width}. + +@subsubsection{When it does not compile} + +The messages for the common mistakes, and what to write instead: + +@read-table{ +| The compiler says | Write | +|----------------------------------------------------------------------+-----------------------------------------------------| +| expected @code{(Vec fmt/Block)}, found @code{string} | @code{["x"]} for @code{"x"} | +| expected @code{(Vec fmt/Block)}, found @code{fmt/Block} | @code{[b]} for @code{b} | +| expected @code{fmt/Block}, found @code{(Vec ...)} | @code{(each bs)} for @code{bs} | +| They differ inside the Vec: @code{fmt/Block} against @code{string} | @code{(: bs (Vec Block))} above @code{(def bs ...)} | +| Block: expected a string, an int, a char ...; found a vec | @code{(each [...])}, or no inner brackets | +| Block: ... found the number 1.5 | @code{(numeric 1.5)} | +| Block: ... found a boolean | @code{(if b "yes" "no")} | +| Block: 'n' is a number, and nothing says yet whether it is an int ... | @code{(: n int)} above @code{(def n 3)} | +| what @code{f} returns has the wrong type: expected @code{fmt/Block}, found @code{string} | @code{(BStr s)} or @code{(each [s])} | +} + +When none of these fits, find the place where a @code{Block} and a @code{(Vec Block)} meet, and check it against the rule: content in brackets, one block out. + +@subsubsection{A block of your own} + +@code{captured} renders content to a string and gives it to a function, which returns the block to write in its place. It is how a block that needs the whole of its content is made, and how padding and trimming are written: + +@codeblock[#:lang "bjolang"]|{ +(: boxed (-> (Vec Block) Block)) +(defun (boxed bs) + (captured (fun (s) (each ["[" s "]"])) bs)) + +(show [(boxed ["x = " 7]) nl]) ;; [x = 7] +}| + +A block that writes as it goes is a @code{BRun}: a function given the sink to write to and the column the cursor is at, which returns the column it ends at. @code{run-blocks} runs content inside one. The module's page describes both. + @subsection{Concurrency} Bjolang's concurrency model rests on three foundations: lightweight fibers called *bjoroutines*, communication channels, and structured concurrency scopes ensuring background work is cleanly contained. In addition, Bjolang adopts the Concurrent ML (CML) paradigm, where communication operations—such as sending or receiving on a channel—are first-class values that can be composed and combined before being synchronized. diff --git a/_03.Standard-library.sz b/_03.Standard-library.sz index 907937b..811a032 100644 --- a/_03.Standard-library.sz +++ b/_03.Standard-library.sz @@ -19,9 +19,7 @@ For more details, see @link["modules/std/array.html"]{@code{(std array)}} and @l @subsection{std/fmt} -Formatting in Bjolang is divided into two layers: interpolation and layout. -Interpolation provides a simple way to combine mixed scalar types into strings or print them directly using @code{println*} and @code{str*}. -The layout layer provides a composable block algebra for more complex text formatting, such as padding, aligning, wrapping paragraphs, and laying out tables. +Formatting in two layers: interpolation and layout. @code{println*} prints strings, ints, doubles, chars and bools written in one vec. The layout layer is made of blocks: a @code{Block} is one piece of output, and every combinator takes its content as a @code{(Vec Block)}, a vec literal such as @code{["x = " 7 nl]}, and gives back one @code{Block}. With them you pad, cut and align text, wrap and justify paragraphs, format numbers, and lay out tables with borders, a header, and columns that fit the width. The section on formatting text in "Writing programs" is a guide, with what goes in the brackets and what to write when it does not compile. For more details, see @link["modules/std/fmt.html"]{@code{(std fmt)}}. @subsection{std/rx}