Everyday data works out of the box
Task now carries a numeric priority alongside its id and title. Because TaskId and Task are concrete data types, Ship synthesizes Display, Inspect, Encode, and Decode automatically—no #[derive(...)] boilerplate on every type in the tracker. Notice how TaskId formats flat in Display and JSON, while Inspect keeps the TaskId(...) constructor:
type TaskId = new String
type Task = {
id: TaskId;
title: String;
priority: Int;
}
let trackertracker: Book.YouTube.Behaviors.Task = Task {
id = TaskId("task-tracker""task-tracker": String);
title = "Build the task tracker""Build the task tracker": String;
priority = 11: Int;
};
let walkthroughwalkthrough: Book.YouTube.Behaviors.Task = Task {
id = TaskId("youtube-walkthrough""youtube-walkthrough": String);
title = "Write the YouTube walkthrough""Write the YouTube walkthrough": String;
priority = 22: Int;
};
Synthesized Behaviors
expect ✓Display.displaydisplay: pure fn!('a: Display)(value: Book.YouTube.Behaviors.Task) -> String(trackertracker: Book.YouTube.Behaviors.Task)
== "Task { id = task-tracker; title = Build the task tracker; priority = 1 }""Task { id = task-tracker; title = Build the task tracker; priority = 1 }": String;
expect ✓Inspect.inspectinspect: pure fn!('a: Inspect)(value: Book.YouTube.Behaviors.Task) -> String(trackertracker: Book.YouTube.Behaviors.Task)
== "Task { id = TaskId(\"task-tracker\"); title = \"Build the task tracker\"; priority = 1 }""Task { id = TaskId(\"task-tracker\"); title = \"Build the task tracker\"; priority = 1 }": String;
let encodedencoded: Bytes = must Json.encodeencode: pure fn('a: Encode)(value: Book.YouTube.Behaviors.Task) -> Bytes, exn [ :encode(Wire.EncodeError) ](trackertracker: Book.YouTube.Behaviors.Task);
expect ✓must String.of_bytesof_bytes: pure fn(bytes: Bytes) -> String, exn [ :utf8(Int) ](encodedencoded: Bytes)
== "{\"id\":\"task-tracker\",\"title\":\"Build the task tracker\",\"priority\":1}""{\"id\":\"task-tracker\",\"title\":\"Build the task tracker\",\"priority\":1}": String;
let decodeddecoded: Book.YouTube.Behaviors.Task: Task = must Decode.decode(Json.Reader.newnew: pure fn!(body: Bytes, ~max_depth: Int = 128) -> Json.Reader(encodedencoded: Bytes));
expect ✓decodeddecoded: Book.YouTube.Behaviors.Task == trackertracker: Book.YouTube.Behaviors.Task;Opting out of synthesis
When a type holds a secret or an internal handle, you opt out right on the type declaration:
!Encodeafter the type name disables onlyEncodeand keeps the other synthesized behaviors.type!disables automatic synthesis as a category;deriveopts back into specific behaviors explicitly.
type DraftTask !Encode = {
id: TaskId;
preview_token: String;
}
type! TicketHandle = {
id: Int;
}
derive Inspect(TicketHandle);
Synthesis Opt-Outs
let draftdraft: Book.YouTube.Behaviors.DraftTask = DraftTask {
id = TaskId("auth""auth": String);
preview_token = "tok_live_123""tok_live_123": String;
};
expect ✓Display.displaydisplay: pure fn!('a: Display)(value: Book.YouTube.Behaviors.DraftTask) -> String(draftdraft: Book.YouTube.Behaviors.DraftTask) == "DraftTask { id = auth; preview_token = tok_live_123 }""DraftTask { id = auth; preview_token = tok_live_123 }": String;
let handlehandle: Book.YouTube.Behaviors.TicketHandle = TicketHandle { id = 4242: Int; };
expect ✓Inspect.inspectinspect: pure fn!('a: Inspect)(value: Book.YouTube.Behaviors.TicketHandle) -> String(handlehandle: Book.YouTube.Behaviors.TicketHandle) == "TicketHandle { id = 42 }""TicketHandle { id = 42 }": String;Defining a behavior module
A behavior interface is a module signature parameterized by struct 'a. Rank requires a compare operation, while Summary declares a required priority_number operation and a default badge helper built on top of it:
module behavior Rank(struct 'a) {
compare: pure fn(left: 'a, right: 'a) -> Int;
}
module behavior Summary(struct 'a) {
priority_number: pure fn(item: 'a) -> Int;
let badgebadge: fn(item: 'a) -> String = (itemitem: 'a) => "P""P": String ++ Int.to_stringto_string: pure fn!(value: Int) -> String(priority_numberpriority_number: pure fn(item: 'a) -> Int(itemitem: 'a));
}
Canonical implementations
Each (Behavior, Target) pair can have at most one unnamed canonical implementation. Summary(Task) supplies priority_number and inherits badge, while Rank(Task) orders higher-priority tasks first:
module behavior Summary(Task) {
priority_numberpriority_number: fn(task: Book.YouTube.Behaviors.Task) -> Int = (tasktask: Book.YouTube.Behaviors.Task: Task) => tasktask: Book.YouTube.Behaviors.Task.prioritypriority: Int;
}
module behavior Rank(Task) {
comparecompare: fn(left: Book.YouTube.Behaviors.Task, right: Book.YouTube.Behaviors.Task) -> Int = (leftleft: Book.YouTube.Behaviors.Task: Task, rightright: Book.YouTube.Behaviors.Task: Task) => rightright: Book.YouTube.Behaviors.Task.prioritypriority: Int - leftleft: Book.YouTube.Behaviors.Task.prioritypriority: Int;
}
A function requests a behavior with ~rank = Rank(struct 'a). When callers omit rank, Ship supplies the canonical Rank(Task) from the argument type:
let pick_firstpick_first: fn(struct 'a)(left: 'a, right: 'a, ~rank: Book.YouTube.Behaviors.Rank('a) = Rank(struct 'a)) -> 'a = fn(struct 'a)(leftleft: 'a: 'a, rightright: 'a: 'a, ~rankrank: Book.YouTube.Behaviors.Rank('a) = Rank(struct 'a)) {
if rankrank: Book.YouTube.Behaviors.Rank('a).comparecompare: pure fn(left: 'a, right: 'a) -> Int(leftleft: 'a, rightright: 'a) <= 00: Int {
| leftleft: 'a
| rightright: 'a
}
};
Canonical Behavior
expect ✓Summary.badge(trackertracker: Book.YouTube.Behaviors.Task) == "P1""P1": String;
expect ✓Summary.badge(walkthroughwalkthrough: Book.YouTube.Behaviors.Task) == "P2""P2": String;
expect ✓pick_firstpick_first: fn(struct 'a)(left: Book.YouTube.Behaviors.Task, right: Book.YouTube.Behaviors.Task, ~rank: Book.YouTube.Behaviors.Rank(Book.YouTube.Behaviors.Task) = Rank(struct 'a)) -> Book.YouTube.Behaviors.Task(trackertracker: Book.YouTube.Behaviors.Task, walkthroughwalkthrough: Book.YouTube.Behaviors.Task) == walkthroughwalkthrough: Book.YouTube.Behaviors.Task;Named alternatives and adapters
What if one view wants to rank tasks alphabetically by title, or invert an existing ranking? Because behaviors are modules, you don't wrap Task in a newtype. You write a named behavior module conforming to Rank(Task) and pass it as rank = ByTitle, or pass a one-off module #{ ... } inline:
module behavior ByTitle: Rank(Task) {
comparecompare: fn(left: Book.YouTube.Behaviors.Task, right: Book.YouTube.Behaviors.Task) -> Int = (leftleft: Book.YouTube.Behaviors.Task: Task, rightright: Book.YouTube.Behaviors.Task: Task) =>
String.comparecompare: pure fn!(left: String, right: String) -> Int(leftleft: Book.YouTube.Behaviors.Task.titletitle: String, rightright: Book.YouTube.Behaviors.Task.titletitle: String);
}
Named Behavior
expect ✓pick_firstpick_first: fn(struct 'a)(left: Book.YouTube.Behaviors.Task, right: Book.YouTube.Behaviors.Task, ~rank: Book.YouTube.Behaviors.Rank(Book.YouTube.Behaviors.Task) = Rank(struct 'a)) -> Book.YouTube.Behaviors.Task(trackertracker: Book.YouTube.Behaviors.Task, walkthroughwalkthrough: Book.YouTube.Behaviors.Task, rank = ByTitleByTitle: Book.YouTube.Behaviors.Rank(Book.YouTube.Behaviors.Task)) == trackertracker: Book.YouTube.Behaviors.Task;
let reverse_titlereverse_title: Book.YouTube.Behaviors.Rank(Book.YouTube.Behaviors.Task): Rank(Task) = module #{
compare = (leftleft: Book.YouTube.Behaviors.Task: Task, rightright: Book.YouTube.Behaviors.Task: Task) =>
00: Int - String.comparecompare: pure fn!(left: String, right: String) -> Int(leftleft: Book.YouTube.Behaviors.Task.titletitle: String, rightright: Book.YouTube.Behaviors.Task.titletitle: String);
};
expect ✓pick_firstpick_first: fn(struct 'a)(left: Book.YouTube.Behaviors.Task, right: Book.YouTube.Behaviors.Task, ~rank: Book.YouTube.Behaviors.Rank(Book.YouTube.Behaviors.Task) = Rank(struct 'a)) -> Book.YouTube.Behaviors.Task(trackertracker: Book.YouTube.Behaviors.Task, walkthroughwalkthrough: Book.YouTube.Behaviors.Task, rank = reverse_titlereverse_title: Book.YouTube.Behaviors.Rank(Book.YouTube.Behaviors.Task)) == walkthroughwalkthrough: Book.YouTube.Behaviors.Task;A behavior adapter is a functor from one behavior module to another. Reverse flips any Rank(struct 'a):
module behavior Reverse(struct 'a)(Base: Rank(struct 'a)): Rank(struct 'a) {
comparecompare: fn(left: 'a, right: 'a) -> Int = (leftleft: 'a: 'a, rightright: 'a: 'a) => Base.comparecompare: pure fn(left: 'a, right: 'a) -> Int(rightright: 'a, leftleft: 'a);
}
Behavior Adapter
expect ✓pick_firstpick_first: fn(struct 'a)(left: Book.YouTube.Behaviors.Task, right: Book.YouTube.Behaviors.Task, ~rank: Book.YouTube.Behaviors.Rank(Book.YouTube.Behaviors.Task) = Rank(struct 'a)) -> Book.YouTube.Behaviors.Task(trackertracker: Book.YouTube.Behaviors.Task, walkthroughwalkthrough: Book.YouTube.Behaviors.Task, rank = Reverse(ByTitleByTitle: Book.YouTube.Behaviors.Rank(Book.YouTube.Behaviors.Task))) == walkthroughwalkthrough: Book.YouTube.Behaviors.Task;
expect ✓pick_firstpick_first: fn(struct 'a)(left: Book.YouTube.Behaviors.Task, right: Book.YouTube.Behaviors.Task, ~rank: Book.YouTube.Behaviors.Rank(Book.YouTube.Behaviors.Task) = Rank(struct 'a)) -> Book.YouTube.Behaviors.Task(trackertracker: Book.YouTube.Behaviors.Task, walkthroughwalkthrough: Book.YouTube.Behaviors.Task, rank = Reverse(Rank(Task))) == trackertracker: Book.YouTube.Behaviors.Task;Behavior witnesses in types
Passing a closure comparator at runtime loses track of which ordering built a data structure. In Ship, a type can parameterize over a behavior witness (order: Rank(Task)), recording the behavior module's identity in the type itself so values with different invariants cannot silently mix:
type! RankedTasks(order: Rank(Task)) = {
tasks: Vector(Task);
}
Behavior Witness In Types
let taskstasks: Vector(Book.YouTube.Behaviors.Task) = [walkthroughwalkthrough: Book.YouTube.Behaviors.Task, trackertracker: Book.YouTube.Behaviors.Task];
let by_priorityby_priority: Book.YouTube.Behaviors.RankedTasks(Book.YouTube.Behaviors.Rank(Book.YouTube.Behaviors.Task)): RankedTasks(Rank(Task)) = RankedTasks { tasks = taskstasks: Vector(Book.YouTube.Behaviors.Task); };
✓by_priorityby_priority: Book.YouTube.Behaviors.RankedTasks(Book.YouTube.Behaviors.Rank(Book.YouTube.Behaviors.Task)): RankedTasks(Rank(Task));
let title_counttitle_count: fn(ranked: Book.YouTube.Behaviors.RankedTasks(Book.YouTube.Behaviors.ByTitle)) -> Int = fn(rankedranked: Book.YouTube.Behaviors.RankedTasks(Book.YouTube.Behaviors.ByTitle): RankedTasks(ByTitle)) {
Vector.lengthlength: pure fn!('a)(items: Vector(Book.YouTube.Behaviors.Task)) -> Int(rankedranked: Book.YouTube.Behaviors.RankedTasks(Book.YouTube.Behaviors.ByTitle).taskstasks: Vector(Book.YouTube.Behaviors.Task))
};
✓title_counttitle_count: fn(ranked: Book.YouTube.Behaviors.RankedTasks(Book.YouTube.Behaviors.ByTitle)) -> Int: pure fn!(RankedTasks(ByTitle)) -> Int;
let by_titleby_title: Book.YouTube.Behaviors.RankedTasks(Book.YouTube.Behaviors.ByTitle): RankedTasks(ByTitle) = RankedTasks { tasks = taskstasks: Vector(Book.YouTube.Behaviors.Task); };
expect ✓title_counttitle_count: fn(ranked: Book.YouTube.Behaviors.RankedTasks(Book.YouTube.Behaviors.ByTitle)) -> Int(by_titleby_title: Book.YouTube.Behaviors.RankedTasks(Book.YouTube.Behaviors.ByTitle)) == 22: Int;
let reversedreversed: Book.YouTube.Behaviors.RankedTasks(Reverse(ByTitle)): RankedTasks(Reverse(ByTitle)) = RankedTasks { tasks = taskstasks: Vector(Book.YouTube.Behaviors.Task); };
✓reversedreversed: Book.YouTube.Behaviors.RankedTasks(Reverse(ByTitle)): RankedTasks(Reverse(ByTitle));
expect ✓Vector.lengthlength: pure fn!('a)(items: Vector(Book.YouTube.Behaviors.Task)) -> Int(reversedreversed: Book.YouTube.Behaviors.RankedTasks(Reverse(ByTitle)).taskstasks: Vector(Book.YouTube.Behaviors.Task)) == 22: Int;