Collections
Lists, maps, sets, and the higher-order methods. Most of the data your programs handle lives in one of these.
Lists
A List<T> is an ordered, growable sequence of values of the same type. Literal syntax uses square brackets:
let xs: List<Int> = [1, 2, 3, 4]
let empty: List<Int> = []
The type parameter on the empty literal matters: an empty [] with no context is ambiguous; the annotation tells the compiler what element type to fix.
Methods you reach for most:
fun main(stdio: Stdio)
let xs = [10, 20, 30]
stdio.println("length: ${xs.length()}")
stdio.println("contains 20? ${xs.contains(20)}")
stdio.println("empty? ${xs.is_empty()}")
Indexed access returns an Option rather than raising on out-of-bounds: xs.get(1) returns Some(20), xs.get(99) returns None. The same shape applies to xs.first() and xs.last(). You will not get an "index out of range" exception at runtime; the type system forces you to handle the absent case.
Higher-order: map, filter, fold
The three classic higher-order methods. map applies a function to every element and returns a new list:
fun main(stdio: Stdio)
let xs = [1, 2, 3, 4]
let doubled = xs.map(|n: Int| n * 2)
stdio.println("${doubled}") // [2, 4, 6, 8]
filter keeps elements for which a predicate returns true:
let evens = xs.filter(|n: Int| n % 2 == 0) // [2, 4]
fold reduces the list to a single value, threading an accumulator:
let sum = xs.fold(0, |acc: Int, n: Int| acc + n) // 10
The three compose naturally:
let sum_of_doubled_evens = xs
.filter(|n: Int| n % 2 == 0)
.map(|n: Int| n * 2)
.fold(0, |acc: Int, n: Int| acc + n)
Two more worth knowing: find returns the first element matching a predicate as an Option<T>; find_index returns the index instead.
Maps
A Map<K, V> is an unordered set of key-value pairs. There is no map literal in v1; you create one with new_map() and populate it with insert:
fun main(stdio: Stdio)
var ages: Map<String, Int> = new_map()
ages.insert("Ana", 30)
ages.insert("Bruno", 25)
stdio.println("size: ${ages.size()}")
stdio.println("Ana's age: ${ages.get(\"Ana\")}") // Some(30)
stdio.println("Carla's age: ${ages.get(\"Carla\")}") // None
Same shape as List.get: keys that are not present yield None, not a runtime error. Other methods worth knowing: contains_key, keys, values, pairs, remove.
Sets
A Set<T> is an unordered collection of unique values:
fun main(stdio: Stdio)
var tags: Set<String> = new_set()
tags.insert("red")
tags.insert("blue")
tags.insert("red") // no effect; "red" already in
stdio.println("size: ${tags.size()}") // 2
stdio.println("has blue? ${tags.contains(\"blue\")}") // true
Sets shine when you have to ask "is this in the collection?" often.
Ranges revisited
Chapter 4 introduced ranges as the thing you iterate over in a for. They are also a type, Range<Int>, with their own small method surface:
fun main(stdio: Stdio)
let r = 0..1000
stdio.println("length: ${r.length()}") // 1000
stdio.println("contains 42? ${r.contains(42)}") // true
A range is lazy: building 0..1_000_000_000 does not allocate a billion integers. The list operations (map / filter / fold) are not on Range; convert with .to_list() first when you need them.
Try this. Compute the sum of the squares of the first 100 integers. Two ways: with a for loop accumulating into a var, and with (0..100).to_list().map(...).fold(...). Which one is easier to read?
The two error modes
First: type mismatch in a literal. Capa infers a single element type for a list and rejects elements that do not match:
let mixed = [1, 2, "three"]
error: list literal: elements must have the same type;
got Int and String
1 | let mixed = [1, 2, "three"]
^
If you genuinely want a heterogeneous collection, you need a sum type (chapter 6).
Second: forgetting that indexed access returns Option:
fun main(stdio: Stdio)
let xs = [10, 20, 30]
let n = xs.get(0)
stdio.println("${n + 1}")
error: '+': expected Int + Int or Float + Float or String + String,
got Option<Int> + Int
3 | stdio.println("${n + 1}")
^
xs.get(0) is an Option<Int>, not an Int. To use the value you either pattern-match on Some(n) / None (chapter 6) or apply the ? operator (chapter 7).
Where you are now
You can store and transform collections of data. The natural next step is to define your own types: a struct for "a record with named fields" and a sum type for "one of these tagged variants". That gives you the vocabulary to model anything more interesting than a list of integers.