Omitted fields
sparse makes every field omittable during construction. A read sees whether the field was supplied, so each stored field is wrapped in Option.
type UserUpdate = sparse {
name: String;
display_name: Option(String);
avatar_url: String;
}
A field that is already an Option gains a second layer. The outer layer distinguishes an omitted update from an update that explicitly clears the value.
let describe_displaydescribe_display: fn(Book.Records.SparseRecords.UserUpdate) -> String = function {
| UserUpdate { display_namedisplay_name: Option(Option(String)); ..; } ->
match display_namedisplay_name: Option(Option(String)) {
| None -> "omitted""omitted": String
| Some(None) -> "cleared""cleared": String
| Some(Some(valuevalue: String)) -> "set:""set:": String ++ valuevalue: String
}
};
Sparse Records
let emptyempty: Book.Records.SparseRecords.UserUpdate = UserUpdate {};
✓emptyempty: Book.Records.SparseRecords.UserUpdate.namename: Option(String): Option(String);
✓emptyempty: Book.Records.SparseRecords.UserUpdate.display_namedisplay_name: Option(Option(String)): Option(Option(String));
expect ✓emptyempty: Book.Records.SparseRecords.UserUpdate.namename: Option(String) == NoneNone: Option(String);
let renamerename: Book.Records.SparseRecords.UserUpdate = UserUpdate { name = "Ada""Ada": String; };
expect ✓renamerename: Book.Records.SparseRecords.UserUpdate.namename: Option(String) == SomeSome: fn(String) -> Option(String)("Ada""Ada": String);
let clear_displayclear_display: Book.Records.SparseRecords.UserUpdate = UserUpdate { display_name = None; };
let set_displayset_display: Book.Records.SparseRecords.UserUpdate = UserUpdate { display_name = Some("Countess""Countess": String); };
✓describe_displaydescribe_display: fn(Book.Records.SparseRecords.UserUpdate) -> String: fn(UserUpdate) -> String;
expect ✓describe_displaydescribe_display: fn(Book.Records.SparseRecords.UserUpdate) -> String(emptyempty: Book.Records.SparseRecords.UserUpdate) == "omitted""omitted": String;
expect ✓describe_displaydescribe_display: fn(Book.Records.SparseRecords.UserUpdate) -> String(clear_displayclear_display: Book.Records.SparseRecords.UserUpdate) == "cleared""cleared": String;
expect ✓describe_displaydescribe_display: fn(Book.Records.SparseRecords.UserUpdate) -> String(set_displayset_display: Book.Records.SparseRecords.UserUpdate) == "set:Countess""set:Countess": String;
let with_avatarwith_avatar: Book.Records.SparseRecords.UserUpdate = { renamerename: Book.Records.SparseRecords.UserUpdate with avatar_url = "ada.png""ada.png": String; };
expect ✓with_avatarwith_avatar: Book.Records.SparseRecords.UserUpdate.avatar_urlavatar_url: Option(String) == SomeSome: fn(String) -> Option(String)("ada.png""ada.png": String);Unknown field names and wrong field types are rejected, the same as on an ordinary record.
Sparse encoding
Default JSON encoding follows the sparse constructor shape: fields stored as None are omitted. Decode reads that same shape: missing keys stay omitted.
Sparse Encoding
let empty_jsonempty_json: String = must String.of_bytesof_bytes: pure fn(bytes: Bytes) -> String, exn [ :utf8(Int) ](Json.encodeencode: pure fn('a: Encode)(value: Book.Records.SparseRecords.UserUpdate) -> Bytes, exn [ :encode(Wire.EncodeError) ](UserUpdate {}));
expect ✓empty_jsonempty_json: String == "{}""{}": String;
expect ✓must Json.decodedecode: pure fn('target: Decode)(body: Bytes, target: Struct(Book.Records.SparseRecords.UserUpdate), ~max_depth: Int = 128) -> Book.Records.SparseRecords.UserUpdate, exn [ :decode(Wire.DecodeError) ](Bytes.of_stringof_string: pure fn!(string: String) -> Bytes(empty_jsonempty_json: String), UserUpdate) == UserUpdate {};
let rename_jsonrename_json: String = must String.of_bytesof_bytes: pure fn(bytes: Bytes) -> String, exn [ :utf8(Int) ](Json.encodeencode: pure fn('a: Encode)(value: Book.Records.SparseRecords.UserUpdate) -> Bytes, exn [ :encode(Wire.EncodeError) ](UserUpdate { name = "Ada""Ada": String; }));
expect ✓rename_jsonrename_json: String == "{\"name\":\"Ada\"}""{\"name\":\"Ada\"}": String;
expect ✓must Json.decodedecode: pure fn('target: Decode)(body: Bytes, target: Struct(Book.Records.SparseRecords.UserUpdate), ~max_depth: Int = 128) -> Book.Records.SparseRecords.UserUpdate, exn [ :decode(Wire.DecodeError) ](Bytes.of_stringof_string: pure fn!(string: String) -> Bytes(rename_jsonrename_json: String), UserUpdate) == UserUpdate { name = "Ada""Ada": String; };A JSON null on an already-optional field is a clear: the field was supplied, and the stored value is None.
Sparse tag payloads
A tag in an open variant can carry a sparse record payload. Construction may omit any field, and a bare tag is the payload with every field omitted: :checkbox and :checkbox {} are the same value.
type InputKind = [
| :checkbox sparse { checked: Bool; label: String }
| :number sparse { value: Int; min: Int; max: Int }
]
Reads see each field as an Option, as on a sparse record. A bare tag pattern matches its tag whichever fields were supplied.
let describe_inputdescribe_input: fn(input: Book.Records.SparseRecords.InputKind) -> String = fn(inputinput: Book.Records.SparseRecords.InputKind: InputKind) -> String {
match inputinput: Book.Records.SparseRecords.InputKind {
| :checkbox { checked = Some(True); ..; } -> "checked""checked": String
| :checkbox -> "checkbox""checkbox": String
| :number { min = Some(_); max = Some(_); ..; } -> "bounded""bounded": String
| :number -> "number""number": String
}
};
Sparse Tag Payloads
let checkedchecked: Book.Records.SparseRecords.InputKind: InputKind = :checkbox { checked = TrueTrue: Bool; };
let barebare: Book.Records.SparseRecords.InputKind: InputKind = :checkbox;
let emptyempty: Book.Records.SparseRecords.InputKind: InputKind = :checkbox {};
expect ✓barebare: Book.Records.SparseRecords.InputKind == emptyempty: Book.Records.SparseRecords.InputKind;
expect ✓describe_inputdescribe_input: fn(input: Book.Records.SparseRecords.InputKind) -> String(checkedchecked: Book.Records.SparseRecords.InputKind) == "checked""checked": String;
expect ✓describe_inputdescribe_input: fn(input: Book.Records.SparseRecords.InputKind) -> String(barebare: Book.Records.SparseRecords.InputKind) == "checkbox""checkbox": String;
expect ✓describe_inputdescribe_input: fn(input: Book.Records.SparseRecords.InputKind) -> String(:number { min = 00: Int; max = 1010: Int; }) == "bounded""bounded": String;
expect ✓describe_inputdescribe_input: fn(input: Book.Records.SparseRecords.InputKind) -> String(:number { value = 55: Int; }) == "number""number": String;JSON encoding omits omitted fields, the same as a sparse record.
Sparse Tag Encoding
let barebare: Book.Records.SparseRecords.InputKind: InputKind = :checkbox;
expect ✓must String.of_bytesof_bytes: pure fn(bytes: Bytes) -> String, exn [ :utf8(Int) ](Json.encodeencode: pure fn('a: Encode)(value: Book.Records.SparseRecords.InputKind) -> Bytes, exn [ :encode(Wire.EncodeError) ](barebare: Book.Records.SparseRecords.InputKind)) == "{\":checkbox\":{}}""{\":checkbox\":{}}": String;
let checkedchecked: Book.Records.SparseRecords.InputKind: InputKind = :checkbox { checked = TrueTrue: Bool; };
expect ✓must String.of_bytesof_bytes: pure fn(bytes: Bytes) -> String, exn [ :utf8(Int) ](Json.encodeencode: pure fn('a: Encode)(value: Book.Records.SparseRecords.InputKind) -> Bytes, exn [ :encode(Wire.EncodeError) ](checkedchecked: Book.Records.SparseRecords.InputKind)) == "{\":checkbox\":{\"checked\":true}}""{\":checkbox\":{\"checked\":true}}": String;Display and inspection show only the supplied fields.
A payload field that is already an Option gains a second layer, so an omitted field and a cleared one stay distinct.
Aliases that include the tag keep its payload sparse.
type Toggle('msg) = [
| :toggle sparse { on: Bool; changed: pure fn(Bool) -> 'msg }
]
type Control('msg) = [
| Toggle('msg)
| :button(String)
]
Sparse Tag Composition
let onon: fn(control: Book.Records.SparseRecords.Control(String)) -> Option(Bool) = fn(controlcontrol: Book.Records.SparseRecords.Control(String): Control(String)) -> Option(Bool) {
match controlcontrol: Book.Records.SparseRecords.Control(String) {
| :toggle { onon: Option(Bool); ..; } -> onon: Option(Bool)
| :button(_) -> None
}
};
let toggletoggle: Book.Records.SparseRecords.Control(String): Control(String) = :toggle { on = TrueTrue: Bool; };
let barebare: Book.Records.SparseRecords.Control(String): Control(String) = :toggle;
expect ✓onon: fn(control: Book.Records.SparseRecords.Control(String)) -> Option(Bool)(toggletoggle: Book.Records.SparseRecords.Control(String)) == SomeSome: fn(Bool) -> Option(Bool)(TrueTrue: Bool);
expect ✓onon: fn(control: Book.Records.SparseRecords.Control(String)) -> Option(Bool)(barebare: Book.Records.SparseRecords.Control(String)) == NoneNone: Option(Bool);