From 48771a225a7101d384434abecda1ea6d4b36241859b10179bd4eaadf18fc95a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Bj=C3=B6rnstam?= Date: Sat, 3 Oct 2026 15:03:56 +0200 Subject: [PATCH] Paths, .field, hoisting, and (std hashset) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - (.field value), and get, try-get, set and update for reading and copying along a path of fields and keys, with ref-set in the generic operations. - please-hoist-i-promise-i-am-not-naughty, in the macros section. - (std set) is (std hashset), and the collector is hash-setting. 🤖 Generated with [ECA](https://eca.dev) (anthropic/claude-opus-4-5) Co-Authored-By: eca-agent --- _01.Language-basics.sz | 63 +++++++++++++++++++++++++++++++++++----- _02_.Writing-programs.sz | 31 ++++++++++++++++++-- _04.Appendices.sz | 2 +- 3 files changed, 85 insertions(+), 11 deletions(-) diff --git a/_01.Language-basics.sz b/_01.Language-basics.sz index 2d1aa99..121a601 100644 --- a/_01.Language-basics.sz +++ b/_01.Language-basics.sz @@ -559,6 +559,52 @@ Records are updated by copying, with @code{record-set}: (record-ref ada tricks) ;; still 0 }| +A field can also be read with @code{(.field value)}, which is the same as @code{record-ref}. On its own, @code{.field} is a function, so it can be passed around: + +@codeblock[#:lang "bjolang"]|{ +(.tricks ada) ;; 0 +(list-map .tricks (list ada (won-trick ada))) ;; '(0 1) +}| + +@subsubsection{Reading and changing nested data} + +Data is often a record holding a map holding a vec of records. Reading something deep inside it, or making a copy with one thing changed deep inside it, is a lot of @code{record-ref} and @code{record-set} by hand. The prelude has four macros for it instead. Each takes a value and a @em{path}: a list of steps, where a step written @code{.field} is a field, and any other step is a key into a map or an index into a vec. + +@codeblock[#:lang "bjolang"]|{ +(type + (: Player (Record (: name string) (: score int))) + (: Team (Record (: players (Vec Player)) (: tags (Map string int))))) + +(def team (Team (players [(Player (name "Ada") (score 3)) + (Player (name "Bo") (score 5))]) + (tags #map(("fruit" 1))))) + +(get team .players 1 .name) ;; "Bo" +(try-get team .tags "veg") ;; None +(set team .players 0 .name "Bea") ;; a new Team; team is unchanged +(update team .players 1 .score (fun (s) (+ s 10))) ;; Bo has 15 in the new one +}| + +@read-table{ +| Form | Does | +|---------------------------------------+------------------------------------------------------------------------------| +| @code{(get value step ...)} | reads along the path, and throws if a key is missing | +| @code{(try-get value step ...)} | the same as an @code{Option}: @code{None} at the first key that is missing | +| @code{(set value step ... new)} | a copy of @code{value} with @code{new} at the end of the path | +| @code{(update value step ... f)} | a copy of @code{value} with @code{(f old)} at the end of the path | +} + +A key is read with @code{ref} and written with @code{ref-set}, so the path can go through anything that implements the traits behind them: @code{Refable} to read, and @code{Settable} to write, which @code{Vec} and @code{Map} do. A field step can read a .NET property too, as in @code{(get team .players 0 .name .-Length)}, but only a Bjolang record or struct can be copied with a field changed, so @code{set} and @code{update} refuse one. + +Some details worth knowing: + +@itemlist{ + @item{@code{set} does not read the last key, so setting a key a map does not have adds it. A key that is missing earlier in the path throws, as @code{ref} does.} + @item{@code{try-get} gives @code{None} for a missing map key, but a vec index out of range is still an error, as it is for @code{try-ref}. The keys after a missing one are not evaluated.} + @item{@code{set} and @code{update} evaluate the value, then each key from left to right, then the new value or the function, each of them exactly once.} + @item{These are macros, so a step is a field or a key by how it is written, not by its type: @code{.name} is always a field, and anything else is always a key.} +} + @subsection{Pattern matching} Pattern matching is how you take values apart in bjolang. There is no @code{car}/@code{cdr} style of programming on unions: you say what shape you expect and the compiler binds the parts for you, and checks that you did not forget a shape. @@ -1043,7 +1089,7 @@ Bjolang's collections are immutable by default. Adding to a vec or setting a key | @code{(Vec %a)} | @code{[1 2 3]} | an indexable vector (an RRB tree), cheap everywhere | | @code{(Array %a)} | @code{#[1 2 3]} | a mutable .NET array of fixed length | | @code{(Map %k %v)} | @code{#map(("a" 1) ("b" 2))} | a hash map (a CHAMP trie) | -| @code{(Set %a)} | | a hash set, in @code{(std set)} | +| @code{(HashSet %a)} | | a hash set, in @code{(std hashset)} | | @code{(OrderedMap %k %v)} | | a sorted map (a B-tree), in @code{(std orderedmap)} | | @code{(OrderedSet %a)} | | a sorted set, in @code{(std orderedset)} | } @@ -1146,13 +1192,13 @@ When a map is walked as a collection, for example in a @code{loop} or with @code Sets and the sorted collections are libraries, so they have to be imported. Their functions are named like the map functions, with their own prefix: @codeblock[#:lang "bjolang"]|{ -(import (std set)) +(import (std hashset)) (import (std orderedmap)) -(def s (list->set (list 3 1 4 1 5 9 2 6 5))) -(set-length s) ;; 7 -(set-contains? s 4) ;; #t -(set-contains? (set-add s 7) 7) ;; #t +(def s (list->hashset (list 3 1 4 1 5 9 2 6 5))) +(hashset-length s) ;; 7 +(hashset-contains? s 4) ;; #t +(hashset-contains? (hashset-add s 7) 7) ;; #t (def prices (orderedmap-set (orderedmap-set @@ -1202,6 +1248,7 @@ Many operations are traits, so they work on every collection that implements the | @code{(length c)} @code{(empty? c)} | lists, vecs, arrays, maps, strings | size, and whether it is empty | | @code{(ref c key)} | vecs, arrays, maps, strings | the element at an index or key; throws | | @code{(try-ref c key)} | the same | the element as an @code{Option} | +| @code{(ref-set c key x)} | vecs, maps | a copy with @code{x} at the key | | @code{(add c x)} @code{(add-all c xs)} | vecs, maps, sets | a new collection with more in it | | @code{(add! b x)} @code{(add-all! b xs)} | builders and transients | put things in, in place | | @code{(fold f init c)} | anything you can loop over | a left fold | @@ -1214,7 +1261,7 @@ Many operations are traits, so they work on every collection that implements the (ref [10 20 30] 1) ;; 20 (try-ref #map((:a 1)) :b) ;; None (add #map(("a" 1)) ("b" . 2)) ;; for a map, the element is a pair -(add-all (set-empty) [1 1 2 2 3]) ;; a set of three +(add-all (hashset-empty) [1 1 2 2 3]) ;; a set of three (fold + 0 [1 2 3 4]) ;; 10 (find #(> & 3) (list 1 5 7)) ;; (Some 5) @@ -1347,7 +1394,7 @@ An accumulator names a collector and what to feed it: | @code{(listing x)} | a @code{List}, in order | | @code{(vecing x)} | a @code{Vec} | | @code{(mapping (k . v))} | a @code{Map}, from pairs | -| @code{(setting x)} | a @code{Set}, from @code{(std set)} | +| @code{(hash-setting x)} | a @code{HashSet}, from @code{(std hashset)} | | @code{(stringing c)} | a @code{string}, from characters | | @code{(summing x)} | the sum of @code{x} | | @code{(counting x)} | how many rounds reached it | diff --git a/_02_.Writing-programs.sz b/_02_.Writing-programs.sz index 4664f8c..ad3ab89 100644 --- a/_02_.Writing-programs.sz +++ b/_02_.Writing-programs.sz @@ -97,7 +97,7 @@ A list-shaped module path such as @code{(std random)} refers to a standard libra | Module | What it has | |---------------------------------+------------------------------------------------------| | @code{(std random)} | random numbers, @code{shuffle-vec} | -| @code{(std set)} | sets; @code{(std orderedset)}, @code{(std orderedmap)} | +| @code{(std hashset)} | sets; @code{(std orderedset)}, @code{(std orderedmap)} | | @code{(std ports)} | reading a whole port: @code{port->lines} and friends | | @code{(std fmt)} | text layout | | @code{(std rx)} | regular expressions | @@ -178,7 +178,7 @@ Imports can be wrapped in modifiers to filter which definitions are brought into 0) }| -Modifiers can be nested and evaluate from the inside out. For example, @code{(prefix (except (std set) set-map) "s/")} excludes @code{set-map} and prefixes all remaining names with @code{s/}. +Modifiers can be nested and evaluate from the inside out. For example, @code{(prefix (except (std hashset) hashset-map) "s/")} excludes @code{hashset-map} and prefixes all remaining names with @code{s/}. Note that @code{only} and @code{except} apply exclusively to functions and macros, not types. A module's exported types are always imported, ensuring that signatures referencing those types remain valid and resolvable: @@ -1177,6 +1177,33 @@ carries a list, so this literal cannot be one. Its tags are play, say or pass. This elaboration mechanism only applies in positions where the type checker already knows that a specific union type is expected (which is why @code{script} and @code{more} include explicit type signatures). Standard library modules like @code{(std run)} and @code{(std fmt)} use this exact technique for command pipelines and text layouts. +@subsubsection{Computing a value once} + +A macro often builds a value out of constants, and that value is then built again every time the expansion is evaluated. When that is expensive, wrap it in @code{please-hoist-i-promise-i-am-not-naughty}: the form is evaluated the first time it is reached, and every later evaluation gets that same value. This is what @code{#rx(...)} does, so a regex is compiled once rather than once per match. + +@codeblock[#:lang "bjolang"]|{ +;; (tag-set "a" "b" ...) is a HashSet of the strings, built once. +(def/macro (tag-set form inject compare) + (syntax-match form + ((_ tag ...) + #'(please-hoist-i-promise-i-am-not-naughty + (list->hashset (list ,@tag)))))) +}| + +If computing the value throws, nothing is kept, and the next evaluation tries again. The same form written in two places is one value. + +The long name is a promise about what the compiler cannot check: that the form has no side effects worth repeating, and that it does not depend on things that change from call to call, such as a parameter or an effect handler. Everything it can check, it does, and a form that breaks a rule is a compile error that lists every reason. The form cannot: + +@itemlist{ + @item{refer to a local variable or a function parameter;} + @item{read a @code{def} of the module it is in, or any @code{def/mutable};} + @item{use a type variable of the function around it;} + @item{call a bjoroutine, @code{yield} to a sequence, or leave a @code{with-return};} + @item{be void, or produce something that can be changed in place, such as an array, a box or a mutable collection.} +} + +It may use literals, functions, and the defs of other modules. A macro that puts its caller's code inside the form passes these rules on to its caller, so keep the form to parts the macro built itself. + @subsubsection{Macro limitations} When designing macros, keep the following constraints in mind: diff --git a/_04.Appendices.sz b/_04.Appendices.sz index f8f30ef..d4c38e7 100644 --- a/_04.Appendices.sz +++ b/_04.Appendices.sz @@ -288,7 +288,7 @@ Every compiled program links against a small set of optimized runtime assemblies | @code{Collections} | Persistent @code{(Vec %a)} based on Relaxed Radix Balanced (RRB) trees | | @code{SchemeList} | Persistent singly-linked @code{(List %a)} with @code{Cons} and @code{Nil} | | @code{Map} | Persistent @code{(Map %k %v)} based on Compressed Hash-Array Mapped Prefix-trees (CHAMP) | -| @code{BjoSet} | Persistent hash set @code{(Set %a)} | +| @code{BjoSet} | Persistent hash set @code{(HashSet %a)} | | @code{BjoOrderedMap} | Balanced ordered map @code{(OrderedMap %k %v)} | | @code{BjoOrderedSet} | Balanced ordered set @code{(OrderedSet %a)} | | @code{Bjoml} | Concurrency runtime: lightweight fibers, channels, promises, and CML scheduler |