Collections
Lists, maps, sets, and mutability
Collections are typically immutable in Nanyx, although it is possible to use and create mutable versions. Mutable versions live in nanyx/collections.mutable and, by convention, have an M prefix in their name (for example, MList vs List).
Immutable vs mutable
Immutable collections are persistent data structures, while mutable collections are optimized for in-place updates.
-- Immutable list
def numbers = [1, 2, 3]
-- Mutable list (requires $Memory context)
def addItems = {
memory {
def items: MList(int) = mut []
items += 1
items += 2
MList.toList(items)
}
}
Use immutable collections by default. Reach for mutable variants when profiling shows a hot path that benefits from in-place updates.
Common collection abstractions
Most of the time, Nanyx programs don’t care very much about the specific implementation of a collection, just that it supports certain operations. For this reason, the standard library provides a number of common collection abstractions that can be implemented by any type:
| Collection type | Special alias | Characteristics |
|---|---|---|
List(a) | list(a) | Indexable collections |
Set(a) | - | Membership checks |
Map(k, v) | k -> #some(v) | #notFound | Key/value lookup (a map is callable, and calling it is a lookup) |
Collection(a) | - | Any in-memory collection |
Sequence(a) | seq(a) | Finite, deterministic sequences |
Generator(a) | gen(a) | Potentially infinite or non-deterministic sequences |
def lookup = ["a" => 1, "b" => 2]
def unique: Set(int) = [1, 2, 2, 3]
Default literal types
A square-bracket literal takes its type from the annotation or the surrounding usage. When neither says otherwise, [1, 2, 3] is a list(a) and ["a" => 1] is a map(k, v).
For advanced patterns such as associated types and constrained collection capabilities, see Associated types and constraints.
Syntax
Nanyx supports a generalisable literal syntax for all collection types; the square bracket form is available to any target type that implements the correct builder context:
context ListBuilder(a) = (
yield: a -> ()
)
def MyList.apply: ([ListBuilder(a)] () -> ()) -> MyList(a) = { f ->
memory {
def listRef = mut MyList()
use (
yield = { element -> listRef := listRef \add(element) }
) in f()
listRef
}
}
-- The square bracket literal syntax is now available for type MyList
def myList: MyList(int) = [1, 2, 3]
context MapBuilder(k, v) = (
`=>`: (k, v) -> ()
)
def MyMap.apply: ([MapBuilder(k, v)] () -> ()) -> MyMap(k, v) = { f ->
memory {
def mapRef = mut MyMap()
use (
`=>` = { k, v -> mapRef := mapRef \add(k, v) }
) in f()
mapRef
}
}
-- The square bracket literal syntax is now available for type MyMap
def myMap: MyMap(string, int) = ["a" => 1, "b" => 2]
This works because the square bracket syntax desugars to a call to the apply function on the target type, which provides the builder context that defines how the literal should be constructed:
def myList: MyList(int) = [1, 2, 3]
-- Desugars to:
def myList: MyList(int) = MyList.apply {
yield(1)
yield(2)
yield(3)
}
For iteration constructs, see Loops. For query-style collection programming, see Sequences and queries.