From e448ee0d9336cb2cd3e4843607581dacf9366ffa539ddcc93e0ad486bda39b9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Bj=C3=B6rnstam?= Date: Wed, 21 Oct 2026 15:38:01 +0200 Subject: [PATCH] Printing: written and displayed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ->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 --- _01.Language-basics.sz | 112 ++++++++++++++++++++++++++++----------- _02_.Writing-programs.sz | 44 ++++++++------- 2 files changed, 107 insertions(+), 49 deletions(-) diff --git a/_01.Language-basics.sz b/_01.Language-basics.sz index ddbdef5..c7df994 100644 --- a/_01.Language-basics.sz +++ b/_01.Language-basics.sz @@ -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{#}, and a field that holds a function as @code{#}. @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♥") }| diff --git a/_02_.Writing-programs.sz b/_02_.Writing-programs.sz index 5d2d9cf..ae452b5 100644 --- a/_02_.Writing-programs.sz +++ b/_02_.Writing-programs.sz @@ -22,18 +22,22 @@ Here is the deck from the first part as a module of its own. Put it in @code{dec (: Rank (Union (: Pip int) Jack Queen King Ace)) (: Card (Struct (: rank Rank) (: suit Suit)))) -(impl (->str Suit) - (defun (->str s) - (match s (Clubs "♣") (Diamonds "♦") (Hearts "♥") (Spades "♠")))) +(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))) ;; Not exported: only this module needs them. (def all-suits [Clubs Diamonds Hearts Spades]) @@ -78,7 +82,7 @@ Notice how the imports and exports interact here: @read-list{ - Relative imports: @code{(import "deck.bjo")} specifies a file path relative to the importing file. Bjolang compiles @code{deck.bjo} into @code{deck.dll} ahead of time, automatically rebuilding it whenever it (or any of its dependencies) changes. - Type exports are complete: Exporting a type exposes all its constructors, union cases, and fields. Because @code{Card} is exported, @code{game.bjo} can freely construct @code{(Card (rank Ace) (suit Spades))} and pattern-match on its fields. -- Trait implementations are automatic: Although @code{->str} was not explicitly listed in the export list, its implementations still work in @code{game.bjo}. Because @code{impl} blocks define anonymous implementations rather than named bindings, they cannot be exported individually; they are automatically carried along whenever the module defining them is loaded. +- Trait implementations are automatic: Although @code{Displayed} was not explicitly listed in the export list, its implementations still work in @code{game.bjo}. Because @code{impl} blocks define anonymous implementations rather than named bindings, they cannot be exported individually; they are automatically carried along whenever the module defining them is loaded. - Private definitions: Unexported bindings like @code{all-suits} remain private to @code{deck.bjo} and are inaccessible from outside. } @@ -148,7 +152,7 @@ so its representation is visible only to the code of pile. A value of it can be passed on here, and built and taken apart through the functions that module exports. }| -Even though the type's representation is private, any trait implementations defined in @code{pile.bjo} remain available to consumers. For instance, an @code{(impl (->str Pile) ...)} inside @code{pile.bjo} allows any importing module to convert a @code{Pile} to a string. +Even though the type's representation is private, any trait implementations defined in @code{pile.bjo} remain available to consumers. For instance, an @code{(impl (Displayed Pile) ...)} inside @code{pile.bjo} allows any importing module to print a @code{Pile}. Without one, a @code{Pile} is written as @code{#}, so printing it shows nothing of its representation. @subsubsection{Import modifiers} @@ -221,7 +225,7 @@ While @code{export} publishes definitions authored in the current module, @code{ (re-export Card Rank Suit full-deck shuffle Pile new-pile draw) }| -Re-exporting a type exposes the exact original type, not a distinct copy; a @code{Card} referenced via @code{cards.bjo} is identical to one from @code{deck.bjo}. However, trait implementations (@code{impl}) do not transfer through re-exports. They remain bound to the module where they were defined, meaning any file that wants to format cards with @code{->str} must still import @code{deck.bjo}. +Re-exporting a type exposes the exact original type, not a distinct copy; a @code{Card} referenced via @code{cards.bjo} is identical to one from @code{deck.bjo}. However, trait implementations (@code{impl}) do not transfer through re-exports. They remain bound to the module where they were defined, meaning any file that wants to print cards with their @code{Displayed} implementation must still import @code{deck.bjo}. You can provide alternative names for bindings or macros using @code{(:alias new old)}. Combining @code{:alias} with @code{export} or @code{re-export} allows libraries to offer cleaner or more idiomatic public APIs: @@ -471,7 +475,7 @@ To illustrate, we can add a persistent high-score ledger to our card game. Each (defun (main) (save-score "ada" 31) (save-score "bo" 27) - (println (read-scores)) ;; [(ada, 31) (bo, 27)] + (println (read-scores)) ;; [(Tuple ada 31) (Tuple bo 27)] 0) }| @@ -1730,7 +1734,7 @@ Invalid literal syntax is flagged with a compile-time error at the point of use: The hash macro 'card' failed at bad.bjo:3: a suit is clubs, diamonds, hearts or spades — in hurts }| -Notice the @code{re-export} in @code{cardmacros.bjo}: because the macro expansion emits constructor symbols like @code{Card} and @code{Queen}, those types must be resolvable in the module invoking the macro. Re-exporting them ensures that importing @code{cardmacros.bjo} alone brings the required types into scope. Importing @code{deck.bjo} alongside it remains recommended so that the corresponding @code{->str} implementations are also present. +Notice the @code{re-export} in @code{cardmacros.bjo}: because the macro expansion emits constructor symbols like @code{Card} and @code{Queen}, those types must be resolvable in the module invoking the macro. Re-exporting them ensures that importing @code{cardmacros.bjo} alone brings the required types into scope. Importing @code{deck.bjo} alongside it remains recommended so that the corresponding @code{Displayed} implementations are also present. @subsubsection{Literals read by a grammar} @@ -1850,7 +1854,7 @@ ok: an ace is worth 11 ok: shuffling keeps the cards }| -Whenever an assertion fails, the output reports both the computed value and the expected value: @code{FAILURE: deliberately wrong gave 2 but wanted 3}. +Whenever an assertion fails, the output reports both the computed value and the expected value, each as @code{written} gives it: @code{FAILURE: deliberately wrong gave 2 but wanted 3}, or @code{gave "4" but wanted "5"} for strings. So @code{expect} needs a @code{Written} for the type, which every type has. For asynchronous or resource-intensive testing, @code{(with-test "name" body ...)} executes the test body within an isolated structured concurrency scope configured with a default deadline. Any background fibers spawned during the test are automatically awaited, and the test fails if unclosed file ports remain open when the scope exits. Because @code{with-test} forms a concurrency scope, it must be invoked from within a bjoroutine. @@ -1885,14 +1889,18 @@ You can launch an interactive Read-Eval-Print Loop by running @code{bjo repl}. W bjo> (+ 1 2) 3 bjo> (import "deck.bjo") -bjo> (vec-slice (full-deck) 0 3) -[2♣ 3♣ 4♣] +bjo> (vec-slice (full-deck) 0 2) +[(Card (rank (Pip 2)) (suit Clubs)) (Card (rank (Pip 3)) (suit Clubs))] +bjo> (displayed (vec-slice (full-deck) 0 3)) +"[2♣ 3♣ 4♣]" bjo> (defun (double (: x int)) : int (* x 2)) double bjo> (double 21) 42 }| +The REPL shows a result with @code{written}, as the reader reads it: a string in quotes, a card as it is built. @code{println} and @code{displayed} give the text a person reads. + Unlike many Lisp REPLs, Bjolang's REPL does not interpret forms. Instead, each entered interaction is compiled on the fly into an in-memory assembly using the exact same compiler pipeline applied to files on disk, ensuring identical semantics between interactive sessions and compiled production builds. Top-level functions declared in the REPL require explicit type signatures, mirroring the rules for modules. You can supply parameter and return types inline directly on the @code{defun}, or submit a standalone signature on its own line immediately preceding the definition: