Collections
Nomi has four core collection types — List<'T>,
Vector<'T>, Map<'K, 'V>, and
Set<'T> — plus Range<'T>, an interval over comparable values. Ranges over
discrete start and end values, such as Int and
Codepoint, also iterate. The
collections are all immutable and persistent: every operation returns a new collection,
leaving the original alone. The stdlib puts the value being transformed first on
every operation, so chains read naturally through pipes.
[a, b, c] is the literal syntax. The std/list module provides the
List-specific operations (concat, head, tail, …); generic
iteration — map, filter, reduce, and so on — flows through the
Iter module, materializing back with
Iter.to_list() when you want a List:
fn main(): Int {
xs = [1, 2, 3, 4, 5]
xs
|> Iter.filter(|n| n > 2)
|> Iter.map(|n| n * n)
|> Iter.reduce(|acc = 0, n| acc + n)
|> dbg
}
Vectors
Section titled “Vectors”#[a, b, c] is the vector literal. Vector<'T> is an immutable random-access
sequence. Use List<'T> for cons-list pattern matching and cheap prepends; use
Vector<'T> when indexed lookup and push-at-end are the natural operations.
Lists and vectors both implement Add, so xs + ys concatenates two
values of the same collection type; the named functions
List.concat and
Vector.concat are the explicit equivalents.
fn main() {
names =
#["Ada", "Grace"]
|> Vector.push("Katherine")
dbg names
dbg Vector.length(names)
dbg Vector.at(names, 1)
Unit
}
{"key" => value, …} is the map literal. Map.get returns Maybe<'V> —
Some(value) when the key exists, None when it doesn’t:
fn main(): Maybe<String> {
config = {"host" => "localhost", "port" => "8080"}
dbg Map.size(config)
dbg Map.get(config, "host")
dbg Map.get(config, "user")
}
We’ll see how to unwrap Maybe<'T> properly in Pattern Matching.
#{a, b, c} is the set literal. Set<'T> stores unique values and keeps the
first occurrence’s insertion order for iteration. Empty sets need type context:
empty: Set<Int> = #{}.
fn main(): Bool {
s = #{1, 2, 3, 2, 1}
dbg Set.size(s)
dbg Set.contains?(s, 2)
dbg Set.contains?(s, 9)
}
Ranges
Section titled “Ranges”1..5 (half-open) and 1..=5 (inclusive) are range literals. The start and
end values determine the range type: 1..5 is Range<Int>, "a".."m" is
Range<String>, and 0.0..=1.0 is Range<Float>.
Every range is an interval over comparable values, so it can answer containment questions. A range is also iterable only when its value type has a well-defined next value. That makes integer and codepoint ranges natural replacements for generated lists:
fn main(): List<Int> {
dbg Iter.to_list(1..5)
dbg Iter.to_list(1..=5)
// Unbounded ranges work too; bound them with Iter.take downstream.
Range.naturals()
|> Iter.take(3)
|> Iter.to_list()
|> dbg
}
Codepoint is discrete too, so Codepoint ranges iterate:
fn main(): Maybe<List<String>> {
a = try Codepoint.from_int(97)
d = try Codepoint.from_int(100)
letters =
a..=d
|> Iter.map(Codepoint.to_string)
|> Iter.to_list()
|> dbg
Some(letters)
}
Strings and floats are comparable, so they work as range bounds. They are not
plain iterables because neither type has one canonical successor: a string
range like "a".."m" can test whether "a" <= "h" < "m", but there is no
single obvious next string after "a" to enumerate.
fn main(): Bool {
dbg Range.contains?("a".."m", "h")
dbg Range.contains?(0.0..=1.0, 1.0)
}
When the caller supplies the step, Range.step_by turns Steppable ranges into
lazy iterables:
fn main(): List<Decimal> {
1.0d..=1.3d
|> Range.step_by(0.1d)
|> Iter.to_list()
|> dbg
}
Persistent immutability
Section titled “Persistent immutability”A “modifying” operation returns a new collection; the original binding still holds its original value:
fn main(): Int {
m1 = {"a" => 1, "b" => 2}
m2 = Map.put(m1, "c", 3)
dbg Map.size(m1)
dbg Map.size(m2)
}
The next chapter — Iteration & Loops — covers
Iter.loop for stateful iteration and how break / continue / return
behave inside the callbacks you pass to these collection ops.