The Loup Native Book — contents
  1. Chapter 1: Getting started
  2. Chapter 2: Native mode overview
  3. Chapter 3: Variables and numerics
  4. Chapter 4: Functions
  5. Chapter 5: Control flow
  6. Chapter 6: Collections
  7. Chapter 7: Structs
  8. Chapter 8: Enums and pattern matching
  9. Chapter 9: Classes and ARC
  10. Chapter 10: Interfaces
  11. Chapter 11: Generics
  12. Chapter 12: Error handling
  13. Chapter 13: Modules and packages
  14. Chapter 14: Template fences
  15. Chapter 15: Ownership verbs
  16. Chapter 16: borrow, excl borrow and noescape
  17. Chapter 17: unique ownership
  18. Chapter 18: Lifetimes and regions
  19. Chapter 19: @unsafe and calling C
  20. Chapter 20: Concurrency with OS threads
  21. Chapter 21: Async on native
  22. Chapter 22: Desktop UI
  23. Chapter 23: SIMD and GPU
  24. Chapter 24: Building and distributing
  25. Chapter 25: Memory safety patterns

Chapter 6: Collections

Loup has four built-in collection types: list[T], dict[K, V], set[T] and tuples. This chapter covers their everyday operations and the property that surprises people coming from Python, JavaScript or Java: collections are values. Assigning a list or passing it to a function gives the receiver its own copy, exactly as with an int or a struct.

Lists

list[T] is a growable sequence of elements of one type:

@mode(host.native)

func main(): void {
    var queue: list[int] = [10, 20, 30];
    println(queue.length());
    println(queue[0]);
    println(queue[-1]);

    queue.push(40);
    let head = queue.remove(0);
    let tail = queue.pop();
    println(i"removed {$ head $} and {$ tail $}, left {$ queue $}");

    println(queue.contains(20));
    println(queue.indexOf(30));
    println(queue + [50, 60]);
    println(["warn", "error"].join(", "));
}

Listing 6-1: Common list operations

3
10
30
removed 10 and 40, left [20, 30]
true
1
[20, 30, 50, 60]
warn, error

A negative index counts from the end. push appends, pop removes and returns the last element, remove(i) removes and returns the element at index i, and + builds a new list. first() and last() return an element. Other methods include isEmpty(), reverse(), slice(start, end), sort(comparator), and the function-taking map, filter, fold, any and all (chapter 4 notes that native builds do not lower the function-taking ones yet).

sort takes a comparator that returns a negative number, zero, or a positive number:

var latencies = [120, 35, 80];
latencies.sort(func(a: int, b: int) => a - b);
println(latencies);

Listing 6-2: Sorting with a comparator

[35, 80, 120]

Slices

xs[start:end] copies a range of elements into a new list. Either bound may be omitted or negative, and a third part is a step:

let samples = [1, 2, 3, 4, 5, 6, 7, 8];
println(samples[0:3]);
println(samples[-3:]);
println(samples[2:6]);
println(samples[::-1]);

Listing 6-3: Slicing

[1, 2, 3]
[6, 7, 8]
[3, 4, 5, 6]
[8, 7, 6, 5, 4, 3, 2, 1]

Mistake: indexing past the end

Indexing is bounds-checked. An index outside the list throws IndexError; so does first(), last() or pop() on an empty list:

This code throws at run time
@mode(host.native)

func main(): void {
    let readings = [12, 15, 11];
    println(readings[3]);
}
Uncaught IndexError: list index 3 out of range (len 3)
  at main (main.lphc:5:13)

Check the length first (if (i < readings.length())), iterate with for (r in readings) instead of indexing, or catch IndexError where an out-of-range index is an expected input error (chapter 12).

Collections are values

In Python, b = a makes b another name for the same list. In Loup, it makes a copy (ruling COLL-1, Language Reference §1-10.3.1):

let defaults = ["--verbose"];
var flags = defaults;
flags.push("--color");
println(defaults);
println(flags);

Listing 6-4: Assignment copies a list

["--verbose"]
["--verbose", "--color"]

The same holds for dict, set and tuples, and for passing a collection to a function. A parameter without a verb is a read-only view (chapter 4); to change the caller's list, declare the parameter edits and pass edits: xs. Otherwise, copy it into a local and return the result:

func withReviewTag(tags: list[string]): list[string] {
    var result = tags;
    result.push("reviewed");
    return result;
}

func main(): void {
    let original = ["draft"];
    let updated = withReviewTag(original);
    println(original);
    println(updated);
}

Listing 6-5: Returning a changed copy

["draft"]
["draft", "reviewed"]

Value semantics are what make the ownership rules of chapters 15 to 17 work: two bindings never secretly share one mutable list. A collection of class instances copies the references, not the objects: both lists then point to the same objects, which are reference-counted (chapter 9).

Known gap: comparing collections

The specification says == on lists, sets and dictionaries compares their elements (ruling EQ-3). Stage0 1.25.1 does not implement that yet, and the comparison is always false:

This code does not do what it should
let expected = [1, 2, 3];
let actual = [1, 2, 3];
println(expected == actual);
false

Tuples do compare element-wise. Until EQ-3 lands, compare collections yourself:

func sameItems(a: list[int], b: list[int]): bool {
    if (a.length() != b.length()) {
        return false;
    }
    for (i in 0..a.length()) {
        if (a[i] != b[i]) {
            return false;
        }
    }
    return true;
}

func main(): void {
    println(sameItems([1, 2, 3], [1, 2, 3]));
    println((1, "a") == (1, "a"));
}

Listing 6-6: Element-wise comparison

true
true

Dictionaries

dict[K, V] maps keys to values and keeps them in insertion order:

var ages: dict[string, int] = {"alice": 30, "bob": 25};
ages["dave"] = 40;
ages.remove("bob");

println(ages["alice"]);
println(ages.containsKey("bob"));
println(ages.get("zed") ?? -1);
println(ages.keys());

for ((name, age) in ages.entries()) {
    println(i"{$ name $} is {$ age $}");
}

Listing 6-7: Dictionary operations

30
false
-1
["alice", "dave"]
alice is 30
dave is 40

d[key] expects the key to be present; d.get(key) returns an Option[V], and ?? supplies a default when it is None. A missing key with d[key] throws:

This code throws at run time
@mode(host.native)

func main(): void {
    let ports: dict[string, int] = {"http": 80, "https": 443};
    println(ports["ssh"]);
}
Uncaught KeyError: key 'ssh' not found in dict
  at main (main.lphc:5:13)

Use ports.get("ssh") ?? 22 when there is a sensible default, or containsKey first.

Mistake: destructuring a dictionary directly

A for loop over a dictionary visits its keys. Python programmers sometimes write a key-value pattern there; in Loup it destructures each key string instead, so the program runs and prints nonsense:

This code does not do what it should
let ages: dict[string, int] = {"alice": 30};
for ((name, age) in ages) {
    println(i"{$ name $} is {$ age $}");
}
a is l

Loop over ages.entries() for pairs, as in Listing 6-7, or over the keys and index: for (name in ages) { println(ages[name]); }.

Sets

set[T] holds distinct values:

var seen: set[string] = {"a.log", "b.log"};
seen.add("c.log");
seen.add("a.log");
seen.remove("b.log");
println(seen.contains("a.log"));
println(seen.length());
for (file in seen) {
    println(file);
}

Listing 6-8: A set of file names

true
2
a.log
c.log

⚠ Planned. Set algebra (union, intersection, difference) is not available on set[T] in 1.25.1. Loop over one set and test membership in the other.

Tuples

A tuple groups a fixed number of values of possibly different types. Its type is written in parentheses, its fields are read as .0, .1, …, and a let can destructure it:

func parseHostPort(addr: string): (string, int) {
    let parts = addr.split(":");
    return (parts[0], parts[1].toInt());
}

func main(): void {
    let endpoint = parseHostPort("db.local:5432");
    println(endpoint.0);
    let (host, port) = endpoint;
    println(i"{$ host $} on port {$ port $}");
}

Listing 6-9: Returning two values in a tuple

db.local
db.local on port 5432

Use a tuple for a short-lived pair; once the fields deserve names, make a struct (chapter 7). (Native builds cannot yet print an enum whose payload is a tuple; see chapter 2.)

List comprehensions

A comprehension builds a list from a loop and an optional filter. Its for clause has no parentheses:

let squares = [x * x for x in 0..5];
let sizes = [120, 4096, 75, 9000];
let large = [s for s in sizes if s > 1000];
println(squares);
println(large);

Listing 6-10: List comprehensions

[0, 1, 4, 9, 16]
[4096, 9000]

⚠ Planned. Dictionary and set comprehensions ({k: v for …}, {x for …}) do not parse in 1.25.1. Build the collection in a loop.

Summary

  • list[T] has indexing (negative from the end), slicing xs[a:b:step], push/pop/remove, +, contains, indexOf, join, sort(comparator); an index out of range throws IndexError.
  • Collections are values: assignment and argument passing copy them (COLL-1). Use edits to change a caller's collection.
  • == on lists, sets and dicts is always false in 1.25.1 (EQ-3 not implemented); tuples compare correctly.
  • dict[K, V]: d[k] throws KeyError for a missing key, d.get(k) ?? default does not; iterate entries() for pairs.
  • set[T] has add, remove, contains; set algebra is ⚠ planned.
  • Tuples group values and destructure with let (a, b) = t; list comprehensions are [f(x) for x in xs if cond], and dict/set comprehensions are ⚠ planned.
  • Chapter 7 covers structs.