Skip to content

The book · 05

Collections and strings

book.md · 92 lines · 2 min read

Goal: use List/Map/Set and the functional layer, and understand the string rules (explicit units, interpolation, byte comparison).

List, Map, Set (the collections package, tier 0)

Section titled “List, Map, Set (the collections package, tier 0)”

They are not builtin: you import them. List[T] is a contiguous array that grows (not a linked list):

use collections
var xs := List[int].new()
xs.push(1); xs.push(2); xs.push(3)
io.println("{{xs.len()}} items")
primeiro := xs.get(0) // Optional[int] (safe access; none out of bounds)
@requires(i < xs.len())
fn at_seguro(xs: List[int], i: usize) -> int { return xs.at(i) } // contracted 'at'

Access is by method (xs.at(i)/xs.get(i)), not xs[i]: raw [i] is only for builtin []T/[N]T/[*]T. get -> Optional (“maybe”), at -> T (“I guarantee it”, under contract).

Map[K, V] is a hash table; the key must be hashable, a property derived by reflection (structs work without declaring anything):

m := Map[string, int].new()
m.put("ana", 90)
score := m.get("ana") // Optional[int]
m.update_or_insert("ana", fn(cur) => cur.or(0) + 1) // entry API, anti double-lookup

Set[T] is a hash set (add/contains/union/intersection). Specialized structures (Deque, PriorityQueue, SortedMap, Trie, RingBuffer) live in the official containers lib (tier 1), in chapter 12.

map/filter/fold/collect via UFCS; collect materializes into the container you choose:

pares := xs.filter(fn(x) => x % 2 == 0)
dobr := xs.map(fn(x) => x * 2)
soma := xs.fold(0, fn(acc, x) => acc + x)
como_set: Set[int] = primos.collect() // same iterator, output container chosen by the type

string is always valid UTF-8. It has three views (.bytes, .codepoints, .chars, the last of which is graphemes), and s[i] is forbidden (the unit is ambiguous). You qualify it:

s := "café"
b := s.bytes.len // number of bytes (the view's .len field)
n := s.chars.len // number of graphemes
c0 := s.char_at(0) // one grapheme
view := s.chars[1..3] // '..' = O(1) view; ':' = O(n) copy

Interpolation uses {{ }}, through the Display interface (interpolating a type without Display is a compile error, none of that <Object@0x...>):

io.println("{{nome}} placed {{count}} orders, total {{total.fixed(2)}}")

Comparison is byte-by-byte: ==/!= (byte equality), </> (lexicographic order, which feeds a sort directly). Unicode semantics (normalization, collation) is a method, not an operator, and there is no ===:

a == b // bytes (the common, fast case)
a.equals(b, .semantic) // Unicode normalization (pulls in 'unicode' tables, opt-in)

Growing a string follows the usual mutability rule: var mutates, by doubling, with no separate StringBuilder (use string.with_capacity(n) when you know the size):

var buf := string.with_capacity(256)
buf.append("a"); buf.append("b")

Building from bytes: string.from[u8](bytes) -> Result[string, error{InvalidUtf8}] (validates at the boundary).

Next: 06 · Processes and channels