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.

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
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

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?)