Lesson 02: Collections, Slicing & Yield Comprehensions
In this lesson, we explore Tinsel's core collection types—arrays, tuples, and hash maps—along with range slicing, universal membership testing (in / !in), for ... in ... { yield } comprehensions, and lazy pull-based iterators (.iter()).
1. Learning Objectives
By the end of this lesson, you will be able to:
- Index, slice (
arr[a..b]), and mutate dynamic arrays, including negative tail indexing (arr[-1]). - Group composite states in tuples and store key-value data in hash maps.
- Test membership across collections, strings, and ranges with
inand!in. - Construct and filter arrays declaratively with
for ... in ... { yield }comprehensions. - Chain eager array methods (
.map(),.filter(),.sort_by()) and lazy short-circuiting.iter()streams.
2. Dynamic Arrays & Range Slicing
Arrays are ordered, dynamically sized sequences:
let nums = [10, 20, 30, 40, 50]
// 0-based indexing and negative indexing from the end:
assert(nums[0] == 10)
assert(nums[-1] == 50)
assert(nums[-2] == 40)
Range Slicing (arr[start..end])
Both arrays and strings support direct slicing with half-open (..), inclusive (..=), open-start (..b), open-end (a..), and negative-bound (a..-1) ranges:
let items = ["a", "b", "c", "d", "e"]
assert(items[1..4] == ["b", "c", "d"])
assert(items[..2] == ["a", "b"])
assert(items[3..] == ["d", "e"])
assert(items[1..-1] == ["b", "c", "d"])
let word = "advent"[1..=4]
assert(word == "dven")
In-Place Mutation & Concatenation
.push(val)appends an element in-place;.pop()removes and returns the last element.arr1 + arr2returns a new concatenated array.
let stack = [1, 2]
stack.push(3)
assert(stack == [1, 2, 3])
let top = stack.pop()
assert(top == 3 && stack == [1, 2])
assert(stack + [4, 5] == [1, 2, 4, 5])
3. Fixed Tuples
Tuples are created with comma-separated expressions inside parentheses (a, b, c) (a 1-element tuple uses a trailing comma (x,)). They are ideal for coordinate pairs, graph search states, and returning multiple values from a function:
let state = (vec2(3, 4), "NORTH", 100)
let (pos, heading, energy) = state
assert(pos == vec2(3, 4))
assert(heading == "NORTH" && energy == 100)
4. Hash Maps & Universal Structural Hashing
Maps associate keys with values. Unlike Go (where slices cannot be map keys) or Java/Python (where mutable lists cannot be hashed), Tinsel hashes arrays, tuples, vectors, and structs by value.
Map Literals & Key Syntax
- Bare identifiers
{ x: 10, y: 20 }create string keys"x"and"y". - String, integer, float, and boolean literals can be used directly as keys (
{ -1: "left", 1: "right" }). - Non-literal expression keys (variables, vectors, tuples, structs) are wrapped in brackets
{ [expr]: val }:
let start = vec2(0, 0)
let visited = {
label: "origin",
[start]: 0,
[(1, 2)]: 15
}
assert(visited.label == "origin")
assert(visited["label"] == "origin")
assert(visited[vec2(0, 0)] == 0)
assert(visited[(1, 2)] == 15)
Universal Membership (in and !in)
The in and !in operators work uniformly across maps, arrays, strings, ranges (0..10, 'a'..='z'), sets, grids, and range sets:
let scores = { alice: 100, bob: 85 }
if "alice" in scores {
scores.alice += 20
}
assert(scores.alice == 120 && "charlie" !in scores)
assert('v' in "advent" && 7 in 1..=10)
5. for ... in ... { yield } Comprehensions
In Tinsel, for loops are expressions. When a for loop body contains yield, it evaluates to a new array containing all yielded elements:
let evens_squared = for x in 1..=8 {
if x % 2 == 0 {
yield x * x
}
}
assert(evens_squared == [4, 16, 36, 64])
Nested Comprehensions
Each for ... yield expression produces an array for its own loop level; yielding an inner comprehension produces a 2D array (or call .flatten() to flatten into a 1D array):
let pairs = (for r in 0..2 {
yield for c in 0..3 {
if r != c {
yield (r, c)
}
}
}).flatten()
assert(pairs == [(0, 1), (0, 2), (1, 0), (1, 2)])
Zero-Allocation Comprehension Fusion
When a for ... { yield expr } comprehension is immediately reduced with .sum(), .product(), .min(), .max(), or .len(), the Tinsel bytecode optimizer fuses the loop and reduction into a single stack accumulator without allocating an intermediate array:
let sum_of_cubes = (for x in 1..=10 {
yield x * x * x
}).sum()
assert(sum_of_cubes == 3025)
6. Higher-Order Array Methods & Lazy .iter() Streams
Eager Array Methods
Arrays provide rich functional transformations out of the box:
let nums = [5, 2, 8, 2, 9, 1]
assert(nums.unique() == [5, 2, 8, 9, 1])
assert(nums.sort() == [1, 2, 2, 5, 8, 9])
assert(nums.sort_desc() == [9, 8, 5, 2, 2, 1])
assert(nums.filter(x -> x >= 5).map(x -> x * 10) == [50, 80, 90])
assert([1, 2, 3, 4].fold(0, (acc, x) -> acc + x) == 10)
let words = ["pear", "fig", "banana", "apple"]
assert(words.sort_by(w -> w.len()) == ["fig", "pear", "apple", "banana"])
let matrix = [[1, 2, 3], [4, 5, 6]]
assert(matrix.transpose() == [[1, 4], [2, 5], [3, 6]])
Lazy Pull-Based Iterators (.iter())
When searching across huge ranges (0..1_000_000_000) where you only need the first few matches, call .iter() (std/iter) to stream values on demand and short-circuit immediately with .take(n), .find(pred), .any(pred), or .all(pred):
let first_match = (100..1_000_000_000)
.iter()
.find(x -> x % 37 == 0 && x % 41 == 0)
assert(first_match == 1517)
7. Common Pitfalls & Gotchas
Gotcha 1: Bare Identifier vs. Variable Key in Map Literals
In { k: 10 }, k is a literal string key "k". If k is a variable holding a key value, wrap it in brackets: { [k]: 10 }.
Gotcha 2: .sort() returns a new sorted array
Calling arr.sort() or arr.reverse() returns a new array and does not mutate arr in-place. Assign the result (let sorted = arr.sort()).
8. Hands-On Exercises
Exercise 2.1: Calibration Sum (Warmup)
Given an array of digit rows [[1, 2], [3, 8], [1, 2, 3, 4, 5], [7]], use a for ... yield comprehension and negative indexing (r[-1]) to form first * 10 + last for each row and compute their sum.
Solution & Explanation
let rows = [[1, 2], [3, 8], [1, 2, 3, 4, 5], [7]]
let total = (for r in rows {
yield r[0] * 10 + r[-1]
}).sum()
assert(total == 142)
println("Calibration sum:", total)
Exercise 2.2: Character Frequency Map (Practice)
Count the frequency of each character in "abracadabra" using a map and verify how many times 'a' and 'r' appear.
Solution & Explanation
let counts = {}
for ch in "abracadabra".chars() {
counts[ch] = counts.get(ch, 0) + 1
}
assert(counts['a'] == 5 && counts['r'] == 2)
println("Count of 'a':", counts['a'])
Exercise 2.3: Lazy Pythagorean Triples (Challenge)
Use a comprehension over a in 1..=20 and b in a..=20 to yield all triples (a, b, c) where a^2 + b^2 = c^2 for an integer c \le 25.
Solution & Explanation
let triples = (for a in 1..=20 {
yield for b in a..=20 {
let c2 = a * a + b * b
let c = round(sqrt(c2))
if c <= 25 && c * c == c2 {
yield (a, b, c)
}
}
}).flatten()
assert(triples.len() == 7)
assert((3, 4, 5) in triples && (5, 12, 13) in triples)
println("Triples:", triples)
Next Steps
In Lesson 03: Functions, Closures & Pipelines, we will explore named functions, parameter destructuring, lexical closures, and the |> pipeline operator.