Blog › ICP guides
Standard ML developer on retainer: module system, functors, signature ascription, value restriction, MLton whole-program optimization, and SML on monthly retainer
November 29, 2026 · ~15 min read
A Standard ML program using the module system defined a functor Apply(S : STACK) where STACK was a signature. The signature declared push : 'a -> 'a stack -> 'a stack — a polymorphic push function that works for any element type 'a. The developer wrote a structure IntStack providing push with a concrete monomorphic implementation: fun push (x : int) (s : int stack) = ..., with explicit type annotations restricting x to int and s to int stack. When applying the functor as Apply(IntStack), SML type-checking failed: the structure’s push had type int -> int stack -> int stack, but the signature required 'a -> 'a stack -> 'a stack. The structure’s push was not polymorphic — it was a monomorphic int version. SML’s module system requires that the structure provides a value that satisfies the signature’s type specification, and for a polymorphic type 'a -> 'a stack -> 'a stack, the provided value must be universally polymorphic: it must work for all 'a, not just int. The monomorphic type int -> int stack -> int stack is an instantiation of the polymorphic type at 'a = int, but the structure cannot provide a monomorphic instantiation where the signature requires a polymorphic function — the functor body might apply push with non-int elements. One type error per functor application. The Standard ML developer on retainer diagnosed the polymorphism mismatch: the push implementation needed to be truly polymorphic, without the explicit int type annotation. The fix was to restructure push as fun push x s = ... without type annotations, allowing SML’s type inference to generalize the type to 'a -> 'a stack -> 'a stack. Functor application: 1 type error → 0.
The work log entry read “fixed functor type mismatch, 6h.” It names the symptom and duration. It cannot explain why removing the explicit int annotation from push fixes the functor application — SML’s type inference generalizes the type of push to a polymorphic type scheme ∀'a. 'a -> 'a stack -> 'a stack when no concrete type annotation forces monomorphism; the annotation (x : int) specializes the type to int, preventing generalization. It cannot explain why this matters at functor application: in SML’s module system, when a structure is ascribed to a signature (either transparently with : or opaquely with :>), the type checker verifies that each value in the structure has a type that subsumes the corresponding type in the signature; for a polymorphic signature type 'a -> 'a stack -> 'a stack, the structure must provide a value with a type at least as polymorphic — a monomorphic int -> int stack -> int stack is less polymorphic and does not subsume. It cannot explain the retainer’s decision to use opaque ascription :> rather than transparent : for the final module boundary — opaque ascription hides the implementation type int stack behind the abstract type stack declared in the signature, preventing callers from relying on the concrete representation. The 6 hours of SML type generalization analysis, polymorphism subsumption verification, and ascription mode selection are invisible in the diff.
Standard ML module system: structure, signature, functor, opaque vs transparent ascription, and where type
Standard ML’s module system is one of the most expressive in any production programming language. A structure is a named collection of type, value, and exception declarations — analogous to a module or namespace in other languages. A structure groups related definitions: structure IntStack = struct type 'a stack = ... val push = ... val pop = ... end. A signature is a type specification for structures: it declares the types of exported values, the names and kinds of exported types (abstract or concrete), and the types of exported exceptions. Structures are matched against signatures to verify that the structure provides everything the signature requires. A functor is a function from structures to structures, parameterized by a signature: functor F(S : SIG) = struct ... end. Functors enable parameterized programming at the module level — the functor body can use everything declared in the signature SIG, and any structure that satisfies SIG can be passed as an argument. Functors are applied as F(ConcreteStruct), producing a new structure.
Ascription determines how the type checker relates a structure to a signature and how much of the implementation is visible to callers. Transparent ascription (using :) checks the structure against the signature but leaves implementation types visible: callers can see that stack is actually int list, for example, and can use that knowledge to call List functions on stack values. This is convenient but creates representation dependence — callers become coupled to the internal representation. Opaque ascription (using :>) checks the structure against the signature and seals all implementation types: callers can only use the abstract type stack as declared in the signature, with no knowledge of whether it is a list, array, or any other representation. Opaque ascription is the correct choice for library boundaries because it allows the implementation to change the representation without breaking callers. The retainer decision between : and :> is a software architecture decision with long-term maintenance consequences, not a mechanical choice.
Type sharing constraints (where type t = concrete_t) appear in signatures when two signatures share a type that must be the same concrete type. For example, a functor that takes two structure arguments, both providing an abstract type t, needs a way to declare that the t in the first argument and the t in the second argument are the same type — otherwise the functor body cannot use values of one structure’s t where the other structure expects its t. The where type clause solves this: SIG where type t = OtherSig.t refines the signature to constrain t to a particular concrete type. This mechanism is essential for functors that thread abstract types through multiple argument structures and enables separate compilation while maintaining type compatibility across module boundaries. Value bindings in structures are subject to the same type generalization rules as top-level bindings; values bound in let expressions inside structures that involve mutable types are subject to the value restriction.
SML type system: value restriction, let-polymorphism, MLton whole-program optimization, and SML/NJ
Standard ML’s type system is based on Hindley-Milner type inference with let-polymorphism. Let-polymorphism means that a val binding generalizes its type to a polymorphic type scheme if the bound expression is a syntactic value: val id = fn x => x gives id the type ∀'a. 'a -> 'a, and id can be applied at any type within its scope. This is the mechanism that gives SML its expressive generic programming without explicit type parameters at call sites.
The value restriction is an important subtlety of SML’s type system. A val binding is only generalized to a polymorphic type scheme if the right-hand side is a syntactic value — a variable, literal, lambda abstraction (fn), or constructor application to values. Expressions involving mutable state — such as ref [] or function calls that may allocate references — are not generalized. This rule prevents a soundness hole: if val r = ref [] were generalized to ∀'a. 'a list ref, a program could store an int into r through one instantiation and read a string out of it through another, breaking type safety. The value restriction forces the type of ref [] to remain monomorphic. In practice, the value restriction surfaces when a developer writes code like val cache = ref [] intending to use the cache polymorphically, and finds that SML infers a monomorphic type. The retainer fix is typically to wrap the expression in a function: fun makeCache () = ref [] returns a fresh 'a list ref each call, and the function itself is polymorphic because it is a syntactic value. Recognizing whether a type variable is left ungeneralized due to the value restriction — rather than a genuine type error — is a diagnostic skill that takes experience to develop.
MLton is a Standard ML compiler that optimizes the entire program at once rather than module-by-module. Whole-program optimization allows MLton to eliminate virtually all allocation overhead for programs that do not actually use polymorphism at runtime: if a polymorphic function is only ever called at one concrete type, MLton can specialize it and eliminate the polymorphic dispatch. MLton requires fully polymorphic types at module boundaries and is strict about the SML ’97 standard; it produces highly optimized native code and is the preferred compiler for performance-critical SML programs. SML/NJ (Standard ML of New Jersey) is the interactive SML compiler — more flexible for development, with a REPL that supports incremental evaluation and top-level declarations. SML/NJ generates continuation-passing style (CPS) bytecode and is excellent for REPL-driven development, but with different performance characteristics than MLton. Standard ML was designed by Robin Milner, Robert Harper, David MacQueen, and Mads Tofte in the 1980s; the SML ’97 standard is the authoritative language definition. Its closest retainer-ecosystem relatives are OCaml (which shares ML-family heritage and a similarly expressive module system but adds objects and a different approach to polymorphic variants) and Haskell (for the purely functional and type-inference positioning), but SML’s module system with functors and opaque ascription, value restriction discipline, and MLton/SML-NJ dual-compiler ecosystem make the retainer work distinct in module type engineering and polymorphism boundary management.
How HourTab tracks Standard ML developer retainer hours
Standard ML retainer work carries the invisible-hours problem common to all type-system-heavy language retainers, amplified by the gap between the elegance of SML’s module system and the precision required to navigate its polymorphism constraints. Teams using SML for language research, systems programming, or maintaining existing functor architectures frequently encounter the functor polymorphism mismatch pattern when a developer adds a concrete type annotation for clarity or documentation: the annotation (x : int) appears harmless, reads as a helpful type note, and silently monomorphizes the function, breaking every functor that applies the structure to a polymorphic signature. The 1 type error per functor application described above is one instance of this pattern; the retainer work is the SML type generalization analysis that identifies the annotation as the cause, the polymorphism subsumption verification that confirms the fix, and the ascription mode selection that prevents future representation leakage. SML retainers produce visible outcomes — functor type errors: 1 per application → 0; value restriction generalization failures: N per binding → 0 — but the hours spent on polymorphism subsumption analysis (does this structure type satisfy the signature?), ascription mode selection (opaque :> vs transparent : and what types are hidden from callers), value restriction diagnosis (why is this ref expression not generalized?), and MLton whole-program optimization tuning appear in work logs as “resolved module type error” without explaining the generalization mechanics.
HourTab gives Standard ML developers a public retainer-hours URL they send to clients — typically academic groups using SML for language research and type theory work, systems programmers using MLton for high-performance functional code where allocation matters, and organizations maintaining existing SML codebases with complex functor architectures accumulated over years of development. For SML retainers, each work log entry should name the mechanism (functor polymorphism mismatch repair: explicit type annotation removal for generalization; opaque :> vs transparent : ascription selection; where type sharing constraint addition; value restriction refactor: ref expression to function wrapping value; MLton whole-program optimization flag tuning; SML/NJ CPS compilation analysis; Standard Basis Library API usage), the specific structure names, signature types, functor parameters, and ascription modes involved in the fix, and the before/after metric. SML retainers are often compared to OCaml developer retainers for the shared ML-family module system positioning, but SML’s value restriction discipline, opaque ascription semantics, MLton whole-program optimization requirements, and SML ’97 standard conformance make the retainer work distinct in polymorphism boundary management, module sealing architecture, and compiler-specific optimization engineering. HourTab’s work log makes the type generalization analysis, polymorphism subsumption verification, and ascription mode selection visible to clients who would otherwise see only the symptom — one functor type error — and not understand why the fix required understanding the difference between a polymorphic type scheme and a monomorphic instantiation, and why a single explicit type annotation was enough to break every functor application that required the polymorphic version.
Track Standard ML developer retainer hours without the status emails
HourTab gives Standard ML developers a public URL per client retainer. One link, no login, live burn-down. Your clients stop asking “how many hours do I have left?” and your module engineering log becomes the proof of value that gets the retainer renewed.
See HourTab pricing →FAQ: Standard ML developer retainers
What does a Standard ML developer on retainer typically do?
A Standard ML developer on monthly retainer covers SML module system engineering (structure, signature, functor, opaque :> vs transparent : ascription, where type sharing constraints), SML type system (let-polymorphism, value restriction diagnosis, type abbreviation, datatype, exception), MLton whole-program optimization (compilation flags, inlining, allocation elimination), SML/NJ interactive development, and Standard Basis Library usage.
What SML work is most commonly underlogged in a retainer?
Functor polymorphism mismatch repair (monomorphic structure implementation doesn’t satisfy polymorphic signature; explicit type annotation prevents generalization; annotation removed; functor application: 1 error → 0; 6–10 hrs invisible); value restriction diagnosis (ref expression not generalized to polymorphic type; refactored to value; binding type: monomorphic → polymorphic; 5–9 hrs invisible); opaque ascription selection (transparent : exposed concrete type; callers depended on representation; switched to :>; representation type hidden; 4–8 hrs invisible).
What are typical Standard ML developer retainer rates?
Entry-level Standard ML developers (1–2 years, SML basics, basic module system, SML standard library, SML/NJ REPL) bill at $70–$120/hr. Mid-level SML engineers (2–4 years, functor architecture, opaque vs transparent ascription, value restriction, where type constraints, MLton optimization) bill at $115–$195/hr. Senior SML architects (4–8 years, large-scale functor architecture, MLton whole-program optimization, SML/NJ CPS, complex module type engineering, SML metaprogramming) bill at $165–$285/hr. Monthly retainer ranges: $1,900–$4,800/mo advisory (15–25 hrs), $6,500–$17,000/mo for full SML systems development engagements.
What should a Standard ML developer retainer agreement include?
A Standard ML developer retainer agreement should specify: module system scope (structure, signature, functor, opaque :> vs transparent :, where type, type sharing); type system scope (let-polymorphism, value restriction, datatype, exception, type abbreviation); compiler scope (MLton optimization flags, SML/NJ interactive development, CPS transformation); Standard Basis Library scope; and hour logging format (advisory category, before/after error count, whether fix required annotation removal, :> ascription, where type, or MLton flag change).
How should Standard ML developer retainer hours be logged?
Log each Standard ML retainer session with: advisory category (functor polymorphism mismatch repair: explicit type annotation removal for generalization; opaque :> vs transparent : ascription selection; where type sharing constraint addition; value restriction refactor: ref expression to value; MLton whole-program optimization flag tuning; SML/NJ CPS compilation analysis; Standard Basis Library API usage); the specific structure names, signature types, functor parameters, and ascription modes involved in the bug (structure IntStack providing push : int -> int stack -> int stack; STACK signature requiring push : 'a -> 'a stack -> 'a stack; 1 functor type error; removed int annotation; push generalized to polymorphic; type error: 1 → 0); and the before/after metric. Include SML compiler (MLton or SML/NJ) and whether fix required annotation removal, :> ascription, where type, or value restriction refactor.