Copying declared fields
..Source; inside a record body copies the declared fields of Source, in order, at the spread position. The result is one flat nominal record: an Article is not a Timestamps, and nothing at runtime remembers where a field came from.
type Timestamps = {
created_at: Int;
updated_at: Int;
}
type Article = {
id: Int;
..Timestamps;
title: String;
}
let articlearticle: Book.Records.SpreadingRecordTypes.Article = Article {
id = 11: Int;
created_at = 1010: Int;
updated_at = 2020: Int;
title = "Ship""Ship": String;
};
Basic Spread
✓articlearticle: Book.Records.SpreadingRecordTypes.Article.created_atcreated_at: Int: Int;
expect ✓Inspect.inspectinspect: pure fn!('a: Inspect)(value: Book.Records.SpreadingRecordTypes.Article) -> String(articlearticle: Book.Records.SpreadingRecordTypes.Article)
== "Article { id = 1; created_at = 10; updated_at = 20; title = \"Ship\" }""Article { id = 1; created_at = 10; updated_at = 20; title = \"Ship\" }": String;Ordinary fields
Spread fields are written fields in every other respect. Construction, reads, record patterns, and with updates treat them the same way.
let ageage: fn(Book.Records.SpreadingRecordTypes.Article) -> Int = function {
| Article { created_atcreated_at: Int; updated_atupdated_at: Int; ..; } -> updated_atupdated_at: Int - created_atcreated_at: Int
};
Ordinary Fields
let editededited: Book.Records.SpreadingRecordTypes.Article = { articlearticle: Book.Records.SpreadingRecordTypes.Article with updated_at = 3030: Int; title = "Ship 2""Ship 2": String; };
expect ✓editededited: Book.Records.SpreadingRecordTypes.Article == Article {
id = 11: Int;
created_at = 1010: Int;
updated_at = 3030: Int;
title = "Ship 2""Ship 2": String;
};
expect ✓ageage: fn(Book.Records.SpreadingRecordTypes.Article) -> Int(editededited: Book.Records.SpreadingRecordTypes.Article) == 2020: Int;Field metadata
Each copied field keeps its declared type and field metadata, so generated codecs see the same wire names and defaults as on the source record.
type Audit = {
created_by: String #{ Wire.name = "createdBy""createdBy": String; };
revision: Int #{ default = 11: Int; };
}
type Page = {
slug: String;
..Audit;
}
JSON Round Trip
let pagepage: Book.Records.SpreadingRecordTypes.Page = Page { slug = "home""home": String; created_by = "ada""ada": String; revision = 22: Int; };
let jsonjson: String = must String.of_bytesof_bytes: pure fn(bytes: Bytes) -> String, exn [ :utf8(Int) ](Json.encodeencode: pure fn('a: Encode)(value: Book.Records.SpreadingRecordTypes.Page) -> Bytes, exn [ :encode(Wire.EncodeError) ](pagepage: Book.Records.SpreadingRecordTypes.Page));
expect ✓jsonjson: String == "{\"slug\":\"home\",\"createdBy\":\"ada\",\"revision\":2}""{\"slug\":\"home\",\"createdBy\":\"ada\",\"revision\":2}": String;
expect ✓must Json.decodedecode: pure fn('target: Decode)(body: Bytes, target: Struct(Book.Records.SpreadingRecordTypes.Page), ~max_depth: Int = 128) -> Book.Records.SpreadingRecordTypes.Page, exn [ :decode(Wire.DecodeError) ](Bytes.of_stringof_string: pure fn!(string: String) -> Bytes(jsonjson: String), Page) == pagepage: Book.Records.SpreadingRecordTypes.Page;
let defaulteddefaulted: Book.Records.SpreadingRecordTypes.Page = must Json.decodedecode: pure fn('target: Decode)(body: Bytes, target: Struct(Book.Records.SpreadingRecordTypes.Page), ~max_depth: Int = 128) -> Book.Records.SpreadingRecordTypes.Page, exn [ :decode(Wire.DecodeError) ](
Bytes.of_stringof_string: pure fn!(string: String) -> Bytes(
"{\"slug\":\"about\",\"createdBy\":\"grace\"}""{\"slug\":\"about\",\"createdBy\":\"grace\"}": String,
),
Page,
);
expect ✓defaulteddefaulted: Book.Records.SpreadingRecordTypes.Page == Page { slug = "about""about": String; created_by = "grace""grace": String; revision = 11: Int; };Generic sources
A spread names a fully applied record. Applying the source to the declaring record's own parameters keeps the result generic.
type Envelope('a) = {
request_id: Int;
value: 'a;
}
type Attempt('a) = {
..Envelope('a);
attempts: Int;
}
type Labeled = {
..Envelope(String);
}
Generic Spread
let attemptattempt: Book.Records.SpreadingRecordTypes.Attempt(Bool) = Attempt { request_id = 77: Int; value = TrueTrue: Bool; attempts = 11: Int; };
✓attemptattempt: Book.Records.SpreadingRecordTypes.Attempt(Bool): Attempt(Bool);
✓attemptattempt: Book.Records.SpreadingRecordTypes.Attempt(Bool).valuevalue: Bool: Bool;
let labeledlabeled: Book.Records.SpreadingRecordTypes.Labeled = Labeled { request_id = 88: Int; value = "ship""ship": String; };
✓labeledlabeled: Book.Records.SpreadingRecordTypes.Labeled.valuevalue: String: String;Transitive spreads
A spread copies the source's complete field list, including fields that the source itself received from a spread. The source must be declared before the record that spreads it.
type Entity = {
id: Int;
..Timestamps;
}
type Comment = {
..Entity;
body: String;
}
Transitive Spread
let commentcomment: Book.Records.SpreadingRecordTypes.Comment = Comment {
id = 33: Int;
created_at = 11: Int;
updated_at = 22: Int;
body = "hi""hi": String;
};
expect ✓Display.displaydisplay: pure fn!('a: Display)(value: Book.Records.SpreadingRecordTypes.Comment) -> String(commentcomment: Book.Records.SpreadingRecordTypes.Comment)
== "Comment { id = 3; created_at = 1; updated_at = 2; body = hi }""Comment { id = 3; created_at = 1; updated_at = 2; body = hi }": String;Repeated fields
A field may arrive more than once only when every occurrence has the same name, type, modifiers, and metadata. The occurrences merge into one field at the first position. Any other repeat is a compile-time error naming both sources; there is no "later wins" override.
type Owned = {
id: Int;
owner: String;
}
type OwnedComment = {
..Comment;
..Owned;
}
Identical Fields Merge
let ownedowned: Book.Records.SpreadingRecordTypes.OwnedComment = OwnedComment {
id = 44: Int;
created_at = 11: Int;
updated_at = 22: Int;
body = "hi""hi": String;
owner = "ada""ada": String;
};
expect ✓Inspect.inspectinspect: pure fn!('a: Inspect)(value: Book.Records.SpreadingRecordTypes.OwnedComment) -> String(ownedowned: Book.Records.SpreadingRecordTypes.OwnedComment)
== "OwnedComment { id = 4; created_at = 1; updated_at = 2; body = \"hi\"; owner = \"ada\" }""OwnedComment { id = 4; created_at = 1; updated_at = 2; body = \"hi\"; owner = \"ada\" }": String;Sparse and plain records
Sparseness belongs to the declaring record. Spreading into a sparse record makes every copied field omittable, and source defaults do not carry over: omission is data.
type PageChanges = sparse {
..Audit;
}
Sparse Target
let emptyempty: Book.Records.SpreadingRecordTypes.PageChanges = PageChanges {};
✓emptyempty: Book.Records.SpreadingRecordTypes.PageChanges.revisionrevision: Option(Int): Option(Int);
expect ✓must String.of_bytesof_bytes: pure fn(bytes: Bytes) -> String, exn [ :utf8(Int) ](Json.encodeencode: pure fn('a: Encode)(value: Book.Records.SpreadingRecordTypes.PageChanges) -> Bytes, exn [ :encode(Wire.EncodeError) ](emptyempty: Book.Records.SpreadingRecordTypes.PageChanges)) == "{}""{}": String;
expect ✓must Json.decodedecode: pure fn('target: Decode)(body: Bytes, target: Struct(Book.Records.SpreadingRecordTypes.PageChanges), ~max_depth: Int = 128) -> Book.Records.SpreadingRecordTypes.PageChanges, exn [ :decode(Wire.DecodeError) ](Bytes.of_stringof_string: pure fn!(string: String) -> Bytes("{}""{}": String), PageChanges) == emptyempty: Book.Records.SpreadingRecordTypes.PageChanges;
let renamedrenamed: Book.Records.SpreadingRecordTypes.PageChanges = PageChanges { created_by = "grace""grace": String; };
expect ✓must String.of_bytesof_bytes: pure fn(bytes: Bytes) -> String, exn [ :utf8(Int) ](Json.encodeencode: pure fn('a: Encode)(value: Book.Records.SpreadingRecordTypes.PageChanges) -> Bytes, exn [ :encode(Wire.EncodeError) ](renamedrenamed: Book.Records.SpreadingRecordTypes.PageChanges)) == "{\"createdBy\":\"grace\"}""{\"createdBy\":\"grace\"}": String;Spreading a sparse record into a plain record copies its declared field types, so every copied field becomes required.
type Draft = sparse {
title: String;
body: String;
}
type Published = {
..Draft;
published_at: Int;
}
Plain Target
let publishedpublished: Book.Records.SpreadingRecordTypes.Published = Published {
title = "Ship""Ship": String;
body = "...""...": String;
published_at = 55: Int;
};
✓publishedpublished: Book.Records.SpreadingRecordTypes.Published.titletitle: String: String;
expect ✓publishedpublished: Book.Records.SpreadingRecordTypes.Published.titletitle: String == "Ship""Ship": String;Sparse tag payloads
A sparse tag payload spreads a record type by the same rule as a record body: the copied fields join the payload at the spread position, and each one is omittable.
Sparse Tag Payload Spread
let edited: [ :edited sparse { ..Timestamps; by: String } ] =
:edited { updated_at = 20; by = "ada"; };
let written: [
:edited sparse { created_at: Int; updated_at: Int; by: String }
] = edited;
expect written == :edited { by = "ada"; updated_at = 20; };Pending: does not parse yet
spreads in tag payloads are not supported yetTo select some fields instead of all of them, use record projection.