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 endpoints, 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 endpoint
type determines the range type: 1..5 is Range<Int>, "a".."m" is
Range<String>, and 0.0..=1.0 is Range<Float>.
Plain ranges are iterable when the endpoint type has a discrete successor. 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
}
Nomi has no separate Char type; single-character text is a String.
Codepoint represents a Unicode scalar value, and 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)
}
Ranges can also be intervals over any comparable type. Non-discrete ranges still support interval queries, but they are not plain iterables because strings and floats have no single successor operation:
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. There’s no aliasing footgun:
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.