Blog › ICP guides
Clean developer on retainer: uniqueness types, in-place array update, World type, type classes, and Clean functional systems programming on monthly retainer
December 2, 2026 · ~15 min read
A Clean program implementing an in-place array transformation pipeline used Clean’s unique type annotations to enable efficient array updates without copying. The program defined a record type for configuration, a unique array of integers ({*} {Int}), and a combinator function combinedOp :: {*} {Int} {*} {Int} -> ({*} {Int}, {*} {Int}) that was supposed to perform two different transformations on two arrays. The developer called combinedOp arr arr passing the same unique array binding twice — intending the combinator to use the same array for both operations and return two independent result arrays. Clean’s uniqueness type checker rejected the call: a unique value may not be used more than once. The rule is absolute — uniqueness guarantees that there is exactly one live reference to the underlying data, enabling in-place mutation. Passing the same unique binding to two arguments would create two references to the same array, violating the uniqueness invariant. The type checker produces a uniqueness error: “argument is unique but has been used before.” Two uniqueness type errors per combinator call. The Clean developer on retainer diagnosed the sharing violation: the developer’s intention of using the same array for both operations was fundamentally incompatible with uniqueness semantics. Unique values can be threaded through operations sequentially (each operation receives the array, transforms it in place, and passes the result to the next operation) but cannot be shared across simultaneous function arguments. The fix was to restructure the combinator call to sequential threading: first transform the array with operation one, receiving a new unique binding arr1; then transform arr1 with operation two, receiving arr2. The two operations no longer share a reference — each receives and returns a unique binding, threading the array through the pipeline. Uniqueness type errors: 2 per combinator call → 0.
The work log entry read “fixed unique array pipeline, 8h.” It names the result and duration. It cannot explain why Clean’s uniqueness type system rejects sharing — the invariant is that a unique type annotation guarantees exclusive ownership of the underlying heap data, which allows the compiler to generate in-place mutation instructions instead of allocating new arrays on every update; if two references to the same unique array existed simultaneously, an in-place update through one reference would corrupt data visible through the other reference, breaking program correctness. It cannot explain why the sequentially-threaded version is semantically equivalent to the intended shared version in this case — the two operations are independent transformations; applying them sequentially to the same array produces the same result as applying them “simultaneously” in any order, because neither operation depends on the other’s result. It cannot explain the retainer’s decision to annotate intermediate bindings as unique rather than converting the array to a non-unique type — losing the unique annotation would force the compiler to allocate a new array on every update, eliminating the performance benefit that motivated the unique type in the first place. The 8 hours of uniqueness semantics analysis, sequential threading design, and binding annotation review are invisible in the diff.
Clean uniqueness types: *T annotation, sharing restrictions, sequential threading, and World type
Clean’s uniqueness type system is a static type discipline that tracks ownership of heap-allocated data. A type annotated with * (pronounced “unique”) carries a compile-time guarantee that exactly one live reference to the underlying heap object exists at any program point. This guarantee enables an important optimization: operations on unique values can be implemented as in-place mutations. A unique array update — writing a new value to an index — can overwrite the existing array cell without allocating a new array, because uniqueness guarantees no other reference can observe the change. Non-unique (shareable) values may have any number of live references, so operations on them must produce copies to avoid aliasing; unique values have exactly one reference, so the runtime can safely update in place.
The sharing restriction is the central constraint of Clean’s uniqueness system: a unique value may be used exactly once as an expression. Binding a unique value to a name and then using that name in two positions — as two function arguments, in two branches of a case expression, or in two let bindings — is a type error. The compiler enforces this at every use site. Sequential threading is the idiomatic pattern for applying multiple operations to a unique value: each operation receives the unique binding as input and returns a new unique binding as output; the next operation receives the output of the previous one. The chain of unique bindings models a sequence of in-place transformations, where each step transfers ownership from the input binding (which is consumed and may no longer be used) to the output binding (which is the new exclusive reference). The pattern is familiar to Rust programmers who have worked with ownership and move semantics, though Clean’s uniqueness system predates Rust by decades and operates entirely at the type level without borrow checking or lifetime annotations.
The World type is Clean’s mechanism for safe I/O in a purely functional setting. A World value represents the entire external state of the program — file system, network, standard input and output. The World type is always unique: at any program point there is exactly one live World reference. I/O operations take a World as input and return an updated World as output, threading the token through each operation sequentially. This sequentiality is not a limitation — it enforces that I/O operations happen in a well-defined order, which is necessary for programs that read from a file and then write to it, or that perform operations whose order matters. The File* type follows the same pattern: a File* value is a unique reference to an open file, enabling in-place read and write operations without locking. Retainer work involving Clean I/O frequently involves repairing pipelines where a developer consumed the World token without threading it back, or attempted to branch the World across two paths, both of which are uniqueness violations.
Clean type classes, lazy evaluation, strict annotations, and foreign function interface
Clean’s type class system is similar in structure to Haskell’s. A class declaration introduces a set of method signatures that instances must provide: class Eq a where (==) :: a a -> Bool. An instance declaration provides implementations for a specific type: instance Eq Int where (==) x y = ... . Class hierarchies work as in Haskell: a class can constrain its type variable to be an instance of another class, requiring that the type also satisfy the superclass constraints. Default method implementations in a class declaration are used when an instance does not provide a specific implementation. Clean’s type class system supports overloaded functions whose behavior varies with the type of their argument, enabling the same sort function to sort lists of any type with an Ord instance.
Clean evaluates expressions lazily by default, like Haskell: expressions are not evaluated until their value is required. This enables programming with infinite data structures and deferred computation, but can lead to performance problems when excessive thunk allocation dominates runtime. Clean provides strict annotations — placing ! before a function argument type or a record field type — to force immediate evaluation of that expression. In performance-critical inner loops, changing a lazy argument to strict (f ! x instead of f x) eliminates thunk allocation and can significantly reduce runtime and memory usage. Diagnosing when to apply strict annotations requires profiling and understanding the evaluation order consequences — forcing evaluation of an argument that is only conditionally needed can cause unnecessary computation, while lazy evaluation of an argument needed on every path adds thunk allocation overhead. Senior Clean retainers develop intuition for this trade-off through profiling cycles that are often not visible in work logs.
Clean’s foreign function interface allows calling C functions from Clean programs. A Clean declaration foreign import ccall "c_function" cleanName :: CInt -> CInt binds a C function to a Clean name with explicit type mapping. The interface requires care around uniqueness: C functions that modify data through a pointer must be called with unique arguments in Clean to ensure the uniqueness invariant holds; calling a C function with a non-unique argument that the C function modifies in place would violate uniqueness by creating an aliased mutation. Clean was developed at Radboud University Nijmegen by Marko van Eekelen, Rinus Plasmeijer, and others from the late 1980s onward; it shares the lazy purely functional approach of Haskell but is distinguished by its uniqueness type system, which achieves safe in-place mutation without reference tracking or garbage collection for unique values. Its closest retainer-ecosystem relatives are Idris (for the intersection of functional programming and uniqueness/linearity research) and Haskell (for the lazy purely functional positioning), but Clean’s uniqueness type system with *T annotations, World-typed I/O, and in-place array update guarantee make the retainer work distinct in sharing violation analysis, sequential threading design, and unique type annotation engineering.
How HourTab tracks Clean developer retainer hours
Clean retainer work carries the invisible-hours problem common to all uniqueness-typed and linearity-typed language retainers, amplified by the mismatch between Clean’s guarantee of in-place mutation efficiency and the programming discipline required to thread unique values correctly. Teams using Clean for high-performance functional programming, systems work where allocation matters, or academic research into uniqueness type systems frequently encounter the sharing violation pattern described above: a developer passes the same unique array to two arguments of a function, expecting both arguments to receive the same data. The 2 uniqueness type errors per combinator call described above is one instance of a broader pattern; the retainer work is the uniqueness semantics analysis that identifies why sharing violates the in-place mutation guarantee, the sequential threading design that restructures the pipeline to pass ownership sequentially, the binding annotation review that ensures all intermediate values carry the correct uniqueness markers, and the World type threading repair that restores correct I/O sequencing. Clean retainers produce visible outcomes — uniqueness type errors: 2 per call → 0; I/O pipeline compilation failures: N → 0 — but the hours spent on uniqueness semantics analysis (why can’t this value be passed twice?), sequential threading design (how to restructure the pipeline for ownership transfer), strict annotation tuning (where to force evaluation to reduce thunk allocation), and World type threading (how to thread the I/O token through the correct order) appear in work logs as “fixed unique type errors” without explaining the ownership mechanics.
HourTab gives Clean developers a public retainer-hours URL they send to clients — typically academic groups using Clean for uniqueness type research and functional systems programming courses, organizations maintaining Clean codebases built for high-performance functional computation where allocation elimination was a design requirement, and developers exploring uniqueness types as an alternative to Rust’s borrow checker for safe mutation. For Clean retainers, each work log entry should name the mechanism (uniqueness sharing violation repair: unique value passed to two arguments restructured to sequential threading; World type threading: I/O pipeline restructured to thread World token sequentially; strict annotation tuning: ! annotation added to reduce thunk allocation; type class instance design; foreign function interface with unique argument annotation), the specific types, functions, and uniqueness violations involved, and the before/after metric. Clean retainers are often compared to Haskell developer retainers for the shared lazy purely functional positioning, but Clean’s uniqueness type system with *T annotations, World-typed I/O sequencing, in-place array update guarantees, and strict annotation performance tuning make the retainer work distinct in ownership analysis, sequential threading design, and uniqueness boundary engineering. HourTab’s work log makes the sharing violation analysis, sequential threading restructure, and strict annotation decisions visible to clients who would otherwise see only the symptom — a uniqueness type error — and not understand why the fix required understanding that passing a unique value to two function arguments simultaneously creates two live references, violating the very invariant that enables in-place mutation.
Track Clean developer retainer hours without the status emails
HourTab gives Clean 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 uniqueness type engineering log becomes the proof of value that gets the retainer renewed.
See HourTab pricing →FAQ: Clean developer retainers
What does a Clean developer on retainer typically do?
A Clean developer on monthly retainer covers Clean uniqueness types (*T for unique values; non-unique values may be shared; unique values may not be shared; threading unique values through sequential function calls; World type for I/O; File* for in-place file operations), Clean type classes (class and instance declarations; hierarchies; default methods), Clean pattern matching and guards, Clean lazy evaluation (lazy by default like Haskell; strict ! annotations for performance), and Clean array operations with in-place update semantics.
What Clean work is most commonly underlogged in a retainer?
Uniqueness sharing violation repair (unique array passed to two arguments of combinator; uniqueness checker rejected sharing; restructured to sequential threading; errors: 2 per call → 0; 6–10 hrs invisible); World type threading (I/O pipeline consumed World token without returning updated token; subsequent I/O lacked current World; restructured to thread World token sequentially; errors: 4 per I/O pipeline → 0; 5–9 hrs invisible); strict annotation tuning (lazy thunk allocation dominated profile; ! annotation forced evaluation; thunk overhead eliminated; 4–8 hrs invisible).
What are typical Clean developer retainer rates?
Entry-level Clean developers (1–2 years, Clean basics, uniqueness basics, Clean standard environment) bill at $70–$125/hr. Mid-level Clean engineers (2–4 years, uniqueness threading patterns, World type I/O, type class instances, Clean array programming) bill at $120–$200/hr. Senior Clean architects (4–8 years, large-scale uniqueness type architectures, strict annotation tuning, advanced type class hierarchies, Clean-to-C FFI) bill at $170–$295/hr. Monthly retainer ranges: $2,000–$4,900/mo advisory (15–25 hrs), $6,800–$18,000/mo for full Clean systems development engagements.
What should a Clean developer retainer agreement include?
A Clean developer retainer agreement should specify: uniqueness type scope (*T unique annotations; sharing restrictions; sequential threading patterns; World type I/O; File* operations); type class scope (class and instance declarations; hierarchies; default methods); array operations scope (in-place update semantics; uniqueness threading through pipelines); performance tuning scope (strict annotations; lazy evaluation trade-offs); and hour logging format (advisory category, before/after uniqueness error count, whether fix required sequential threading, World token threading, or strict annotation addition).
How should Clean developer retainer hours be logged?
Log each Clean retainer session with: advisory category (uniqueness sharing violation repair: unique value passed to two arguments restructured to sequential threading; World type threading: I/O pipeline restructured to thread World token through each operation; strict ! annotation tuning: added to reduce thunk allocation; type class instance design; FFI with unique argument annotation); the specific types, functions, and uniqueness violations involved (unique {*} {Int} arr passed to combinedOp arr arr; uniqueness error: argument already used; restructured to let arr1 = op1 arr in op2 arr1; errors: 2 per call → 0); and the before/after metric. Include whether fix required sequential threading, World token threading, or strict annotation addition.