RatMinimal web framework

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)]
Result

[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)]
Result

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])]
Result

[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)]
Result

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)]
Result

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)]
Result

[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)]
Result

[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)]
Result

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)]
Result

{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))]
Result

{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])]
Result

[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]])]
Result

[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)]
Result

[[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'])]
Result

[[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')]
Result

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')]
Result

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)]
Result

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