Ship

Book · Behaviors

Behaviors

Draft

Use synthesized behaviors, write canonical and named behavior modules, and track orderings in types.

On this page · 6 sections

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

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

  • !Encode after the type name disables only Encode and keeps the other synthesized behaviors.

  • type! disables automatic synthesis as a category; derive opts back into specific behaviors explicitly.

type DraftTask !Encode = {
  id: TaskId;
  preview_token: String;
}

type! TicketHandle = {
  id: Int;
}

derive Inspect(TicketHandle);

Synthesis Opt-Outs

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

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

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

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

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