Collections
The array and map builtins. The first five (range, sum, avg, sort, first/last) cover the common slicing and aggregating cases without a loop. push/pop are immutable helpers: the call returns a new array, the source is untouched. filter/map/reduce take lambdas, Rat's fn >> expr spelled inline at the call site. group_by, count_by, unique, flatten, chunk, and zip handle the shape conversions that come up over and over in real apps. The last group - at/get/set/put/drop plus slice/index_of/shift - are the O(1) indexing and dynamic-key map primitives that make hand-written algorithms (hashmaps, two pointers, queues) ergonomic.
range: generate a sequence
1, 2, or 3-arg forms - plus the a..b shorthand
range(n) is [0, 1, ..., n-1]. range(start, end) is the half-open interval. range(start, end, step) walks with a step (the step can be negative for descending). The two-arg form has a shorthand: a..b is exactly range(a, b), so a loop reads in 2..20. It binds looser than arithmetic, so i..i+k means range(i, i + k).
<p> [range(5)]
<p> [2..6]
<p> [range(0, 10, 3)] [0, 1, 2, 3, 4]
[2, 3, 4, 5]
[0, 3, 6, 9]
sum / avg
Empty array → 0
sum(arr) totals the values; avg(arr) returns the mean. Both expect numeric elements and return 0 on an empty array, so guard upstream if "no data" must read differently from "data sums to zero".
<p> [sum(nums)]
<p> [avg(nums)] sum: 150
avg: 30
sort: stable ascending
Returns a new array; source unchanged
sort(arr) returns a new array sorted in ascending order using the default comparator: numbers numerically, strings lexicographically. It's stable, so equal keys keep their input order. For descending or custom orderings, sort then call reduce with the order you want, or use a small helper.
<p> [sort([3, 1, 4, 1, 5, 9])] [1, 1, 3, 4, 5, 9]
first / last
One-arg form returns the element; two-arg returns N
first(arr) returns the first element (or null on empty); first(arr, n) returns the first n elements as an array. last is symmetric.
<p> [first(items)]
<p> [last(items)]
<p> [first(nums, 2)]
<p> [last(nums, 2)] first(items) = apples
last(items) = figs
first(nums, 2) = [10, 20]
last(nums, 2) = [40, 50]
push / pop
Returns a new array; for in-place use .add() / .pop() on state
push(arr, value) returns a new array with the value appended; pop(arr) returns the last element. Neither mutates the input. For in-place writes on page state, use name.add(v) / name.pop() in a handler instead.
<p> [push(items, 'kiwi')]
<p> [pop(items)] push: [apples, pears, figs, kiwi]
pop: figs
filter: keep matching elements
Lambda predicate; keeps truthy returns
filter(arr, lambda) returns the elements for which the lambda returns truthy. The lambda is one-arg. For index-aware filters, write a custom reduce or use map first to attach indices.
<p> [filter(nums, [x] >> x > 20)] [30, 40, 50]
map: transform each element
Lambda returns the new element
map(arr, lambda) applies the lambda to each element and collects the results into a new array. The lambda can return any value (number, string, object) and the output array adopts whatever shape it returns.
<p> [map(nums, [x] >> x + 1)] [11, 21, 31, 41, 51]
reduce: fold to a single value
Lambda is (accumulator, element); third arg is the seed
reduce(arr, lambda, init) folds the array. The lambda takes the running accumulator and the current element; the third arg seeds the accumulator. Common idiom: build a histogram with reduce(arr, [acc, x] >> set_field(acc, x, get_or(acc, x, 0) + 1), {}), though for the histogram case count_by is shorter.
<p> [reduce(nums, [a, b] >> a + b, 0)] 150
group_by: bucket by key
Returns an object: key → array of matching elements
group_by(arr, lambda) calls the lambda on each element and groups elements with equal lambda results into the same bucket. The result is an object; iterate with in keys(result) if you need the bucket name alongside the bucket.
<p> [group_by(nums, [x] >> x > 25)] {false: [10, 20], true: [30, 40, 50]}
count_by: histogram
Object: key → count
count_by(arr, lambda) is group_by plus len on each bucket. Use it for quick frequency counts: word length distributions, status code tallies, or anything where you want "how many fell into each category".
<p> [count_by(items, [w] >> len(w))] {6: 1, 5: 1, 4: 1}
unique: deduplicate
Preserves first-seen order
unique(arr) removes duplicate elements, keeping the first occurrence of each. Useful for cleaning up tag lists, recipient sets, or any "list of distinct" output.
<p> [unique([1, 2, 2, 3, 1])] [1, 2, 3]
flatten: one level of nesting
arr of arrs → flat arr
flatten(arr) concatenates the inner arrays into one. It strips exactly one level: call it twice for two-deep, or write a recursive helper for arbitrary depth. The shallow form is deliberate, since most real data is one-deep and a generic deep-flatten loses type structure.
<p> [flatten([[1, 2], [3], [4, 5]])] [1, 2, 3, 4, 5]
chunk: fixed-size sub-arrays
Last chunk may be shorter
chunk(arr, n) splits the array into sub-arrays of length n. The last chunk is shorter than n if the array doesn't divide evenly. Useful for paginating, rendering grids row by row, or batching API calls.
<p> [chunk(range(7), 3)] [[0, 1, 2], [3, 4, 5], [6]]
zip: merge two arrays
Result length = shorter input
zip(a, b) pairs up elements by index, producing an array of two-element arrays. If the inputs are different lengths, the longer one is truncated. Pair with map when you need a record shape: map(zip(keys, values), >> {k: pair0, v: pair1}).
<p> [zip([1, 2, 3], ['a', 'b', 'c'])] [[1, a], [2, b], [3, c]]
at / get: read one element
O(1) index; negative counts from the end; out-of-range is null
at(coll, i) reads the i-th element of an array or string directly (constant time, not a scan). A negative index counts from the end, so at(xs, -1) is the last element; an out-of-range index reads null. get(coll, key) is the polymorphic read that also covers object keys - get(m, k) works with a key computed at runtime, which plain dot-access can't.
<p> [at(nums, 1)]
<p> [at(nums, -1)]
<p> [at('hello', 0)]
<p> [get({a: 1, b: 2}, 'b')] at(nums, 1) = 20
at(nums, -1) = 50
at('hello', 0) = h
get(m, 'b') = 2
set / put / drop: write one element
In-place O(1); put also lands object keys; drop deletes one
set(arr, i, v) writes one array element in place and returns the array (a negative index counts from the end; writing past the end errors - a real bug, unlike a forgiving read). put(coll, key, v) is the polymorphic writer: an array index or an object key. drop(m, key) deletes a key from a map, keeping the order of the survivors. Together they make the hashmap real - frequency counts, memoized DP, two-sum in O(n).
Index-assignment sugar. You rarely call put/set by hand: m[k] << v writes a map key and xs[i] << v an array element, with m.field << v for a static key. Compound forms work too - m[k] += 1 (a missing key reads as 0, so the first += lands 1), xs[i]++, and nested paths like grid[r][c] << v. It's pure sugar over get/put/at/set - the same in a function body, a service method, or an on_click handler.
<p> [set([1, 2, 3], 0, 9)]
<p> [put({a: 1}, 'b', 2)]
<p> [drop({a: 1, b: 2}, 'a')] set([1,2,3], 0, 9) = [9, 2, 3]
put({a:1}, 'b', 2) = {a: 1, b: 2}
drop({a:1,b:2}, 'a') = {b: 2}
slice / index_of / shift
Windowing, search, and the front-of-queue read
slice(coll, a, b) is the half-open window [a, b) over an array or string; b defaults to the end, negative indexes count from the end, and bounds clamp instead of erroring. index_of(coll, item) returns the first position (by value for arrays, substring for strings) or -1. shift(arr) peeks the front element - the queue counterpart to pop; pair it with slice(q, 1) to dequeue the rest.
<p> [slice('hello', 1, 3)]
<p> [slice(nums, -2)]
<p> [index_of(nums, 30)]
<p> [shift(nums)] slice('hello', 1, 3) = el
slice(nums, -2) = [40, 50]
index_of(nums, 30) = 2
shift(nums) = 10
See also Guards and loops · Inspection · JSON