List[T] — almost always written []T — is an owned, growable sequence. It is
the workhorse collection: the higher-order surface (map, filter, fold,
sorted_by, …) lives on it directly, with no separate iterator type to opt
into.
fn main(): void {
mut names: []string = ["Ada", "Grace"]
names.add("Lin")
for name in names {
println(name)
}
}Construction
A literal infers its element type from its contents. An empty literal needs
context — an annotation, a parameter type, or a later add.
fn main(): void {
mut empty: []int = []
mut sized := List.with_capacity[int](64)
mut fresh := List.new[string](8)
original := [1, 2, 3]
duplicate := List.copy(original)
empty.reserve(16)
sized.add(1)
fresh.add("x")
println("${empty.len()} of ${empty.capacity()}")
println("${sized.len()} ${fresh.len()} ${duplicate.len()}")
}List.new[T](capacity) and List.with_capacity[T](capacity) both create an
empty list with room reserved; the capacity argument never changes the length.
List.copy(other) produces an independent list and never picks a capacity
smaller than the source. reserve(additional) grows the backing storage of an
existing list without changing what len() reports.
Access returns an Option
Indexing is bounds-checked, and the result is an Option — xs[0] and
xs.get(0) are the same expression with the same type.
fn main(): void {
xs := [10, 20, 30]
head: int? = xs.get(0)
same: int? = xs[0]
past_end: int? = xs[99]
println("${head ?? -1} ${same ?? -1} ${past_end ?? -1}")
}Treating the result as a bare value is a type error:
fn first(xs: []int): int {
return xs[0]
}Discharge it with ??, unwrap_or, or a match, depending on how much the
absent case deserves to be said out loud:
fn total_or_zero(xs: []int): int {
return xs.get(0) ?? 0
}
fn label(xs: []string): string {
match xs.first() {
Some(name) => return "first is ${name}"
None => return "empty"
}
}
fn main(): void {
println("${total_or_zero([])} ${total_or_zero([7])}")
println(label([]))
println(label(["Ada"]))
}| Method | Out-of-range behavior |
|---|---|
get(index), xs[index] |
None |
first(), last() |
None on an empty list |
set(index, value) |
no operation — never extends the list |
get_unchecked(index) |
caller must prove the index is valid |
Index arguments use the Index alias declared in stubs/index.at, which is
u32. ByteIndex and CharIndex are the same alias under different names, on
[]byte and string respectively. An int index therefore needs .to_u32(),
and mixing an int into a range passed to slice is a type error.
fn head_unchecked(xs: []int): int {
if xs.is_empty() { return 0 }
return xs.get_unchecked(0u32)
}
fn at(xs: []int, position: int): int {
return xs.get(position.to_u32()) ?? -1
}
fn main(): void {
println("${head_unchecked([])} ${head_unchecked([5, 6])}")
println("${at([5, 6], 1)} ${at([5, 6], 9)}")
}Inspection methods are size, len (aliases), capacity, is_empty, and
is_not_empty. Mutation is add, set, reserve, and clear. There is no
push, pop, insert, or remove_at.
fn main(): void {
mut xs := [1, 2, 3]
xs.add(4)
xs.set(0u32, 10)
xs.set(99u32, 0) // out of range: no-op, list unchanged
println("${xs.len()} ${xs[0] ?? 0} ${xs.is_not_empty()}")
xs.clear() // length 0, capacity retained
println("${xs.len()} of ${xs.capacity()}")
}Membership uses contains, index_of, and last_index_of; the two position
methods return Index?.
fn position(names: []string, wanted: string): int {
match names.index_of(wanted) {
Some(i) => return i.to_int()
None => return -1
}
}
fn main(): void {
names := ["ada", "lin", "grace", "ada"]
println("${position(names, "lin")} ${position(names, "zoe")}")
println("${names.contains("ada")}")
match names.last_index_of("ada") {
Some(i) => println("last ada at ${i.to_int()}")
None => println("no ada")
}
}Mapping and filtering
map produces a list of whatever the closure returns; filter keeps the
elements the predicate accepts. Neither touches the receiver.
struct User { id: int, name: string, active: bool }
fn active_names(users: []User): []string {
return users
.filter(u => u.active)
.map(u => u.name)
}
fn main(): void {
users := [
User { id: 1, name: "ada", active: true },
User { id: 2, name: "lin", active: false },
User { id: 3, name: "grace", active: true },
]
for name in active_names(users) {
println(name)
}
}flat_map maps each element to a list and concatenates the results, which is
how you flatten a one-to-many relationship in a single pass. With the identity
closure it is also the substitute for flatten(), whose declared signature
(fn flatten(self): []T) cannot express the flattening it names.
struct Order { id: int, items: []string }
fn every_item(orders: []Order): []string {
return orders.flat_map(o => o.items)
}
fn merge(pages: [][]int): []int {
return pages.flat_map(page => page)
}
fn main(): void {
orders := [
Order { id: 1, items: ["bolt", "nut"] },
Order { id: 2, items: ["nut", "washer"] },
]
println("${every_item(orders).len()}")
println("${every_item(orders).distinct().sorted().len()}")
println("${merge([[1, 2], [3]]).len()}")
}Closures accept either the arrow form or a brace block, which reads better for a multi-line body:
fn expensive(prices: []float): []float {
return prices.filter { p =>
adjusted := p * 1.2
adjusted > 100.0
}
}
fn main(): void {
println("${expensive([50.0, 100.0, 200.0]).len()}")
}Pipelines
Chaining is the point. A filter narrows, a sort orders, a window bounds, and a map renders — each step an ordinary list, each step readable on its own line.
struct User { name: string, active: bool, score: int }
fn join(parts: []string, separator: string): string {
mut out := ""
mut first := true
for part in parts {
if !first { out.append_string(separator) }
out.append_string(part)
first = false
}
return out
}
fn leaderboard(users: []User, top: int): string {
return join(
users
.filter(u => u.active)
.sorted_by_descending(u => u.score)
.take(top)
.map(u => "${u.name} (${u.score})"),
", ",
)
}
fn main(): void {
users := [
User { name: "ada", active: true, score: 93 },
User { name: "lin", active: false, score: 99 },
User { name: "grace", active: true, score: 97 },
]
println(leaderboard(users, 2)) // grace (97), ada (93)
}That join helper appears throughout this page. join and join_of are
declared on List but do not lower, so the separator bookkeeping is written out
once and reused.
Folding and scanning
fold and reduce are the same operation under two names: both take an
explicit initial accumulator and thread it left to right.
fn total(values: []int): int {
return values.fold(0, (acc, v) => acc + v)
}
fn longest(names: []string): string {
return names.reduce("", (best, name) => if name.length() > best.length() { name } else { best })
}
fn main(): void {
println("${total([1, 2, 3])} ${longest(["a", "abc", "ab"])}")
}A fold is not limited to scalars — it is the general way to build any summary value in one pass:
struct Stats { count: int, sum: float, max: float }
fn summarize(samples: []float): Stats {
return samples.fold(
Stats { count: 0, sum: 0.0, max: 0.0 },
(s, v) => Stats { count: s.count + 1, sum: s.sum + v, max: s.max.max(v) },
)
}
fn main(): void {
s := summarize([1.5, 4.0, 2.25])
println("${s.count} ${s.sum} ${s.max}")
}scan returns every intermediate accumulator, with the initial value as its
first element — so a scan of n elements has n + 1 results.
fn running_balance(deltas: []int): []int {
return deltas.scan(0, (balance, d) => balance + d)
}
fn main(): void {
for v in running_balance([10, -3, 5]) {
println("${v}") // 0, 10, 7, 12
}
}Predicates and search
any, all, and none short-circuit as soon as the answer is known. find
returns the first matching element as an Option, find_index returns its
position, and count(predicate) is filter(...).len() without the intermediate
list.
struct Ticket { name: string, done: bool, priority: int }
fn next_ticket(tickets: []Ticket): string {
return tickets
.find(t => !t.done)
.map(t => t.name)
?? "nothing left"
}
fn main(): void {
tickets := [
Ticket { name: "deploy", done: false, priority: 9 },
Ticket { name: "write docs", done: true, priority: 3 },
]
println("${tickets.all(t => t.done)}") // false
println("${tickets.any(t => !t.done && t.priority > 8)}") // true
println("${tickets.none(t => t.priority > 100)}") // true
println("${tickets.count(t => !t.done)} open")
println(next_ticket(tickets))
match tickets.find_index(t => !t.done) {
Some(i) => println("blocked at ${i.to_int()}")
None => println("clear")
}
}Enumerating
enumerate() and zip_with_index() both pair each element with its Index. In
a for loop the pair destructures into two bindings; used as a value it stays a
tuple with .0 and .1.
fn numbered(names: []string): []string {
mut out: []string = []
for i, name in names.zip_with_index() {
out.add("${i + 1u32}. ${name}")
}
return out
}
fn main(): void {
for line in numbered(["alpha", "beta"]) {
println(line)
}
for pair in ["alpha", "beta"].enumerate() {
println("${pair.0} -> ${pair.1}")
}
}zip(other) — pairing two different lists positionally — is declared but does
not lower. Walk the shorter length by index:
fn scoreboard(names: []string, scores: []int): []string {
mut out: []string = []
n := if names.len() < scores.len() { names.len() } else { scores.len() }
mut i := 0
for i < n {
name := names.get(i.to_u32()) ?? ""
score := scores.get(i.to_u32()) ?? 0
out.add("${name}: ${score}")
i = i + 1
}
return out
}
fn main(): void {
for row in scoreboard(["ada", "lin", "grace"], [93, 88]) {
println(row) // stops after two rows
}
}Aggregates
| Method | Result | Requirement |
|---|---|---|
sum() |
T |
T: Numeric |
sum_of(f) |
R |
R: Numeric |
average() |
float |
T: Numeric |
min(), max() |
T? |
T: Comparable[T] |
min_by(f), max_by(f) |
T? |
key K: Comparable[K] |
min_of(f), max_of(f) |
K? |
key K: Comparable[K] |
min and max are optional because an empty list has neither. The _by
variants return the element with the extreme key; the _of variants return
the key itself.
struct Product { name: string, price: float, stock: int }
fn inventory_report(products: []Product): string {
total_value := products.fold(0.0, (acc, p) => acc + p.price * p.stock.to_float())
mut cheapest := "n/a"
match products.min_by(p => p.price) {
Some(p) => cheapest = p.name
None => {}
}
highest := products.max_of(p => p.price) ?? 0.0
return "value=${total_value} cheapest=${cheapest} max=${highest}"
}
fn main(): void {
products := [
Product { name: "bolt", price: 0.25, stock: 400 },
Product { name: "gear", price: 12.5, stock: 12 },
]
println(inventory_report(products))
println("${products.count(p => p.stock > 100)} well-stocked")
println("${products.sum_of(p => p.stock)} units")
}sum_of over an int projection lowers; over a float projection the backend
currently miscompiles it, so use map(f).sum() or a fold for float totals, as
above.
Ordering
sorted, sorted_descending, sorted_by, and sorted_by_descending all
return new lists; the receiver is never reordered. sorted_by takes a key
function, not a comparator.
struct Employee { name: string, dept: string, salary: int }
fn by_salary(staff: []Employee): []Employee {
return staff.sorted_by_descending(e => e.salary)
}
fn main(): void {
staff := [
Employee { name: "lin", dept: "eng", salary: 180 },
Employee { name: "ada", dept: "eng", salary: 210 },
]
for e in by_salary(staff) {
println("${e.name} ${e.salary}")
}
for e in staff.sorted_by(e => e.name) {
println(e.name)
}
}sorted() and sorted_descending() sort elements directly, which needs
Comparable[T]. Numbers and strings already have it; for your own types write
the impl — @derive(Comparable) alone does not satisfy the bound.
struct Version { major: int, minor: int }
impl Comparable[Version] for Version {
fn compare_to(self, other: Version): int {
if self.major != other.major { return self.major - other.major }
return self.minor - other.minor
}
}
fn main(): void {
vs := [
Version { major: 1, minor: 4 },
Version { major: 2, minor: 0 },
Version { major: 1, minor: 12 },
]
for v in vs.sorted() {
println("${v.major}.${v.minor}")
}
match vs.sorted_descending().first() {
Some(v) => println("newest ${v.major}.${v.minor}")
None => println("none")
}
}reversed() returns an iterator rather than a list — walk it with for or
collect it yourself.
fn countdown(xs: []int): []int {
mut out: []int = []
for v in xs.reversed() {
out.add(v)
}
return out
}
fn main(): void {
for v in countdown([1, 2, 3]) {
println("${v}")
}
}Windows
take, take_last, take_while, skip, skip_last, and skip_while all
return owned lists and clamp rather than failing when n exceeds the length.
fn main(): void {
scores := [4, 9, 1, 7]
println("${scores.sorted_descending().take(2).len()}") // 2
println("${scores.take_last(2).len()}") // 2
println("${scores.skip(1).len()}") // 3
println("${scores.skip_last(1).len()}") // 3
println("${scores.take_while(v => v > 0).len()}") // 4
println("${scores.skip_while(v => v > 2).len()}") // 3
println("${scores.take(99).len()}") // 4 — clamped
lines := ["# header", "# more", "body"]
for l in lines.skip_while(l => l.starts_with("#")) {
println(l)
}
}chunked(size) and windowed(size, step) are declared but do not lower. Both
are short loops:
fn chunked[T](xs: []T, size: int): [][]T {
mut out: [][]T = []
if size <= 0 { return out }
mut batch: []T = []
for v in xs {
batch.add(v)
if batch.len() == size {
out.add(batch)
batch = []
}
}
if batch.is_not_empty() { out.add(batch) }
return out
}
fn moving_average(samples: []float, width: int): []float {
mut out: []float = []
if width <= 0 { return out }
mut start := 0
for start + width <= samples.len() {
mut sum := 0.0
mut i := start
for i < start + width {
sum = sum + (samples.get(i.to_u32()) ?? 0.0)
i = i + 1
}
out.add(sum / width.to_float())
start = start + 1
}
return out
}
fn main(): void {
for batch in chunked([1, 2, 3, 4, 5], 2) {
println("${batch.len()}") // 2, 2, 1
}
for v in moving_average([1.0, 2.0, 3.0, 4.0], 2) {
println("${v}") // 1.5, 2.5, 3.5
}
}Slices
slice(range) returns Slice[T]?, a borrowed half-open view over the parent’s
storage. It allocates nothing. Range endpoints are Index, so write them as
u32.
fn middle_sum(xs: []int): int {
match xs.slice(1u32..3u32) {
None => return 0
Some(view) => {
head := view.first() ?? 0
tail := view.last() ?? 0
return head + tail + view.len()
}
}
}
fn owned_copy(xs: []int): []int {
match xs.slice(0u32..2u32) {
None => return []
Some(view) => return view.to_list()
}
}
fn main(): void {
println("${middle_sum([1, 2, 3, 4])}")
println("${middle_sum([1])}") // 0 — range past the end
println("${owned_copy([1, 2, 3]).len()}")
}slice returns None when start > end or the end lies past the length. A
Slice[T] supports get, get_unchecked, first, last, set,
set_unchecked, size/len, is_empty, nested slice, and to_list() for
an owned copy. It cannot grow.
Grouping and indexing
| Operation | Result | Status |
|---|---|---|
partition(predicate) |
([]T, []T) — matching, then non-matching |
lowers |
associate_by(key) |
Map[K, T] keyed by each element; last duplicate wins |
lowers |
distinct() |
keeps the first occurrence | lowers |
to_set() |
Set[T] |
lowers |
group_by(key) |
Map[K, []T] |
declared, no lowering path |
associate(pair) |
Map[K, V] from returned (key, value) tuples |
declared, no lowering path |
distinct_by(key) |
keeps the first occurrence per key | declared, no lowering path |
partition returns a tuple that destructures into two bindings:
struct Record { id: int, valid: bool }
fn split(records: []Record): string {
good, bad := records.partition(r => r.valid)
return "${good.len()} accepted, ${bad.len()} rejected"
}
fn main(): void {
println(split([
Record { id: 1, valid: true },
Record { id: 2, valid: false },
Record { id: 3, valid: true },
]))
}associate_by builds an index for repeated lookup. associate — an arbitrary
key/value map from one pass — does not lower, so write the loop:
struct Node { id: int, label: string }
fn labels(nodes: []Node): Map[int, string] {
mut out: Map[int, string] = Map.new()
for n in nodes {
out.put(n.id, n.label)
}
return out
}
fn main(): void {
nodes := [Node { id: 7, label: "root" }, Node { id: 9, label: "leaf" }]
println(labels(nodes).get(7) ?? "missing")
match nodes.associate_by(n => n.id).get(9) {
Some(n) => println(n.label)
None => println("missing")
}
}group_by does not lower either. When the group keys are few and the list is
small, filtering per distinct key is the clearest substitute:
struct Sale { region: string, amount: float }
fn regional_totals(sales: []Sale): []string {
mut out: []string = []
for region in sales.map(s => s.region).distinct().sorted() {
group := sales.filter(s => s.region == region)
total := group.map(s => s.amount).sum()
out.add("${region}: ${total} over ${group.len()} sales")
}
return out
}
fn main(): void {
sales := [
Sale { region: "emea", amount: 120.0 },
Sale { region: "amer", amount: 80.0 },
Sale { region: "emea", amount: 45.5 },
]
for line in regional_totals(sales) {
println(line)
}
}For a large list, accumulate into a Map in one pass instead — the same shape
group_by would have produced:
struct Sale { region: string, cents: int }
fn group_by_region(sales: []Sale): Map[string, []Sale] {
mut grouped: Map[string, []Sale] = Map.new()
for s in sales {
mut bucket := grouped.get(s.region) ?? []
bucket.add(s)
grouped.put(s.region, bucket)
}
return grouped
}
fn main(): void {
sales := [
Sale { region: "emea", cents: 12000 },
Sale { region: "amer", cents: 8000 },
Sale { region: "emea", cents: 4550 },
]
for region, group in group_by_region(sales) {
println("${region}: ${group.sum_of(s => s.cents)} over ${group.len()}")
}
}Combining lists
plus, plus_all, minus, intersect, union, and subtract all return
new lists and leave both operands alone. The set-like three require elements
with equality and hashing.
fn main(): void {
a := [1, 2, 3]
b := [3, 4]
println("${a.plus(99).len()}") // 4
println("${a.plus_all(b).len()}") // 5 — duplicates kept
println("${a.minus(1).len()}") // 2
println("${a.intersect(b).len()}") // 1
println("${a.union(b).len()}") // 4 — deduplicated
println("${a.subtract(b).len()}") // 2
println("${[3, 1, 3, 2].to_set().len()}") // 3 — the bridge to Set
}union deduplicates; plus_all does not. minus(item) removes elements equal
to item.
Mutating in place
for mut v in list writes each rebound value back into the list, which avoids
building a second list when the transformation is the point.
fn scale(mut values: []int, factor: int): []int {
for mut v in values {
v = v * factor
}
return values
}
fn main(): void {
mut values := [1, 2, 3]
for v in scale(values, 10) {
println("${v}")
}
}A mut parameter needs a mutable place at the call site — passing a literal
directly is rejected. There is no index-based remove or insert, so build a
new list with filter when elements have to go:
fn drop_blanks(lines: []string): []string {
return lines.filter(l => l.trim().is_not_empty())
}
fn main(): void {
for l in drop_blanks(["a", " ", "b"]) {
println(l)
}
}A worked example
An order-analysis pipeline: parse, validate, aggregate per key, sort, and render, with every intermediate an ordinary list. Money is held as integer cents, per the advice in Numbers.
struct Order { id: int, customer: string, region: string, cents: int }
error ReportError { NoOrders }
fn join(parts: []string, separator: string): string {
mut out := ""
mut first := true
for part in parts {
if !first { out.append_string(separator) }
out.append_string(part)
first = false
}
return out
}
fn parse(rows: []string): []Order {
mut orders: []Order = []
for i, row in rows.zip_with_index() {
fields := row.split(",").map(f => f.trim())
if fields.len() != 3 { continue }
match (fields.get(2u32) ?? "").to_int() {
None => continue
Some(cents) => orders.add(Order {
id: i.to_int(),
customer: fields.get(0u32) ?? "",
region: fields.get(1u32) ?? "",
cents: cents,
})
}
}
return orders
}
fn top_customers(orders: []Order, n: int): []string {
return orders
.map(o => o.customer)
.distinct()
.map(name => (name, orders.filter(o => o.customer == name).sum_of(o => o.cents)))
.sorted_by_descending(pair => pair.1)
.take(n)
.map(pair => "${pair.0}: ${pair.1} cents")
}
fn regional_share(orders: []Order): []string ! ReportError {
if orders.is_empty() { error NoOrders }
grand_total := orders.sum_of(o => o.cents)
mut out: []string = []
for region in orders.map(o => o.region).distinct().sorted() {
region_total := orders.filter(o => o.region == region).sum_of(o => o.cents)
out.add("${region}: ${100 * region_total / grand_total}%")
}
return out
}
fn main(): void {
rows := [
"ada, emea, 12000",
"lin, amer, 8050",
"ada, emea, 4525",
"malformed row",
]
orders := parse(rows)
println(join(top_customers(orders, 2), "\n"))
match regional_share(orders) {
Ok(lines) => println(join(lines, "\n"))
Err(e) => println("no orders")
}
match regional_share([]) {
Ok(lines) => println(join(lines, "\n"))
Err(e) => println("no orders")
}
}Availability
These List signatures are installed for semantic checking but have no lowering
path in the wasm backend today. atoll check accepts a call to any of them;
atoll build fails with
ATOLL2004: builtin method ... has no lowering path.
| Signature | Working substitute |
|---|---|
join(sep), join_of(sep, f) |
the join loop used throughout this page |
group_by(key) |
filter per distinct key, or accumulate into a Map |
associate(f) |
for loop with Map.put |
distinct_by(key) |
for loop with a Set of seen keys |
chunked(size) |
the chunked loop shown above |
windowed(size, step) |
index-walking loop, as in moving_average |
zip(other) |
index-walking loop, as in scoreboard |
flatten() |
flat_map(x => x) — the declared signature fn flatten(self): []T cannot express flattening anyway |
shuffled() |
no substitute yet — no RNG in the prelude |
to_string(), hash_code() on a list of non-scalar elements |
build the text with a join loop |
Two further shapes type-check but hit backend bugs rather than a missing lowering, so avoid them for now:
sum_of(f)wherefreturns afloat— usemap(f).sum()orfold.average()on a[]float— divide asum()bylen().to_float().opt.map(f)wherefreturns anint, followed by??— usematch.
Everything else on list.at — add, set, clear, reserve, get,
first, last, map, filter, flat_map, fold, reduce, scan, any,
all, none, find, find_index, count, enumerate, zip_with_index,
sum, sum_of, average, min, max, min_by, max_by, min_of,
max_of, sorted*, reversed, take*, skip*, slice, partition,
associate_by, distinct, intersect, union, subtract, plus,
plus_all, minus, to_set — compiles.
Related pages
Iteration covers the Iterator and
Iterable traits behind for, and Sequences
covers the []T / [..]T / [N]T type syntax.