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 behaviorAmbient 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 structwitnessBare
module Order(User)claiming behavior slots (alwaysmodule behavior)Overlapping constructor-wide and specialized canonical equations
Hard-coded
Optionfallback in the compiler (stdlibOrElse(Option)instead)