Learn · Chapter 5 of 12

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.