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:
@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:
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:
@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:
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 onset[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), slicingxs[a:b:step],push/pop/remove,+,contains,indexOf,join,sort(comparator); an index out of range throwsIndexError.- Collections are values: assignment and argument passing copy them (COLL-1). Use
editsto change a caller's collection. ==on lists, sets and dicts is alwaysfalsein 1.25.1 (EQ-3 not implemented); tuples compare correctly.dict[K, V]:d[k]throwsKeyErrorfor a missing key,d.get(k) ?? defaultdoes not; iterateentries()for pairs.set[T]hasadd,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.