Module Signatures And Functors
Ship modules come in a small set of explicit forms (see 01-syntax.md for the file/folder rules):
module Name { ... }— namespace module, notype t.module struct Name { type t = ...; ... }— primary-type module; the module name is the type name and the struct witness.module behavior ...— behavior interfaces and behavior implementations (11-behavior-modules.md).module type Name { ... }— a signature: the shape a module must provide.module type Tis the file module's primary interface (Store.shpexposes it asStore).Functors — modules parameterized by other modules, applicative by default.
module Name = Path;/module Name = Functor(Args);— module aliases and applications.Every form is a keyword-distinct header. The parser knows what kind of module declaration it is looking at before name resolution, so diagnostics can speak in terms of the pattern (
this signature requires a struct module member) instead of post-hoc convention.
Signatures
A module type declares the surface a module must provide. Signature members are declarations, not definitions:
module type Store {
module struct Key where Key: HashKey;
module struct Value;
get: pure fn(key: Key) -> Value, exn [ :not_found(Key) ];
put: pure fn!(key: Key, value: Value) -> Unit;
}
As with behavior interfaces, T is the primary interface name. In Store.shp, module type T { ... } is referenced as either Store or Store.T; the two spellings normalize to one interface identity. Other names remain nested signatures (Store.ReadOnly).
Signature members may be:
Value members —
name: Type;, the same header shape.shccontracts use.Struct-module members —
module struct Name;requires a primary-type module. The member name is the member's primary type inside the signature (Key,Valueabove). Nowith typeequalities: referring to the member is referring to its type.Pinned struct members —
module struct Name = Path;requires the member to be exactly that module (module struct Key = UserId;).Abstract member types —
type Name;for secondary types whose representation stays hidden.Concrete member types —
type Name = ...;when the shape is part of the contract.Primary type —
type t;(abstract) ortype t = ...;(concrete). A signature with atype tmember describes a struct module, mirroring how files infer their form.Nested module members —
module Name: Sig;.Behavior constraints — a trailing
whereon a struct-module member requires canonical behavior evidence for that member (module struct Key where Key: HashKey;). Combine with+as usual.Signature composition —
include OtherSig;copies another signature's requirements into a named signature. An anonymous file signature may keep several requirements inline. Every included signature is checked independently against the same module, so their abstracttype tmembers name the same implementation type. Duplicate includes are errors; incompatible concrete types or operation requirements cannot be satisfied. There is no OCaml-style destructive substitution (:=).Signatures are not behavior interfaces. A
module behavior T(struct 'a)declaration is indexed by struct witnesses and participates in canonical lookup; amodule typeis only a shape. Usemodule typefor functor parameters and sealing; usemodule behaviorfor reusable value behavior.
File Module Ascription
An importable .shp file may use module: Signature; as its first non-comment source line. The filename still supplies the implementation identity; the header checks the inferred file module against the reusable signature and seals its public surface. The ordinary shape is one Store.shp containing module type T, followed by implementations such as Stores/Flat.shp and Stores/JSON.shp beginning with module: Store;. It is an alternative to a companion .shc on each implementation, not a signature declared inside every implementation. A file that needs several interfaces may keep one header and compose them anonymously:
module: #{
include Store;
include Snapshot;
}
The anonymous signature has no reusable identity; it is the conjunction and public union of its included signatures for this file only. Executable examples live in Module Signatures, backed by the linked examples/book/store-modules/ project.
Module Values And Function Results
An expected module slot accepts named modules and anonymous module expressions. Ordinary functions may take ordinary values and return modules:
let clock_for_test = fn(test: Test.Case) -> Service.Provider(Clock) {
return module: Service.Provider(Clock) #{
let service Provider: Clock =
FixedClock(test.started_at);
};
};
providing Clock = Test.Service.per_test(clock_for_test);
The Test.Case argument is an ordinary value, not a module witness, and this is an ordinary function rather than a functor. The anonymous module receives one fresh compiler identity at its source expression; each call creates a module record at runtime. Resource freshness belongs to the returned service provider's init / drop, not module identity.
A named module value satisfies the same expected result signature:
module StableClockProvider {
let service Provider: Clock = StableClock;
}
let stable_clock_for_suite = fn(_suite: Test.Suite) -> Service.Provider(Clock) {
return StableClockProvider;
};
Expected module result signatures provide contextual packing: private associated types such as provider state remain hidden when the result is consumed immediately. This does not create a public existential identity. Public witnesses, anonymous local modules, and explicit runtime alternatives are executable in Behavior Modules and Witnesses.
Functors
A functor is a module with one or more module parameter lists. Application produces a module:
module type Cache {
type t;
module struct Key;
module struct Value;
empty: pure fn!() -> t;
get: pure fn(cache: t, key: Key) -> Value, exn [ :not_found(Key) ];
put: pure fn!(cache: t, key: Key, value: Value) -> t;
}
module struct MakeCache(S: Store): Cache {
type t = {
entries: Map(S.Key, S.Value);
}
module struct Key = S.Key;
module struct Value = S.Value;
let empty = fn() -> t {
return { entries = Map.empty() };
};
let get = fn(cache: t, key: S.Key) -> S.Value {
return Map.get(cache.entries, key);
};
let put = fn(cache: t, key: S.Key, value: S.Value) -> t {
let entries = Map.edit(cache.entries, f = fn(editor) {
Map.set(editor, key, value);
});
return { cache with entries = entries };
};
}
module UserCache = MakeCache(UserStore);
let cache = UserCache.put(UserCache.empty(), user.id, user);
Header rules:
The module form keyword is part of the functor header. A functor whose result declares
type tismodule struct Name(...); a namespace result ismodule Name(...); a behavior implementation result ismodule behavior Name(...)(11-behavior-modules.md).A parameter list contains one or more
Name: Sigmodule parameters. Multiple lists are allowed and applications must supply every list — there is no partial application.A leading
(struct 'a)list binds generic struct witnesses, exactly like function struct binders. Behavior adapters use this form:module behavior Reverse(struct 'a)(Base: Order(struct 'a)): Order(struct 'a).Functors are declared inside ordinary files as nested items. A file is never itself a functor header; the file module exports its functors.
Argument checking is structural against the parameter signature, in the same spirit as explicit structural passing for behavior positions: the application site names the role, so the argument only has to provide the surface.
module UserStore {
module struct Key = UserId;
module struct Value = User;
let get = fn(key: UserId) -> User { ... };
let put = fn(key: UserId, value: User) -> Unit { ... };
}
module UserCache = MakeCache(UserStore); // ok: UserStore satisfies Store
A mismatch points at the signature member, not at a unification trace:
error: `UserStore` does not satisfy `Store`
--> src/Caches.shp:12:19
|
12 | module UserCache = MakeCache(UserStore);
| ^^^^^^^^^^^^^^^^^^^^ `Store` requires `put`, but `UserStore.put` is not defined
|
4 | module struct MakeCache(S: Store): Cache {
| --------- required by functor parameter `S`
Sealing
A result ascription seals the functor: members not in the result signature are private, and type t / type Name; members declared abstract in the signature are opaque outside.
module struct MakeCache(S: Store): Cache {
type t = { entries: Map(S.Key, S.Value) } // concrete inside
let debug_count = fn(cache: t) -> Int { ... }; // not in Cache: private
...
}
let cache: UserCache = UserCache.empty(); // UserCache is opaque outside
let n = UserCache.debug_count(cache); // error: not part of Cache
Without a result ascription, the functor's full body surface is its result. This is the same abstraction model file ascriptions and .shc contracts give file modules, applied to functor results.
Applicative Identity
Functor application is applicative: applying the same functor to the same arguments yields the same module identity and the same types, everywhere in the program.
module A = MakeCache(UserStore);
module B = MakeCache(UserStore);
// A == B as witnesses; A and B name the same abstract type
Identity follows the rules in Behavior Modules and Witnesses:
Aliases normalize:
module UserCache = MakeCache(UserStore);andMakeCache(UserStore)are the samePath(Apply(MakeCache, [UserStore])).UserCache.EntryandMakeCache(UserStore).Entryare the same node after normalize (Dotof thatApply).Applications compare by constructor path plus normalized argument paths:
Reverse(NameOrder) == Reverse(UserByName)whenmodule NameOrder = UserByName;.Functor arguments must be nameable: module paths, aliases, or applications of those. Anonymous module expressions are not valid functor arguments because they would mint a fresh
Identper spelling.A functor is typed once. Application checks arguments against parameter signatures and substitutes parameter
Idents for argumentPaths in the result module type. The compiler does not clone and rename source. Functors lower as functions: a module value is a record; a functor value is a function of module records;module Scores = MakeLookup(ScoreStore)is a call. Result types stayApply(...); two calls may produce two runtime records at one type (state belongs in services).After
module M = …, strengthening makesM.tthe pathM.t(Dot(M, t)), so aliases and ascriptions share identity with the underlyingApply.Applicative identity lets functor-built modules serve as behavior witnesses and keeps canonical behavior declared inside a functor body globally unique. The witness case is executable in Behavior Modules and Witnesses.
Ship does not have generative functors. If a future need appears (state-like freshness per application), the generative form must be a new explicit spelling, never the default. Modules with internal state should be services or resources, not functors.
Functors And Canonical Behavior
A functor body may declare canonical behavior only for struct modules the body itself defines:
module struct MakeCache(S: Store): Cache {
type t = { entries: Map(S.Key, S.Value) }
module struct Stats {
type t = { hits: Int; misses: Int }
}
module behavior Inspect(Stats) { ... } // ok: Stats is defined here
module behavior Inspect(S.Key) { ... } // error: S.Key belongs to the argument
module behavior Inspect(User) { ... } // error: User is not defined here
}
This is the coherence rule that keeps canonical lookup unique: because application is applicative, MakeCache(UserStore).Stats is one struct module program-wide, so its canonical Inspect has exactly one home. Allowing a functor to install canonical behavior for its arguments would let two applications conflict.
Behavior for argument types stays where it always lives: with the behavior owner, the struct owner, or compiler synthesis.
Functors Versus Meta Generators
Ship has two ways to produce modules from other declarations. They stay separate:
| Functors | Meta generators | |
|---|---|---|
| Input | modules satisfying signatures | typed descriptors (Meta.Type) |
| Output | a module, checked once, applied applicatively | generated Ship modules (in memory; .ship/generated/ on review) |
| Power | parameterization the type system can express | introspection over field/variant shape |
| Review | ordinary source | ship check --generated |
Use a functor when the abstraction is expressible in signatures. Use a generator when the output depends on a type's structure (per-field encoders, table functions). Generators may emit functor applications (module UserCache = MakeCache(UserStore);) instead of inlined bodies, which keeps generated artifacts small and reviewable.
Compiler mapping
Rust names, OCaml ideas. Diagnostics and stdlib stay Ship (module Scores =
MakeLookup(ScoreStore)).
| Idea (OCaml) | Ship compiler |
|---|---|
Ident.t (typing/ident.ml) | Ident { name: Symbol, stamp } — equality is the stamp |
Pident / Pdot / Papply (typing/path.ml) | PathKind::Ident / Dot / Apply — hash-consed; Apply is applicative identity |
Subst.t (typing/subst.ml) | Subst: Ident → Path on types, not source clone |
Env.t (typing/env.ml) | ModuleEnv — aliases, functor params, occupancy |
signature include (typing/includemod.ml) | existing pin / include / missing-member / coherence checks |
functor as lambda (lambda/translmod.ml) | lower functor to a function of module records |
F(A, B) is one Apply with args: Vec. A nested argument such as F(G(A)) is Apply(F, [Apply(G, [A])]); every constructor and argument is normalized before interning. Arguments may themselves name functors, so higher-order paths retain the same applicative identity. Checking validates each intermediate result structurally, and lowering emits nested calls between module functions.
What Ship Rejects
with type t = ...equalities and destructive substitution (:=). Struct members andstruct 'abinders carry the type identity OCaml encodes intequality constraints. Functor parameter substitution still exists — that is not:=.Generative functors as a default or as an implicit behavior.
First-class-module packing/unpacking ceremony (
(val e : S)). Modules are already passable at named call sites with tracked identity. Implicit existential packing and public opaque identities inferred from_are also rejected; a future existential package feature must be explicit.File-level functor headers.
Anonymous module expressions as functor arguments.
Stateful functors; use services/resources for state.