Nanyx

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 typeSpecial aliasCharacteristics
List(a)list(a)Indexable collections
Set(a)-Membership checks
Map(k, v)k -> #some(v) | #notFoundKey/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.