# 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:

```loup
@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*

```text
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:

```loup
#~ @mode(host.native)
#~ func main(): void {
var latencies = [120, 35, 80];
latencies.sort(func(a: int, b: int) => a - b);
println(latencies);
#~ }
```

*Listing 6-2: Sorting with a comparator*

```text
[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:

```loup
#~ @mode(host.native)
#~ func main(): void {
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*

```text
[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:

```loup,throws
@mode(host.native)

func main(): void {
    let readings = [12, 15, 11];
    println(readings[3]);
}
```

```text
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):

```loup
#~ @mode(host.native)
#~ func main(): void {
let defaults = ["--verbose"];
var flags = defaults;
flags.push("--color");
println(defaults);
println(flags);
#~ }
```

*Listing 6-4: Assignment copies a list*

```text
["--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:

```loup
#~ @mode(host.native)
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*

```text
["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`:

```loup,not_desired_behavior
#~ @mode(host.native)
#~ func main(): void {
let expected = [1, 2, 3];
let actual = [1, 2, 3];
println(expected == actual);
#~ }
```

```text
false
```

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

```loup
#~ @mode(host.native)
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*

```text
true
true
```

## Dictionaries

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

```loup
#~ @mode(host.native)
#~ func main(): void {
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*

```text
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:

```loup,throws
@mode(host.native)

func main(): void {
    let ports: dict[string, int] = {"http": 80, "https": 443};
    println(ports["ssh"]);
}
```

```text
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:

```loup,not_desired_behavior
#~ @mode(host.native)
#~ func main(): void {
let ages: dict[string, int] = {"alice": 30};
for ((name, age) in ages) {
    println(i"{$ name $} is {$ age $}");
}
#~ }
```

```text
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:

```loup
#~ @mode(host.native)
#~ func main(): void {
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*

```text
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:

```loup
#~ @mode(host.native)
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*

```text
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:

```loup
#~ @mode(host.native)
#~ func main(): void {
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*

```text
[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.
