Ship's default collection values are persistent. A change returns a new value while the original remains available. Array is mutable scratch storage; scoped editors provide mutation during persistent batch updates. See mutability for local and field assignment.
Construction
Brackets construct the default ordered collection, Vector. Brackets with inner bars construct mutable Array. Recursive lists use list(...). %{ ... } constructs Set; brace-and-colon literals construct Map.
Collection Literals
✓[11: Int, 22: Int, 33: Int]: Vector(Int);
✓[|11: Int, 22: Int, 33: Int|]: Array(Int);
✓listlist: pure fn!('a)(...Int) -> List(Int)(11: Int, 22: Int, 33: Int): List(Int);
✓{ idid: String: "todo-1""todo-1": String; }: Map(String, String);
✓%{ 11: Int; 22: Int; 33: Int }: Set(Int);
✓%{}: Set('a);
let uniqueunique: Set(Int) = %{ 11: Int; 22: Int; 22: Int; 33: Int };
expect ✓Set.lengthlength: pure fn!('a)(items: Set(Int)) -> Int(uniqueunique: Set(Int)) == 33: Int;The %{ introducer distinguishes sets from blocks, products, and maps. Semicolons separate member expressions. The syntax does not expose a bitset representation or implicitly lift a bare value into a set expression.
type Permission =
| Read
| Write
| Admin
Set Membership
let grantedgranted: Set(Book.Collections.Permission) = %{ ReadRead: Book.Collections.Permission; Write };
✓grantedgranted: Set(Book.Collections.Permission): Set(Permission);
expect ✓Set.containscontains: pure fn!('a)(items: Set(Book.Collections.Permission), item: Book.Collections.Permission) -> Bool(grantedgranted: Set(Book.Collections.Permission), Read);
expect ✓Set.containscontains: pure fn!('a)(items: Set(Book.Collections.Permission), item: Book.Collections.Permission) -> Bool(grantedgranted: Set(Book.Collections.Permission), Admin) == FalseFalse: Bool;
let statesstates: Set([> :failed(String) | :idle ]) = %{ :idle; :failed("network""network": String) };
expect ✓Set.containscontains: pure fn!('a)(items: Set([> :failed(String) | :idle ]), item: [> :failed(String) | :idle ]) -> Bool(statesstates: Set([> :failed(String) | :idle ]), :idle);
expect ✓Set.containscontains: pure fn!('a)(items: Set([> :failed(String) | :idle ]), item: [> :failed(String) | :idle ]) -> Bool(statesstates: Set([> :failed(String) | :idle ]), :failed("network""network": String));
expect ✓Set.containscontains: pure fn!('a)(items: Set([> :failed(String) | :idle ]), item: [> :failed(String) | :idle ]) -> Bool(statesstates: Set([> :failed(String) | :idle ]), :failed("auth""auth": String)) == FalseFalse: Bool;Sets and maps compare by contents. Members and keys compare structurally, so tuples and variants work as keys.
Structural Keys
expect ✓%{ ReadRead: Book.Collections.Permission; Write } == %{ WriteWrite: Book.Collections.Permission; Read };
expect ✓%{ ReadRead: Book.Collections.Permission; Write } != %{ ReadRead: Book.Collections.Permission; Admin };
expect ✓{ idid: String: "todo-1""todo-1": String; } != { idid: String: "todo-2""todo-2": String; };
let cellscells: Map((Int, Int), String) = Map.editedit: pure fn!('k, 'v)(items: Map((Int, Int), String) @ 'm, ~f: fn(editor: MapEditor((Int, Int), String) @ 'm) -> Unit) -> Map((Int, Int), String) @ 'm(
Map.emptyempty: pure fn!('k, 'v)() -> Map((Int, Int), String)(),
f = fn(editoreditor: MapEditor((Int, Int), String)) {
Map.setset: pure fn!('k, 'v)(editor: MapEditor((Int, Int), String) @ 'm, key: (Int, Int) @ 'm, value: String @ 'm) -> Unit(editoreditor: MapEditor((Int, Int), String), (00: Int, 11: Int), "live""live": String);
},
);
expect ✓cellscells: Map((Int, Int), String)[(00: Int, 11: Int)] == SomeSome: fn(String) -> Option(String)("live""live": String);
expect ✓cellscells: Map((Int, Int), String)[(11: Int, 00: Int)] == NoneNone: Option(String);Persistent updates
Single updates are module functions. Batch updates use a scoped editor and commit automatically when its Unit callback exits.
Persistent Updates
let namesnames: Vector(String) = ["teej""teej": String, "sam""sam": String];
let extendedextended: Vector(String) = Vector.pushpush: pure fn!('a)(items: Vector(String), item: String) -> Vector(String)(namesnames: Vector(String), "drew""drew": String);
expect ✓Vector.lengthlength: pure fn!('a)(items: Vector(String)) -> Int(namesnames: Vector(String)) == 22: Int;
expect ✓Vector.lengthlength: pure fn!('a)(items: Vector(String)) -> Int(extendedextended: Vector(String)) == 33: Int;
let rolesroles: Set(Book.Collections.Permission) = %{ ReadRead: Book.Collections.Permission; Write };
let without_writewithout_write: Set(Book.Collections.Permission) = Set.differencedifference: pure fn!('a)(left: Set(Book.Collections.Permission), right: Set(Book.Collections.Permission)) -> Set(Book.Collections.Permission)(rolesroles: Set(Book.Collections.Permission), %{ Write });
expect ✓Set.containscontains: pure fn!('a)(items: Set(Book.Collections.Permission), item: Book.Collections.Permission) -> Bool(rolesroles: Set(Book.Collections.Permission), Write);
expect ✓Set.containscontains: pure fn!('a)(items: Set(Book.Collections.Permission), item: Book.Collections.Permission) -> Bool(without_writewithout_write: Set(Book.Collections.Permission), Write) == FalseFalse: Bool;
let expandedexpanded: Set(Book.Collections.Permission) = Set.editedit: pure fn!('a)(items: Set(Book.Collections.Permission) @ 'm, ~f: fn(editor: SetEditor(Book.Collections.Permission) @ 'm) -> Unit) -> Set(Book.Collections.Permission) @ 'm(
rolesroles: Set(Book.Collections.Permission),
f = fn(editoreditor: SetEditor(Book.Collections.Permission)) {
Set.addadd: pure fn!('a)(editor: SetEditor(Book.Collections.Permission) @ 'm, item: Book.Collections.Permission @ 'm) -> Unit(editoreditor: SetEditor(Book.Collections.Permission), Admin);
},
);
expect ✓Set.containscontains: pure fn!('a)(items: Set(Book.Collections.Permission), item: Book.Collections.Permission) -> Bool(rolesroles: Set(Book.Collections.Permission), Admin) == FalseFalse: Bool;
expect ✓Set.containscontains: pure fn!('a)(items: Set(Book.Collections.Permission), item: Book.Collections.Permission) -> Bool(expandedexpanded: Set(Book.Collections.Permission), Admin);Map editors follow the same commit rule. Bare map keys are strings; computed keys use brackets.
Map Updates
let field_namefield_name: String = "owner""owner": String;
let labelslabels: Map(String, String) = { idid: String: "todo-1""todo-1": String; [field_namefield_name: String]: "Ada""Ada": String; };
let updatedupdated: Map(String, String) = Map.editedit: pure fn!('k, 'v)(items: Map(String, String) @ 'm, ~f: fn(editor: MapEditor(String, String) @ 'm) -> Unit) -> Map(String, String) @ 'm(
labelslabels: Map(String, String),
f = fn(editoreditor: MapEditor(String, String)) {
Map.setset: pure fn!('k, 'v)(editor: MapEditor(String, String) @ 'm, key: String @ 'm, value: String @ 'm) -> Unit(editoreditor: MapEditor(String, String), "status""status": String, "open""open": String);
},
);
expect ✓Map.getget: pure fn!('k, 'v)(items: Map(String, String), key: String) -> String(labelslabels: Map(String, String), "id""id": String) == "todo-1""todo-1": String;
expect ✓Map.getget: pure fn!('k, 'v)(items: Map(String, String), key: String) -> String(updatedupdated: Map(String, String), "owner""owner": String) == "Ada""Ada": String;
expect ✓Map.getget: pure fn!('k, 'v)(items: Map(String, String), key: String) -> String(updatedupdated: Map(String, String), "status""status": String) == "open""open": String;
expect ✓Map.containscontains: pure fn!('k, 'v)(items: Map(String, String), key: String) -> Bool(labelslabels: Map(String, String), "status""status": String) == FalseFalse: Bool;
expect ✓Map.containscontains: pure fn!('k, 'v)(items: Map(String, String), key: String) -> Bool(updatedupdated: Map(String, String), "status""status": String);Mutable scratch storage
Array changes in place and is appropriate for buffers and local scratch work. Persistent collections remain unchanged unless their returned value is bound.
Mutable Arrays
let scratchscratch: Array(String) = [|"first""first": String, "second""second": String|];
Array.setset: pure fn!('a)(items: Array(String) @ 'm, index: Int, item: String @ 'm) -> Unit(scratchscratch: Array(String), 11: Int, "updated""updated": String);
Array.pushpush: pure fn!('a)(items: Array(String) @ 'm, item: String @ 'm) -> Unit(scratchscratch: Array(String), "third""third": String);
expect ✓scratchscratch: Array(String)[11: Int] == "updated""updated": String;
expect ✓Array.lengthlength: pure fn!('a)(items: Array(String)) -> Int(scratchscratch: Array(String)) == 33: Int;Keyed access
items[key] is Access.get. Each collection chooses its key type and what a read returns. A position into Vector, Array, or Bytes returns the item itself; a position outside the collection is a defect, like any other bug.
Positional Access
let namesnames: Vector(String) = ["teej""teej": String, "sam""sam": String];
let firstfirst: fn(items: Vector(String)) -> String = (itemsitems: Vector(String): Vector(String)) => itemsitems: Vector(String)[00: Int];
✓firstfirst: fn(items: Vector(String)) -> String: fn(Vector(String)) -> String;
expect ✓namesnames: Vector(String)[11: Int] == "sam""sam": String;
expect ✓Access.get(namesnames: Vector(String), 00: Int) == "teej""teej": String;
let scratchscratch: Array(String) = [|"first""first": String, "second""second": String|];
expect ✓scratchscratch: Array(String)[11: Int] == "second""second": String;
let bytesbytes: Bytes = Bytes.of_stringof_string: pure fn!(string: String) -> Bytes("Hi""Hi": String);
expect ✓bytesbytes: Bytes[00: Int] == 'H''H': Byte;A missing map key is ordinary data, so a map read returns an Option.
Map Access
let labelslabels: Map(String, String) = { idid: String: "todo-1""todo-1": String; ownerowner: String: "Ada""Ada": String; };
✓labelslabels: Map(String, String)["id""id": String]: Option(String);
expect ✓labelslabels: Map(String, String)["id""id": String] == SomeSome: fn(String) -> Option(String)("todo-1""todo-1": String);
expect ✓labelslabels: Map(String, String)["status""status": String] == NoneNone: Option(String);?> supplies a fallback for an absent Option. The right side runs only when the left side is None, so it may raise.
Fallback Values
let labelslabels: Map(String, String) = { idid: String: "todo-1""todo-1": String; };
let idid: String = labelslabels: Map(String, String)["id""id": String] ?> "none""none": String;
let ownerowner: String = labelslabels: Map(String, String)["owner""owner": String] ?> "nobody""nobody": String;
expect ✓idid: String == "todo-1""todo-1": String;
expect ✓ownerowner: String == "nobody""nobody": String;
let requirerequire: fn(items: Map(String, String)) -> String, exn [ :unowned ] = (itemsitems: Map(String, String): Map(String, String)) =>
itemsitems: Map(String, String)["owner""owner": String]
?> {
error :unowned;
};
✓requirerequire: fn(items: Map(String, String)) -> String, exn [ :unowned ]: fn(Map(String, String)) -> String, exn [ :unowned ];
expect ✓must requirerequire: fn(items: Map(String, String)) -> String, exn [ :unowned ]({ ownerowner: String: "Ada""Ada": String; }) == "Ada""Ada": String;Option.map transforms a present value and passes None through. It pipes like any other module function.
Mapping Options
let labelslabels: Map(String, String) = { idid: String: "todo-1""todo-1": String; };
✓Option.mapmap: pure fn!('a, 'b)(option: Option(String) @ 'm, ~f: fn(value: String @ 'm) -> Int) -> Option(Int)(labelslabels: Map(String, String)["id""id": String], f = String.byte_lengthbyte_length: pure fn!(string: String) -> Int): Option(Int);
expect ✓Option.mapmap: pure fn!('a, 'b)(option: Option(String) @ 'm, ~f: fn(value: String @ 'm) -> Int) -> Option(Int)(labelslabels: Map(String, String)["id""id": String], f = String.byte_lengthbyte_length: pure fn!(string: String) -> Int) == SomeSome: fn(Int) -> Option(Int)(66: Int);
expect ✓Option.mapmap: pure fn!('a, 'b)(option: Option(String) @ 'm, ~f: fn(value: String @ 'm) -> Int) -> Option(Int)(labelslabels: Map(String, String)["owner""owner": String], f = String.byte_lengthbyte_length: pure fn!(string: String) -> Int) == NoneNone: Option(Int);
expect ✓(labelslabels: Map(String, String)["id""id": String] |> Option.mapmap: pure fn!('a, 'b)(option: Option(String) @ 'm, ~f: fn(value: String @ 'm) -> String) -> Option(String)(f = (idid: String) => idid: String ++ "!""!": String)) == SomeSome: fn(String) -> Option(String)("todo-1!""todo-1!": String);A user collection implements get and chooses its own Value.
module struct Scores {
type t = {
values: Map(String, Int);
}
}
module behavior Access(Scores) {
type Key = String
type Value = Option(Int)
getget: fn(items: Book.Collections.Scores, key: String) -> Option(Int) = (itemsitems: Book.Collections.Scores, keykey: String) => itemsitems: Book.Collections.Scores.valuesvalues: Map(String, Int)[keykey: String];
}
User Collection Access
let scoresscores: Book.Collections.Scores = Scores { values = { adaada: String: 1010: Int; samsam: String: 77: Int; }; };
expect ✓scoresscores: Book.Collections.Scores["ada""ada": String] == SomeSome: fn(Int) -> Option(Int)(1010: Int);
expect ✓(scoresscores: Book.Collections.Scores["drew""drew": String] ?> 00: Int) == 00: Int;Linked lists remain useful when recursive sequence patterns describe the algorithm directly.
List Patterns
let tokenstokens: List(String) = listlist: pure fn!('a)(...String) -> List(String)("open""open": String, "close""close": String);
let firstfirst: Option(String) = match tokenstokens: List(String) {
| [] -> NoneNone: Option(String)
| [headhead: String, .._tail_tail: List(String)] -> Some(headhead: String)
};
expect ✓firstfirst: Option(String) == SomeSome: fn(String) -> Option(String)("open""open": String);