Printing: written and displayed

->str gives way to two traits, as Scheme's write and display: a value
as the reader reads it, and the value for a person. Every type of your
own is written in constructor syntax. The examples, the REPL session and
the expect output follow.

🤖 Generated with [ECA](https://eca.dev)

Co-Authored-By: eca-agent <git@eca.dev>
This commit is contained in:
Linus Björnstam 2026-10-21 15:38:01 +02:00
parent 481538a1cb
commit e448ee0d93
2 changed files with 107 additions and 49 deletions

View file

@ -104,9 +104,11 @@ Then there are some special cases, like string interpolation:
(a-string "hej"))
(println $"This is interpolated: ${an-int} ${a-string}")
;; Which is literally the same as:
(println (str "This is interpolated: " (->str an-int) " " (->str a-string))))
(println (str "This is interpolated: " (displayed an-int) " " (displayed a-string))))
}
Each hole goes through @code{displayed}, so a string hole gives its text without quotes. See @em{Converting to and from strings} below.
@subsubsection{Function types}
A function takes any number of arguments and produces a value. The signature of a function in bjolang is denoted using the arrow @code{->}:
@ -456,7 +458,7 @@ A few things to know about unions:
@read-list{
- Case names share a namespace with everything else in the module, so two unions in the same module cannot both have a case called @code{Red}.
- A union case cannot have a @code{#:mutable} field.
- @code{println} on a union value without a @code{->str} implementation prints the generated C# name, something like @code{Card { rank = Queen { }, suit = Hearts { } }}. Implement @code{->str} (see the traits section) for anything you intend to print.
- @code{println} on a value of your own type prints it as you would build it, for example @code{(Card (rank Queen) (suit Hearts))} or @code{(Pip 7)}. The compiler writes this for each type you declare. To print a type another way, implement @code{Displayed} (see the traits section).
}
@subsubsection{Types that refer to themselves and each other}
@ -543,23 +545,31 @@ The rest of this chapter uses a card game as a running example, so here are its
(: tricks int))))
;; How a card prints. Traits are explained later; for now it is enough
;; to know that this is what println and #"...${...}" use.
(impl (->str Suit)
(defun (->str s)
(match s (Clubs "♣") (Diamonds "♦") (Hearts "♥") (Spades "♠"))))
;; to know that Displayed is what println and #"...${...}" use, and that
;; display-into adds the text to the string builder b.
(impl (Displayed Suit)
(defun (display-into s b)
(stringbuilder-add-string! b
(match s (Clubs "♣") (Diamonds "♦") (Hearts "♥") (Spades "♠")))))
(impl (->str Rank)
(defun (->str r)
(match r
((Pip n) (int->string n))
(Jack "J") (Queen "Q") (King "K") (Ace "A"))))
(impl (Displayed Rank)
(defun (display-into r b)
(stringbuilder-add-string! b
(match r
((Pip n) (int->string n))
(Jack "J") (Queen "Q") (King "K") (Ace "A")))))
(impl (->str Card)
(defun (->str c) #"${(record-ref c rank)}${(record-ref c suit)}"))
(impl (Displayed Card)
(defun (display-into c b)
(display-into (record-ref c rank) b)
(display-into (record-ref c suit) b)))
(println (Card (rank Queen) (suit Hearts))) ;; Q♥
(written (Card (rank Queen) (suit Hearts))) ;; "(Card (rank Queen) (suit Hearts))"
}|
Without the three implementations, @code{println} prints the card as it is written: @code{(Card (rank Queen) (suit Hearts))}.
@code{Card} is a struct because it is two small values and is copied around a lot. @code{Player} is a record because it holds a whole hand. That is the usual rule of thumb: a struct for something small that behaves like a value, a record for everything else. A struct also cannot contain itself and cannot have mutable fields.
Records are updated by copying, with @code{record-set}:
@ -1926,12 +1936,34 @@ Do not build strings by writing to a string port. Writing to a port is I/O as fa
@subsubsection{Converting to and from strings}
@code{->str} turns anything into a string, and @code{println} and interpolation use it. It is a trait, so your own types can decide what they look like (the card game did that for @code{Card}). For the basic types there are also named conversions:
Two functions turn any value into a string:
@read-list{
- @code{written} gives the value as the reader reads it, as Scheme's @code{write} does. A string is in quotes, a char is @code{#\a}, a number keeps the suffix of its type (@code{21L}), and a double always has a point (@code{3.0}). The REPL shows a result this way.
- @code{displayed} gives the value for a person to read, as Scheme's @code{display} does. A string is its text, and a number has no suffix. @code{println}, @code{print} and interpolation use it.
}
@codeblock[#:lang "bjolang"]|{
(written "hej") ;; "\"hej\""
(displayed "hej") ;; "hej"
(written 21L) ;; "21L"
(displayed 21L) ;; "21"
(written (list "a" "b")) ;; "(\"a\" \"b\")"
(displayed (list "a" "b")) ;; "(a b)"
(written (Some (Tuple 1 "x"))) ;; "(Some (Tuple 1 \"x\"))"
(println (Fruit (name "banana") (amount 5) (ripeness Rotten)))
;; prints (Fruit (name "banana") (amount 5) (ripeness Rotten))
}|
Every type you declare is written as you would build it. A type that prints differently for a person implements @code{Displayed}, as the card game did for @code{Card}; one that is written differently implements @code{Written}. The traits section has more. @code{->str} is deprecated: use @code{displayed}.
For the basic types there are also named conversions:
@codeblock[#:lang "bjolang"]|{
(int->string 42) ;; "42"
(double->string 2.5) ;; "2.5"
(->str 3.0) ;; "3"
(displayed 3.0) ;; "3"
(written 3.0) ;; "3.0"
(string->int "42") ;; 42, and throws on anything that is not a number
(string->double "1.5") ;; 1.5
(char->string #\a) ;; "a"
@ -2018,7 +2050,7 @@ Define patterns at the top level, as above, rather than writing @code{#rx(...)}
@subsection{Traits}
A trait is a set of functions that a type can implement. It is how bjolang does what other languages do with interfaces, type classes or overloading: @code{=} is a trait method, so is @code{->str}, and so are @code{length}, @code{fold} and @code{compare}.
A trait is a set of functions that a type can implement. It is how bjolang does what other languages do with interfaces, type classes or overloading: @code{=} is a trait method, so is @code{displayed}, and so are @code{length}, @code{fold} and @code{compare}.
@subsubsection{Declaring and implementing a trait}
@ -2089,7 +2121,7 @@ A generic function that calls a trait method has to say that its type variable i
@code{describe-all} can now be called with a vec of anything that has a @code{Describe}, and calling it with anything else is a compile error at the call.
For @code{Eq} and @code{->str} you may leave the @code{where} out: the compiler adds them by itself when a function uses @code{=} or @code{->str} on a type variable.
For @code{Eq}, @code{Written} and @code{Displayed} you may leave the @code{where} out: the compiler adds them by itself when a function uses @code{=}, @code{written} or @code{displayed} on a type variable.
@subsubsection{Associated types}
@ -2189,7 +2221,7 @@ A trait may say that every type implementing it implements another trait too, wi
@code{introduce} asks only for @code{Greet}, and may still call @code{name-of}: a type that can greet is known to have a name. An implementation of @code{Greet} for a type with no @code{Named} is a compile error at the implementation.
The prelude's @code{Ord} requires @code{Eq} this way, so a function with @code{(where (Ord %a))} can use @code{=} as well as @code{compare}.
The prelude's @code{Ord} requires @code{Eq} this way, so a function with @code{(where (Ord %a))} can use @code{=} as well as @code{compare}. @code{Displayed} requires @code{Written} in the same way.
@subsubsection{Giving a newtype the traits of the type it wraps}
@ -2216,16 +2248,17 @@ A newtype is the type it wraps when the program runs, so it can borrow that type
A trait that should behave differently is written by hand instead. That is the other use of a newtype: the same data with a different behaviour, such as an @code{Eq} that ignores case on a @code{(Newtype string)}. Sets and maps of the newtype use that implementation, not the wrapped type's.
@subsubsection{Eq, Ord and ->str}
@subsubsection{Eq, Ord, Written and Displayed}
Three traits from the prelude are worth knowing by heart, since the rest of the library leans on them.
Four traits from the prelude are worth knowing by heart, since the rest of the library leans on them.
@read-table{
| Trait | Methods | Gives you |
|----------------+---------------------------------+------------------------------------------------------------------|
| @code{Eq} | @code{=}, @code{eq-hash} | equality, and use as a key in maps and sets |
| @code{Ord} | @code{compare} | @code{less?}, @code{least}, @code{list-sort}, @code{list-max}, ordered collections |
| @code{->str} | @code{->str} | @code{println}, @code{print}, string interpolation |
| Trait | Methods | Gives you |
|-------------------+-----------------------------------------+------------------------------------------------------------------|
| @code{Eq} | @code{=}, @code{eq-hash} | equality, and use as a key in maps and sets |
| @code{Ord} | @code{compare} | @code{less?}, @code{least}, @code{list-sort}, @code{list-max}, ordered collections |
| @code{Written} | @code{write-into}, @code{written} | the REPL's output, the failure text of @code{expect} |
| @code{Displayed} | @code{display-into}, @code{displayed} | @code{println}, @code{print}, string interpolation |
}
@code{Eq} and @code{Ord} can be derived with @code{type/derive}, as shown earlier. When you write them by hand, they must be written in the module that declares the type, because they are compiled into the type itself: they become its .NET @code{Equals}, @code{GetHashCode} and @code{CompareTo}, which is why a .NET dictionary or a sorted collection agrees with @code{=} and @code{compare}. @code{=} and @code{eq-hash} must agree: two values that are equal must have the same hash. @code{compare} returns a negative number, zero or a positive number.
@ -2239,19 +2272,36 @@ Three traits from the prelude are worth knowing by heart, since the rest of the
(hash-combine (eq-hash (record-ref c rank)) (eq-hash (record-ref c suit)))))
}|
@code{->str} has a blanket implementation that uses .NET's @code{ToString}, which is why printing one of your own types shows its C# name until you implement it.
The compiler writes a @code{Written} for every type you declare, so a value prints as you would build it: @code{(Card (rank Queen) (suit Hearts))}, @code{(Pip 7)}, @code{Hearts}. A generic type is written when its type arguments are. An @code{#:opaque} type is written as @code{#<Name>}, and a field that holds a function as @code{#<procedure>}. @code{Displayed} has a blanket implementation that displays a value as it is written.
To print a type your own way, implement @code{Displayed} for the text a person reads, as the card game did, or @code{Written} to change the written form too. The method to give is @code{display-into} (or @code{write-into}), which adds the text to a string builder; @code{displayed} and @code{written} then come with the trait:
@codeblock[#:lang "bjolang"]|{
(type (: Money (Union (: Kr int))))
(impl (Displayed Money)
(defun (display-into m b)
(match m ((Kr n) (stringbuilder-add-string! b #"${n} kr")))))
(println (Kr 42)) ;; 42 kr
(written (Kr 42)) ;; "(Kr 42)"
}|
A container that holds other values calls @code{write-into} or @code{display-into} on them with the same builder. @code{elements-print-into} and @code{entries-print-into} from the prelude lay out the elements of a collection, or the entries of a map, for you.
@code{->str} is deprecated. It still works, through @code{displayed}, but each use gives a warning.
@subsubsection{Values of different types in one collection: dyn}
A list holds values of one type. When you need a collection of different types that all implement the same trait, pack each value as a @code{(dyn Trait)}:
@codeblock[#:lang "bjolang"]|{
(: render-all (-> (List (dyn ->str)) (List string)))
(defun (render-all xs) (list-map ->str xs))
(: render-all (-> (List (dyn Displayed)) (List string)))
(defun (render-all xs) (list-map displayed xs))
(render-all (list (dyn ->str 1)
(dyn ->str "two")
(dyn ->str (Card (rank Queen) (suit Hearts)))))
(render-all (list (dyn Displayed 1)
(dyn Displayed "two")
(dyn Displayed (Card (rank Queen) (suit Hearts)))))
;; '("1" "two" "Q♥")
}|