Collections
Vectors, maps, sets, and string operations.
Vectors
Vectors are Loon's workhorse ordered collection. You create them with the #[...] syntax, and they do exactly what you'd expect: hold things in order, let you index into them, and grow when you need them to.
[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 doesn't modify nums in place. It returns a new vector with the extra element. Collections in Loon are immutable by default, which means you can pass them around without worrying about something else changing them behind your back.
Maps
Maps hold key-value pairs, and keywords make natural keys. If you've used objects in JavaScript or dicts in Python, maps will feel familiar, but with the added benefit of immutability.
[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 provides a default value when a key is missing. This is Loon's answer to null checks: instead of getting back some null or undefined, you decide upfront what the fallback should be. And assoc returns a new map with the updated key, leaving the original untouched.
Sets
Sets are unordered collections where every value appears exactly once. They're perfect 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 gives you a new set with the added element.
Strings
Strings in Loon act like sequences, which means many of the same functions you use on vectors and sets also work on strings. 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"This consistency is intentional. Once you learn the collection vocabulary, you can apply it everywhere without memorizing separate string-specific APIs.
Common Operations
The trio of map, filter, and fold work across all collection types. If you've used functional programming before, these are your old friends. If not, they're about to become your new ones.
[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 down to a single value by accumulating from a starting point. Together, these three can express a surprising amount of logic without a single loop.
Pipe Idioms
When you want to chain several operations together, pipe is the idiomatic way to do it. It threads a value through a series of transformations from top to bottom, making complex data pipelines 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 this top to bottom: start with the numbers 1 through 10, keep only the even ones, square each of them, then take the first three results. Each line is a single, understandable step.
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're safer and easier to reason about. But sometimes, especially in performance-critical loops, you need to build up a collection incrementally. That's where mut comes in.
[let items [mut #[]]]
[push! items 1]
[push! items 2]
[println items] ; #[1 2]Functions that mutate end with ! so they stand out visually. This is a deliberate convention: mutation is fine when you need it, but it should never be invisible.
Mutable collections follow ownership rules. Only one reference can mutate at a time, which prevents data races at compile time.