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
forforms aren’t just tidier than a hand-rolledloopwith 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..nis justn, two adjacent ranges0..kandk..nmeet 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.