Skip to content

Collections

Candela has two built-in collections, lists and maps, plus a set built on top of maps in the standard library. All three are passed by reference: handing one to a function lets that function change it.

Lists

A list is written in square brackets and holds elements of a single type.

fn main() {
    let numbers = [1, 2, 3];
    let words = ["alpha", "beta"];
    let empty = [];
    print(numbers, words, empty.len());
}

Mixing types in one list is a compile error: [1, "a"] does not compile.

The empty list

[] names no element type. A local that starts empty takes its element type from the first push.

fn main() {
    let xs = [];
    xs.push(1);
    xs.push(2);
    print(xs, xs.len());
}

Where a declaration says what the elements are, that declaration decides them. A parameter annotated T[] compiles the function body with elements of T however the call site writes the list, and a -> T[] return annotation hands the caller elements of T. So a function that reads its elements still works when it is called with nothing in the list:

enum Cell { Num(int), Text(string) }

fn width(cells: Cell[]) -> int {
    let w = 0;
    for c in cells {
        match c {
            Cell::Num(n) => { w = w + 1; }
            Cell::Text(t) => { w = w + t.len(); }
        }
    }
    return w;
}

fn main() {
    print(width([]), width([Cell::Text("ab"), Cell::Num(1)]));
}

A list that does name an element type is still checked against the annotation, so width([1, 2]) does not compile. A let takes no annotation, so a local that starts empty and is never pushed to keeps elements of no type. Handing such a local to a parameter that declares its elements pins it as well, so what the function pushes into it reads back at that type.

Indexing and slicing

Index from zero with xs[i]. A slice xs[start..end] returns a new list from start up to but not including end, and xs[..end] starts at the beginning. An index past the end raises at runtime.

fn main() {
    let xs = [10, 20, 30, 40];
    print(xs[0], xs[3]);
    print(xs[1..3], xs[..2]);
    xs[0] = 99;
    print(xs);
}

Strings index and slice the same way, returning strings.

Growing and reordering

fn main() {
    let xs = [3, 1, 2];
    xs.push(4);
    xs.sort();
    print(xs);
    xs.reverse();
    print(xs);
    xs.remove(0);
    print(xs);
}

push, sort, reverse, and remove change the list in place. + concatenates two lists into a new one.

fn main() {
    print([1, 2] + [3]);
}

Inspecting

fn main() {
    let xs = [10, 20, 30];
    print(xs.len(), xs.contains(20), xs.find(30));
    print(["a", "b"].join(", "));
    print([1, 0, 2, 0, 3].partition(0));
}

find returns the index of a value, or -1 when it is absent. join concatenates a list of strings, with an optional separator. partition splits a list on a separator element.

Iterating

fn main() {
    let xs = [1, 2, 3];
    for x in xs {
        print(x);
    }
    for i in 0..xs.len() {
        print(xs[i]);
    }
}

Higher-order operations

Lists carry the standard library's list helpers as methods, with no import needed.

fn double(x) {
    return x * 2;
}

fn main() {
    let xs = [1, 2, 3, 4];
    print(xs.map(double));
    print(xs.filter(fn(x) { return x % 2 == 0; }));
    print(xs.reduce(0, fn(a, b) { return a + b; }));
    print(xs.sum(), xs.min(), xs.max(), xs.first(), xs.last());
    print(xs.take(2), xs.drop(2), xs.unique(), xs.chunk(2));
    print(xs.any(fn(x) { return x > 3; }), xs.all(fn(x) { return x > 0; }));
}

The functions you pass read the variables around them, so a predicate can test against what the scope holds; see Functions.

Maps

A map is written in braces as key: value pairs. Keys share one type and values share one type. {} is the empty map.

fn main() {
    let ages = {"ada": 36, "alan": 41};
    let by_number = {1: "one", 2: "two"};
    let empty = {};
    print(ages.len(), by_number.get(1), empty.len());
}

Repeating a key in a literal is a compile error.

The empty map

{} names neither a key type nor a value type, the same way [] names no element type. Where a declaration says what the map holds, that declaration decides it: a parameter annotated {K: V} compiles the function body with keys of K and values of V however the call site writes the map, and a -> {K: V} return annotation hands the caller the same. So a function that matches on what a key holds still works when it is called with an empty map:

enum Cell { Num(int), Text(string) }

fn width(cells: {string: Cell}) -> int {
    let w = 0;
    for name in cells {
        match cells.get(name) {
            Cell::Num(n) => { w = w + 1; }
            Cell::Text(t) => { w = w + t.len(); }
        }
    }
    return w;
}

fn main() {
    print(width({}), width({"a": Cell::Text("ab")}));
}

A map that does name its types is still checked against the annotation, so width({"a": 1}) does not compile. A let takes no annotation, so a local that starts empty and is never inserted into keeps keys and values of no type. Handing such a local to a parameter that declares what the map holds pins it as well, so what the function inserts reads back at that type.

Reading and writing

fn main() {
    let scores = {"a": 1};
    scores.insert("b", 2);
    scores.insert("a", 10);
    print(scores.get("a"), scores.len());
    print(scores.contains("b"), scores.keys(), scores.values());
    scores.remove("b");
    print(scores.len());
}

insert adds a pair or replaces the value of an existing key. remove takes the entry under a key back out, and a key the map does not hold leaves it as it was. get raises when the key is absent, so test with contains first, or use the get_or method from the standard library's map module to supply a fallback.

Iterating

Iterating a map walks its keys in the order they went in, and a literal's keys go in as written. keys, values, and printing read the same order. Inserting a key that is already there replaces the value and leaves the entry where it is; removing a key and inserting it again puts it at the end. Two maps holding the same entries are equal whatever order they were built in.

fn main() {
    let scores = {"a": 1, "b": 2};
    let total = 0;
    for key in scores {
        total += scores.get(key);
    }
    print(total);
}

Sets

A set holds each value at most once. It comes from the set module as Set<T>, a struct built out of a map. Name the member type when you make one.

import "std/set" as set;

fn main() {
    let s = set::new<int>();
    s.add(1);
    s.add(2);
    s.add(2);
    print(s.len(), s.contains(2), s.members());
}

|, &, -, and ^^ are union, intersection, difference, and symmetric difference; each combines two sets into a new one, and each has a named method too.

import "std/set" as set;

fn main() {
    let a = set::new<int>();
    a.add(1);
    a.add(2);
    let b = set::new<int>();
    b.add(2);
    b.add(3);
    print((a | b).members());
    print(a.intersection(b).members());
}

A set is a struct, so it is not iterable itself; members gives you a list to iterate, in the order the members were added. a == b compares the members and ignores that order. The module is covered in full in set.