Ship

Old Docs · Behavior Modules And Canonical Lookup
On this page · 8 sections

Behavior Modules And Canonical Lookup

Reusable value behavior — ordering, hashing, encode/decode, inspect, defaults, DB codecs — uses module behavior with nominal, unique canonical lookup. Non-canonical alternatives are explicit and named. Structurally compatible modules may be passed only when the call site names them. Behavior-sensitive collections carry the module itself as their witness.

module behavior T(struct 'a) { ... }            // interface
module behavior Order(User) { ... }             // canonical
module behavior UserByName: Order(User) { ... } // named non-canonical
let o = module behavior: Order(User) #{ ... };  // anonymous (fresh identity)

Plain module never implements a behavior interface, but may still be passed explicitly where an API names a behavior parameter. This rejects Rust-style method dispatch, ambient instance search, and OCaml with type boilerplate for ordinary app code. Functors stay a separate tool (15-modules.md). Executable canonical, named, anonymous, adapter, and witness examples live in Behavior Modules and Witnesses.

Interfaces

Primary behavior is T, parallel to type t on struct modules. The file name is the public behavior name (Order.shc → Order):

module behavior T(struct 'a) {
  compare: pure fn!(left: 'a, right: 'a) -> Ordering;
}

In behavior positions, Order and Order.T normalize to the same interface. This is the same primary-interface rule as module type T in Store.shp; struct 'a binds the primary type of a module struct witness — no with type t = a. Multi-witness and associated outputs are allowed:

module behavior T(struct 'from, struct 'to) {
  convert: pure fn!(value: 'from) -> 'to;
}

module behavior T(struct 'source) {
  type Output;
  convert: pure fn!(value: 'source) -> Output;
}

Unit-indexed behaviors use unit 'from binders; see measured numerics in 01-syntax.md. module type signatures do not participate in canonical lookup.

Stdlib behaviors live in their own modules (Order.shc, Display.shc, Encode.shc, …) — no central Behavior.shc bucket.

Canonical

At most one canonical Order(User) per program. Lookup consults that nominal slot only — never ambient structural search:

module behavior Order(User) {
  compare = fn(left: User, right: User) -> Ordering {
    return UserId.compare(left.id, right.id);
  };
}

Constructor-wide families claim every pointwise slot (OrElse(Option) covers OrElse(Option(T)) for every T); specialized overlaps are rejected. Aliases normalize before coherence checks. Binary slots use the full witness tuple: Convert(User, PublicUser).

Canonical ownership: behavior owner, struct-module owner, or automatic synthesis (Encode, Decode, Inspect, Display). A project canonical module may replace a checked Core default; other alternatives are named modules passed explicitly.

Heterogeneous behavior packs use uppercase tick variables:

pure fn!('A: Display)(
  ...values: 'A[],
  ~sep: String = " ",
  ~end: String = "\n",
) -> Unit

Plain namespace modules need a nested module struct before they can receive behavior (Default(Http.Method), not Default(Http)).

Defaults

module behavior T(struct 'a) {
  default: pure fn!() -> 'a;
}

let default = fn(struct 'a)(S = struct 'a) -> 'a {
  return Default(S).default();
}

let a = default(User);
let b: User = default();

default() without expected type or context is an error. Default does not yet imply parameter omission or = default syntax.

Associated Outputs And OrElse

Associated outputs are determined by the source-witness tuple; they are not extra lookup inputs. Standard first use:

module behavior T(struct 'source) {
  type Output;
  resolve: pure fn!(scope 'scope)(
    ~source: Code('source, 'scope),
    ~fallback: Code(Output, 'scope) @ once,
  ) -> Code(Output, 'scope);
}

module behavior OrElse(Option) {
  type Output('a) = 'a;
  resolve = fn(~source, ~fallback) {
    return #code(
      match #insert(source) {
        | Some(value) -> value
        | None -> #insert(fallback)
      }
    );
  };
}

let session = Cookie.read(request, "quotes_session") ?> error :unauthorized;

Lookup never chooses an implementation by looking backward from the fallback. ?> laws (source once, fallback 0/1, no incompatible repetition) are part of the operator contract. Other optional-like constructors define their own OrElse families rather than new syntax.

Explicit Structural Passing

module RandomThing {
  let compare = fn(left: User, right: User) -> Ordering { ... };
}

Sort.sort(users, order = RandomThing);  // OK — role named at call site
Sort.sort(users);                       // RandomThing is never an ambient candidate

Same structural rule checks functor arguments (15-modules.md).

Alternate Views

Prefer distinct view types (PublicUser) with their own canonical behavior. Use an explicit module parameter only when the API is intentionally parameterized. Ship has no broad call-site override language for swapping arbitrary canonical behavior.

Include And Constraints

include is module composition, not behavior resolution — it does not create canonical slots or ambient candidates.

let cache_put = fn(struct 'a: Encode)(
  cache: Cache,
  key: String,
  value: 'a,
) -> Unit { ... };

'a: Encode threads hidden canonical evidence after type inference. .shp may infer nominal constraints from bodies; .shc exports must write the inline constraint or a named module parameter explicitly. No structural body search for constraints.

Default synthesis, selective opt-outs, type!, manual implementations, and late derive are specified by the executable Behavior Synthesis chapter.

Rejects

  • Rust impl / method dispatch for behavior

  • Ambient structural instance search or priority selection

  • Local implicit shadowing of which behavior a call uses

  • Comparing module bodies for semantic equality

  • Alternate views competing with canonical Encode(User)

  • Behavior on plain namespace modules without a module struct witness

  • Bare module Order(User) claiming behavior slots (always module behavior)

  • Overlapping constructor-wide and specialized canonical equations

  • Hard-coded Option fallback in the compiler (stdlib OrElse(Option) instead)