Paths, .field, hoisting, and (std hashset)

- (.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 <git@eca.dev>
This commit is contained in:
Linus Björnstam 2026-10-03 15:03:56 +02:00
parent 1fe22a4dfd
commit 48771a225a
3 changed files with 85 additions and 11 deletions

View file

@ -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 |