Ship

Book · Collections

Collections

Draft

Construct and update Ship's persistent and mutable collection families.

On this page · 4 sections

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

test✓
  ✓[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

test✓
  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

test✓
  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

test✓
  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

test✓
  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

test✓
  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

test✓
  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

test✓
  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

test✓
  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

test✓
  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

test✓
  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

test✓
  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);