Collections
Vectors, maps, sets, and string operations.
Vectors
Vectors are Loon's workhorse ordered collection. You write them with the #[...] syntax, and they do the expected work: hold things in order, let you index into them, and grow when you ask.
[let nums #[1 2 3 4 5]]
[get nums 0] ; 1
[len nums] ; 5
[append nums 6] ; #[1 2 3 4 5 6]Notice that append does not change nums in place. It returns a new vector with the extra element. Collections are immutable by default, so you can hand them around with no fear that something elsewhere will alter them behind your back.
Maps
Maps hold key-value pairs, and keywords make natural keys. If you have used objects in JavaScript or dicts in Python, maps will feel familiar, with immutability added to the bargain.
[let user {:name "Ada" :age 30}]
[get user :name] ; "Ada"
[get user :email "none"] ; "none"
[assoc user :age 31] ; updated mapThe two-argument form of get supplies a default when a key is missing. This is Loon's answer to the null check: rather than receive null or undefined, you decide up front what the fallback should be. And assoc returns a new map with the updated key, leaving the original untouched.
Sets
Sets are unordered collections in which every value appears exactly once. That property makes them ideal for membership checks and deduplication.
[let tags #{"fast" "safe" "fun"}]
[contains? tags "safe"] ; true
[insert tags "new"] ; #{...}Like maps and vectors, sets are immutable. insert returns a new set with the added element.
Strings
Strings behave like sequences, so many of the functions you use on vectors and sets work on strings too. Use str to concatenate, len to measure, and contains? to search.
[let s "hello"]
[len s] ; 5
[contains? s "ell"] ; true
[str s " world"] ; "hello world"The consistency is by design. Learn the collection vocabulary once and you carry it everywhere, with no separate string-specific API to memorize.
Common Operations
The trio of map, filter, and fold works across every collection type. If you have written functional code before, these are old friends. If not, they are about to become indispensable.
[map [fn [x] [* x 2]] #[1 2 3]]
; #[2 4 6]
[filter [fn [x] [> x 2]] #[1 2 3 4]]
; #[3 4]
[fold 0 + #[1 2 3 4]]
; 10map transforms every element. filter keeps only the elements that pass a test. fold reduces the whole collection to a single value, accumulating from a starting point. Between them, these three express a surprising amount of logic without a single loop.
Pipe Idioms
To chain several operations together, reach for pipe. It threads a value through a series of transformations from top to bottom, so even an involved data pipeline stays easy to read.
[pipe #[1 2 3 4 5 6 7 8 9 10]
[filter [fn [n] [= 0 [% n 2]]]]
[map [fn [n] [* n n]]]
[take 3]] ; #[4 16 36]Read it top to bottom: start with the numbers 1 through 10, keep only the even ones, square each, then take the first three results. Every line is one clear step.
Tip
Pipe chains read top to bottom. Each step receives the result of the previous one, so the data flows naturally downward.
Mutable Variants
Immutable collections are the default because they are safer and easier to reason about. Still, in a performance-critical loop you sometimes need to build a collection up piece by piece. That is where mut comes in.
[let items [mut #[]]]
[push! items 1]
[push! items 2]
[println items] ; #[1 2]Functions that mutate end in ! so they catch the eye. The convention is deliberate: mutation is fine when you need it, but it should never slip by unseen.
Warning
Mutable collections follow ownership rules. Only one reference can mutate at a time, which prevents data races at compile time.