Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Collections & Lambdas

Range operator ..

start..end produces an inclusive list[int] containing every integer from start to end.

digits = 1..5        # [1, 2, 3, 4, 5]
single = 4..4        # [4]
empty  = 5..3        # []  (start > end → empty)

Both operands must be int — using a float or str is a type error.

Ranges work directly with for:

for i in 0..2 {
  io.notify("{i}")   # 0, 1, 2
}

Because the result is a plain list[int], all list methods apply:

count  = (1..10).count()                  # 10
evens  = (1..10).filter(n => n % 2 == 0) # [2, 4, 6, 8, 10]

Lists

nums = [1, 2, 3, 4, 5]
names = ["alice", "bob", "charlie"]
empty = []

List concatenation and adding elements

Concatenate lists with + or add a single element with push:

base = ["a", "b"]
extra = ["c", "d"]
all = base + extra              # ["a", "b", "c", "d"]

more = all.push("e")            # ["a", "b", "c", "d", "e"]

push returns a new list — it does not mutate in place. Reassign if needed: items = items.push(x).

Collection methods

All methods accept lambdas (x => expr) or task references.

Lambdas don’t capture outer variables. A lambda body can only see its own parameters and global names (tasks, agents, self inside an agent). It cannot read a local declared outside it — n = 10; xs.map(x => x + n) is a check error. Pass the value in another way instead, e.g. as a second parameter via a named task: xs.map(add_n) where add_n(x: int) -> int closes over nothing and takes everything it needs as arguments. See SPEC §7.

map — transform each element

doubled = [1, 2, 3].map(n => n * 2)         # [2, 4, 6]
names = contacts.map(c => c.name)            # extract names

filter — keep matching elements

big = [1, 5, 10, 20].filter(n => n > 5)     # [10, 20]
urgent = emails.filter(e => e.urgency == high)

find — first matching element

found = items.find(n => n > 15)              # first match or none
admin = users.find(u => u.role == "admin") ?? default_user

any / all — boolean checks

has_urgent = emails.any(e => e.urgency == critical)    # true/false
all_done = tasks.all(t => t.status == "complete")

reduce — fold to a single value

The first argument is the combining function (accumulator, element) => ...; the second is the initial accumulator value.

total = [1, 2, 3, 4, 5].reduce((acc, x) => acc + x, 0)   # 15
joined = words.reduce((acc, w) => acc + " " + w, "")

sum / min / max — numeric aggregation

prices = [9.99, 4.50, 14.00]
prices.sum()   # 28.49
prices.min()   # 4.50
prices.max()   # 14.00

min() and max() return none on an empty list. sum() requires a numeric list; calling it on non-numeric elements is a runtime error.

join — concatenate to string

["a", "b", "c"].join(", ")   # "a, b, c"
tags.join(" | ")

sort — natural ordering

Returns a new sorted list. Integers, floats, and strings are compared by value. Structs with impl Comparable are sorted by their compare method.

[3, 1, 4, 1, 5].sort()            # [1, 1, 3, 4, 5]
["cherry", "apple", "banana"].sort()  # ["apple", "banana", "cherry"]

sort(by:) — sort by key function

Pass an optional by: key function to .sort() — consistent with the min(by:) / max(by:) pattern. The key must return an int, float, or str. Ascending only; negate numeric keys for descending.

type Product { name: str, price: float, rating: float }

# Sort by a single field
by_price  = products.sort(by: p => p.price)          # cheapest first
by_name   = products.sort(by: p => p.name)           # alphabetical

# Descending — negate the key
by_rating = products.sort(by: p => 0.0 - p.rating)  # highest rated first

reverse — flip order

[1, 2, 3].reverse()    # [3, 2, 1]
top3 = scores.sort().reverse().take(3)

flatten — unwrap one level of nesting

[[1, 2], [3], [4, 5]].flatten()   # [1, 2, 3, 4, 5]

zip — pair two lists

Returns a new list where each element is a 2-element tuple [a, b] drawn from the two input lists in order. Stops when the shorter list is exhausted.

names  = ["alice", "bob", "carol"]
scores = [90, 85, 95]

pairs = names.zip(scores)
# → [["alice", 90], ["bob", 85], ["carol", 95]]

for (name, score) in names.zip(scores) {
    log.info("{name} scored {score}")
}

The return type is inferred as list[(T, U)], so the loop variables name and score are fully typed.

take / skip — slice by count

[10, 20, 30, 40, 50].take(3)   # [10, 20, 30]
[10, 20, 30, 40, 50].skip(3)   # [40, 50]

Lambda syntax

# Single parameter
n => n * 2

# Multi-parameter
(a, b) => a + b

# Block body
items.map(item => {
  urgency = triage(item)
  {item: item, urgency: urgency}
})

Task references

Named tasks can be passed directly to collection methods:

task double(n: int) -> int { n * 2 }
task is_even(n: int) -> bool { n % 2 == 0 }

result = [1, 2, 3].map(double)        # [2, 4, 6]
evens = [1, 2, 3, 4].filter(is_even)  # [2, 4]

Map / struct access

info = {name: "Alice", age: 30}
info.name                              # "Alice"
info.age                               # 30

# Nested
team = {lead: {name: "Bob", role: "eng"}}
team.lead.name                         # "Bob"

Maps

A typed map[K, V] literal is built with the same {k: v, ...} form. The same syntax serves both struct construction and map[str, V] construction; the checker resolves which based on the declared type.

stock: map[str, int] = {apples: 12, pears: 5, plums: 0}

Map methods

MethodReturns
map.get(k)V?none if the key is absent
map.keys()list[K]
map.values()list[V]
map.len() / map.count() / map.size()int
map.is_empty()bool
map.contains(k) / map.has(k)bool
map.insert(k, v)map[K, V] — a new map
stock: map[str, int] = {apples: 12, pears: 5}

stock.count()              # 2
stock.contains("apples")   # true
stock.keys()               # ["apples", "pears"]
stock.values()             # [12, 5]

# get returns V? — coalesce or assert before using as V.
pears = stock.get("pears") ?? 0      # 5
missing = stock.get("oranges") ?? 0  # 0

insert — add or overwrite a key

Like list.push, insert returns a new map instead of modifying the receiver, so the result has to be bound:

stock: map[str, int] = {apples: 12}

stock.insert("pears", 5)            # no-op — the result was discarded
stock = stock.insert("pears", 5)    # {apples: 12, pears: 5}
stock = stock.insert("apples", 20)  # overwrites: {apples: 20, pears: 5}

Sets

A set[T] holds at most one element equal to any given value. Duplicates collapse on the way in, whether from the literal or from add:

ids: set[int] = set[1, 2, 2, 3]
ids.count()    # 3 — the second 2 was dropped

Elements keep first-insertion order — that is the order a set prints, iterates, and spreads in. Nothing is sorted.

set[3, 1, 2]   # set[3, 1, 2]

Equality is the same == used everywhere else, so the element type is unrestricted — unlike map keys, which must be str, int, or bool:

type Point { x: int, y: int }

a: Point = {x: 1, y: 2}
b: Point = {x: 1, y: 2}
set[a, b].count()   # 1 — equal field values, one element

Set methods

MethodReturns
set.add(v)set[T] — a new set
set.contains(v)bool
set.len() / set.count() / set.size()int
set.is_empty()bool

add follows the same value-method rule as list.push and map.insert — it returns a new set, and adding an element that is already present is a no-op rather than an error:

ids = set[1, 2]

ids.add(3)          # no-op — the result was discarded
ids = ids.add(3)    # set[1, 2, 3]
ids = ids.add(2)    # set[1, 2, 3] — already present

Sets iterate and spread like lists:

for id in set[3, 1, 2] {
  io.show(id)       # 3, then 1, then 2
}

total(...set[1, 2, 2, 3])   # spreads 3 values, not 4

They also accept the read-only list methods — map, filter, find, any, all, reduce, sum, min, max, join, sort, reverse, take, skip, and friends — each returning a list or a scalar, never a set. push is not among them; use add.

set[1, 2, 2, 3].map(n => n * 2)   # [2, 4, 6] — a list
set[1, 2, 3].join("-")            # "1-2-3"

String methods

s = "  Hello, World!  "
s.trim()                  # "Hello, World!"
s.upper()                 # "  HELLO, WORLD!  "
s.lower()                 # "  hello, world!  "
s.contains("Hello")       # true
s.starts_with("  H")      # true
s.split(", ")             # ["  Hello", "World!  "]
s.replace("World", "Keel") # "  Hello, Keel!  "
"42".to_int()             # 42 (int?)