Chapter 6 — Arrays and Iteration

An array is a growable, in-order sequence of values that all share one type. Written [T] — so [int] is an array of ints, [string] an array of strings.

fn main() -> int {
    let xs = [10, 20, 30]
    println("{xs[0]}")        // 10  — index from zero
    println("{xs.len()}")     // 3   — how many
    return xs[2]              // 30
}

Indexing is bounds-checked: reach past the end and the program stops with a runtime error rather than reading rubbish. Every element must be the same type; an empty literal [] takes its type from context (let a: [int] = []).

Growing and shrinking

Through a mutable place (a var, or a mut parameter — Chapter 12 has the full story), arrays grow and change in place:

fn main() -> int {
    var a: [int] = []
    a.append(1)               // grow by one (amortised O(1))
    a.append(2)
    a.append(3)
    a[0] = 10                 // assign to an element
    let last = a.remove_last() // takes the last element off and hands it back: 3
    return a[0] + a.len() + last  // 10 + 2 + 3 = 15
}

The core array methods are append(x) (grow by one), remove_last() (take the last element off and hand it back — a runtime error if the array is empty), remove_at(i) (remove and return the element at index i, shifting the rest down — a runtime error if i is out of range), and len(). The free function len(a) works too if you prefer. Reading and indexing don’t need a mutable place; growing, removing, and assigning do.

Walking over things: for ... in

for x in collection binds each element in turn:

fn main() -> int {
    let xs = [10, 20, 30]
    var sum = 0
    for x in xs {
        if x == 20 { continue }   // skip the 20
        sum = sum + x             // 10 + 30
    }
    return sum + len(xs)          // 40 + 3 = 43
}

for also walks an integer range written lo..hi. The range is exclusive of the top, so 0..n gives you 0, 1, ..., n-1 — exactly the indices of an n-element array, which is not a coincidence:

var sum = 0
for i in 0..10 { sum = sum + i }   // 0+1+...+9 = 45

A range that’s empty or backwards (5..5, or 9..3) simply runs zero times, no drama.

And when you want both the position and the element, there’s a form for that:

let names = ["ada", "alan", "grace"]
for (i, name) in names {
    println("{i}: {name}")
}
0: ada
1: alan
2: grace

That’s the whole iteration story, and it’s deliberately small: for x in a for elements, for i in a..b for a counter, for (i, x) in a for both. Three forms, three distinct jobs, no overlap. There’s exactly one range operator (.., exclusive) on purpose — an inclusive version would just be a second way to write lo..hi+1, and Ingle dislikes having two ways to say one thing.

A small performance note you can mostly ignore. The for forms aren’t just tidier than a hand-rolled loop with a counter — they’re faster, because each compiles down to a single fused machine step (increment, bounds-check, and for arrays the element fetch, all at once) instead of the dozen-odd instructions a manual counter spends per turn. The idiomatic form is also the quick one.

One thing you can’t do: a range only exists as something to iterate. let r = 0..5 on its own is an error — ranges aren’t first-class values, just a way to drive a for.

Slices: a borrowed window, no copy

arr[lo..hi] is a slice — a read-only view into part of an array, from lo (inclusive) to hi (exclusive), made with no copying at all. Its type is Slice<T>, and you treat it like an array: index it, ask its len(), walk it with for, even slice it again.

fn sum(xs: Slice<int>) -> int {
    var t = 0
    for x in xs { t = t + x }
    return t
}

fn main() -> int {
    let data = [10, 20, 30, 40, 50]
    let win = data[1..4]              // a view of [20, 30, 40] — nothing allocated
    let mid = win[1..2]               // a slice of a slice → [30]
    return sum(win) + mid[0]          // 90 + 30 => 120
}
=> 120

A slice borrows the array it looks into, and the compiler keeps that honest the same way it does everywhere else. While a slice is alive its source array is frozen — you can’t append to it, reassign it, or move it, since that could pull the ground out from under the view — and a slice can’t escape: it may be a parameter or a local, but never a return value, a struct field, or an array element. When you genuinely need to keep a sub-array, reach for the copying companion arr.slice(lo, hi), which hands back a fresh, owned [T] you can store or return.

.clone(): an explicit deep copy

Because arrays and structs are uniquely owned (Chapter 12 has the full story), the compiler won’t let you quietly make a second owner of one. Reading a struct straight out of an array element to stash elsewhere is a compile error, in fact — a shallow copy would alias the element’s heap fields and free them twice. When you genuinely want an independent copy, ask for one out loud with .clone():

fn main() -> int {
    var nums = [1, 2, 3]
    var copy = nums.clone()           // an independent deep copy
    copy.append(4)
    return nums.len() + copy.len()    // 3 + 4 => 7  (nums itself is untouched)
}
=> 7

x.clone() copies recursively — array elements, struct fields, and everything they own — so changing the clone never reaches the original, and vice versa. It works on arrays and on structs, including the generic ones like Map<K, V> and Set<K>. The cost is always visible at the call site; Ingle never deep-copies a value behind your back.

Fireside trivia. “Off-by-one errors are the two hardest problems in computer science.” The exclusive-end range is the industry’s hard-won answer to half of them: when the top is exclusive, the length of 0..n is just n, two adjacent ranges 0..k and k..n meet perfectly with no overlap or gap, and you almost never write <= by mistake. Dijkstra wrote an entire note in 1982 arguing for exactly this convention. Ingle simply makes it the only option and saves you the argument.