9. Collections
Lightning provides two general-purpose mutable containers—tables and arrays—plus eager functional operators in the collections module. Packed homogeneous numeric arrays are covered in Chapter 10.
9.1 Tables
A table literal uses braces and stores hashed key/value entries:
let user = {
name: "Ada",
active: true,
score: 41
}
assert(user.name == "Ada")
user.score += 1
assert(user["score"] == 42)
For an identifier-like string key, table.name is shorthand for table["name"]. Bracket indexing accepts a computed key:
let user = {active: true}
const field = "active"
assert(user[field] == true)
A missing key reads as nil. Assigning nil stores a present entry whose value is nil; it does not delete the entry. Table length and iteration include such entries. Use delete table[key] or delete table.name to remove an entry, and use key in table to distinguish a missing key from a present nil value. Deleting an absent entry is harmless; deleting an array index or fixed class field is an error.
let options = {cache: nil}
assert("cache" in options)
delete options.cache
assert(!("cache" in options))
nil is not a valid table key. Numeric keys use SameValueZero hashing: 0 and -0 identify the same entry, and all boxed NaN values identify one retrievable NaN entry even though nan == nan is false. String keys hash by content. Reference-like keys retain their normal identity semantics.
Table iteration yields the original key and value in a stable order for that table state:
let user = {name: "Ada", score: 42}
for key, value in user {
print(key, value)
}
Code should not depend on the specific order of unrelated table keys.
Reserving table capacity
table.reserve(table, capacity) reserves hash capacity without changing entries or length. Capacity must be a finite, non-negative integer. Reserving a frozen table is an error.
Because table is also a type keyword, scripts commonly alias the module:
import "table" as tables
let index = {}
tables.reserve(index, 128)
index["lightning"] = 1
Reserve when the expected number of entries is known and avoiding intermediate rehashes matters.
9.2 Arrays
An array literal uses brackets and stores a contiguous sequence of dynamic values:
let colors = ["red", "green", "blue"]
assert(colors::len() == 3)
assert(colors[0] == "red")
colors[1] = "emerald"
Indices are zero-based. An index store requires an existing finite, non-negative integral index; assignment does not grow the array. A missing or out-of-range read returns nil. Use push, pop, or array.resize to change length.
let colors = ["red", "green", "blue"]
colors::push("violet")
assert(colors::pop() == "violet")
Array iteration yields (index, value):
let colors = ["red", "green", "blue"]
for index, color in colors {
print(index, color)
}
Capacity, length, and filling
The mutable operations live in the array module:
import "array" as arrays
let samples = [1, 2, 3]
arrays.reserve(samples, 32) # length remains 3
arrays.resize(samples, 5) # new elements are nil
arrays.fill(samples, 0, 3, 5) # half-open range [3, 5)
array.reserve(array, capacity)increases capacity when necessary without changing length.array.resize(array, length)truncates or extends the array; extended slots containnil.array.fill(array, value)fills the whole array.array.fill(array, value, start)fills[start, length).array.fill(array, value, start, end)fills the half-open range[start, end).
All counts and bounds must be finite, non-negative integers. fill validates the complete range before changing the array. These mutating operations reject frozen arrays.
9.3 Iterator protocol
Ranges, generators, iterator adapters, and values with a next trait share one logical operation: next -> (value, done). A script-defined next!() method or next trait returns an exact two-element array [value, done], and done must be a boolean. Completion is independent of the value, so [nil, false] yields a legitimate nil, while [nil, true] ends traversal.
import iterator
import traits
let source = {position: 0}
traits.set(source, "next", || {
self.position += 1
if self.position == 1 { return [nil, false] }
if self.position == 2 { return [17, false] }
[nil, true]
})
let pair = iterator.next(source)
assert(pair[0] == nil && pair[1] == false)
pair = iterator.next(source)
assert(pair[0] == 17 && pair[1] == false)
assert(iterator.next(source)[1] == true)
Generic iterators receive increasing numeric enumeration keys. Direct table and array iteration preserves the collection key for for key, value.
An iterator over a mutable array, table, or typed array captures that collection's mutation version. Any successful store, insertion, deletion, push, pop, resize, join, or trait-mediated mutation invalidates it. Its next operation then raises a catchable concurrent-mutation error. Mutation is unrestricted after exhaustion, and mutating a different collection or a nested value is allowed.
9.4 Shallow duplication
dup creates new top-level storage for an array or table while retaining the same elements, keys, and present nil entries. Nested heap values remain shared. A duplicated class instance has distinct top-level identity with shallowly copied fields, and a duplicated closure receives a by-value copy of its captures at duplication time. Immutable values may return themselves. dup never recursively clones an object graph.
let child = {value: 1}
let original = [child]
let copy = original::dup()
copy::push({value: 2})
copy[0].value = 7
assert(original::len() == 1)
assert(original[0].value == 7)
9.5 Functional collection operators
Import the module before using its eager operators:
import collections
Unless stated otherwise, callbacks receive (value, key). For arrays, strings, and typed arrays, key is the numeric index. For tables, it is the table key. Generic iterables receive increasing numeric enumeration keys.
The operators accept arrays, tables, strings, typed arrays, ranges, generators, and objects that implement the next protocol. They are eager: traversal and every callback finish before the operator returns. Returned collection results are ordinary arrays.
A non-iterable source, a non-callable callback, an invalid callback result, or malformed next result raises a catchable error. Callback errors propagate unchanged; no partial result is returned. Predicates and sort comparators must return booleans.
collections.map(collection, callback)
Calls callback(value, key) for every element and returns an array of callback results.
import collections
const labels = collections.map([10, 20, 30], |value, index| `#{index}: {value}`)
assert(labels[1] == "#1: 20")
collections.filter(collection, predicate)
Returns an array containing values for which predicate(value, key) returns true. The predicate must return a boolean.
import collections
const even = collections.filter([1, 2, 3, 4], |value, index| value % 2 == 0)
assert(even::len() == 2)
collections.reduce(collection, initial, callback)
Folds the collection from its traversal order. The callback receives (accumulator, value, key) and returns the next accumulator. Reducing an empty collection returns initial.
import collections
const total = collections.reduce([3, 5, 7], 0,
|sum, value, index| sum + value)
assert(total == 15)
collections.each(collection, callback)
Calls callback(value, key) for side effects and returns the original collection.
import collections
let by_name = {}
collections.each(["Ada", "Grace"], |name, index| {
by_name[name] = index
})
assert(by_name.Grace == 1)
collections.any and collections.all
any returns true as soon as one predicate call returns true. all returns false as soon as one call returns false. Predicates must return booleans.
import collections
assert(collections.any([2, 5, 8], |value, index| value % 2 == 1))
assert(collections.all([2, 4, 8], |value, index| value % 2 == 0))
assert(!collections.any([], |value, index| true))
assert(collections.all([], |value, index| false))
collections.find(collection, predicate)
Returns the first value whose predicate returns true, or nil if there is no match.
import collections
const first_large = collections.find([4, 12, 7], |value, index| value > 10)
assert(first_large == 12)
Because nil is a legitimate collection value, a returned nil can mean either “found a nil value” or “not found.” Track the predicate separately when that distinction matters.
collections.sort(collection, comparator)
Returns a new, stable sorted array. The source is not modified. comparator(left, right) must return true exactly when left belongs before right. A non-boolean result is an error, and an error thrown by the comparator propagates unchanged.
import collections
const records = [
{score: 8, name: "Ada"},
{score: 3, name: "Grace"},
{score: 8, name: "Lin"}
]
const ordered = collections.sort(records, |left, right| left.score < right.score)
assert(ordered[0].name == "Grace")
assert(ordered[1].name == "Ada") # equal scores retain input order
collections.keys and collections.values
keys and values return parallel arrays in the collection's traversal order.
import collections
const source = {alpha: 10, beta: 20}
const keys = collections.keys(source)
const values = collections.values(source)
assert(keys::len() == 2 && values::len() == 2)
For an array, keys returns [0, 1, ...]. Present table entries whose value is nil appear in both results.
collections.zip(iterable, ...)
Traverses one or more iterables in parallel and returns an array of row arrays. It stops when the shortest input is exhausted.
import collections
const rows = collections.zip(["Ada", "Grace"], [10, 20, 30])
assert(rows::len() == 2)
assert(rows[0][0] == "Ada" && rows[0][1] == 10)
Calling zip without an input is an error.
9.6 Mutation during traversal
The mutation-version rule from §9.3 also applies to every eager adapter. Mutating a source array, table, or typed array from its callback invalidates the traversal and raises a catchable concurrent-mutation error—even when the store writes the same value. This check occurs before a short-circuiting operator returns. Mutating a different collection is allowed.
import collections
let source = [2, 4, 6]
let output = []
collections.each(source, |value, index| output::push(value * 2))
assert(output[2] == 12)
This rule prevents skipped elements, duplicate visits, and table-rehash hazards.