Blog › ICP guides

Clean developer on retainer: uniqueness type system, World monad I/O, functional programming, ABC machine compilation, and Clean language engineering on monthly retainer

October 22, 2026 · ~18 min read

A scientific computation program written in Clean had been producing three I/O failures per day. The program performed two distinct analysis phases on the same input data: one phase generated a statistical summary and another phase generated an anomaly report. A developer had attempted to run the two phases in parallel to reduce total computation time by passing the unique *World value into both phases simultaneously. The Clean developer on retainer diagnosed the root cause: Clean’s uniqueness type system forbids aliasing of unique values. Passing *World to two functions in the same expression means the World value would be used twice from the same binding, which the Clean type checker rejects as a uniqueness violation. The type system enforces a linear discipline: a unique *World value must be threaded through each I/O operation in sequence — each operation receives the current World, performs the I/O action, and returns a new unique World token that must be passed to the next operation. There is no parallelism at the World threading level; the appearance of parallel I/O in Clean requires explicit sequential ordering, even if the actual system calls could be issued concurrently. The fix restructured the program to thread *World sequentially through both phases, yielding a function pipeline of type (result1, result2, *World) tupled at the end. Uniqueness violations: eliminated. Runtime I/O failures: 3 per day → 0.

The work log entry read “fixed I/O ordering error, 11h.” It names the symptom and the duration. It cannot explain to a client why the fix required understanding Clean’s uniqueness type system, why the *World discipline exists (to give Clean’s purely functional type system a mechanism for I/O without requiring monads), why two functions receiving the same *World binding is a type error rather than a runtime race condition, why the fix required redesigning the function pipeline structure rather than adding a synchronization barrier, or what the tuple threading pattern (result1, result2, *World) means for function composition in a uniqueness-typed language. The diagnosis required understanding that Clean’s uniqueness types are a compile-time linearity system: a value annotated *T (pronounced “unique T”) can have at most one reference in scope at any given program point. If a function receives a *World and then the compiler detects a second use of that *World binding (either by passing it to two different functions, or by using it after it has been passed to a function that consumed it), the Clean type checker emits a uniqueness violation error. The 11 hours of uniqueness violation tracing (following the *World flow through nested function calls to locate the aliasing point), function pipeline restructuring (changing the two-branch parallel structure into a sequential tuple thread), and type signature correction (updating the affected function signatures to reflect the new return types) are not visible in the diff beyond the restructured pipeline and the corrected type signatures.

Clean’s uniqueness type system: *, #, !, and the linearity discipline

Clean’s uniqueness type system is a compile-time mechanism for tracking which values may be safely mutated in a purely functional language. A type annotated with * — for example, *World, *File, *{Int} (a unique array of Int) — is a unique type: the runtime system guarantees that exactly one reference to the value exists at any given moment, permitting safe in-place mutation without copying. The fundamental rule is: a unique value may be passed to at most one function at any call site, and that function must receive it in a unique parameter position (typed *T). After a function receives a *T argument, the argument binding in the caller’s scope is no longer accessible: the callee has consumed the unique reference. If the callee produces a new unique value of the same type as output (which most I/O functions do: they receive *World and return a new *World), the caller receives that new unique value and threads it to the next operation. If the function does not need to propagate the unique value (for example, a file-closing function), it discards the unique argument and returns a non-unique result.

The # strictness annotation and the ! unboxed field annotation are distinct from uniqueness but frequently confused with it. # T in a function argument position means the argument is evaluated strictly (before the function body begins) rather than lazily: f :: # Int -> Int declares that f evaluates its argument before executing. Without #, Clean evaluates function arguments lazily by default (call-by-need), building graph reduction thunks. Strict annotations are critical for functions that will inevitably evaluate their argument (writing to a file, accumulating a sum) to avoid building up large thunk chains that cause stack overflows or excessive heap use. The ! annotation in record field declarations specifies that the field is stored unboxed: :: MyRecord = { !intField :: Int, lazyField :: String } stores intField without an indirection pointer, reducing heap allocations. The distinction: * is about sharing (unique vs aliased); # is about evaluation order (strict vs lazy); ! is about representation (boxed vs unboxed). A field can be both * and !: { !*fileHandle :: *File } is a record with an unboxed unique file handle field that must be threaded linearly through all record operations.

The /. combinator converts a unique value to a non-unique (shared) value: (./ uniqueVal) or written postfix uniqueVal /. allows a *T to be used as a regular T in a non-unique context, discarding the uniqueness guarantee. This is used when a function needs to read a unique value without consuming it — for example, to inspect the contents of a unique array without destroying it. However, using /. means the Clean compiler no longer tracks uniqueness for that access: if the original unique binding is used again after a /. coercion, the compiler cannot verify safety. In practice, /. is used sparingly, typically in functions that are guaranteed by the programmer to be read-only and where the overhead of threading the unique value through is disproportionate to the operation’s complexity. The correct pattern for read-access to unique arrays without consuming is to use uselect (a standard library function that returns the element and the array back as unique): uselect arr i returns (arr, arr.[i]) where the first element is the same unique array reference and the second is the element at index i. This is the pattern for all “borrow and return” operations on unique types in Clean.

Unique arrays in Clean use the *{} syntax: *{Int} is a unique array of Int values that can be mutated in place. Array indexing: arr.[i] returns element i (but consumes the array if the return type is non-unique); arr.[i] := value performs an in-place update, returning the updated array (the original binding is consumed). Clean’s array comprehension syntax: { f i \\ i <- [0..n-1] } creates an array by applying f to each index. Array comprehensions always produce non-unique arrays by default; to produce a unique array from a comprehension, use { # f i \\ i <- [0..n-1] } (the # here selects strict element evaluation, not uniqueness). The createArray, _createArray, and unsafeCreate functions create mutable arrays. When a function receives a *{T} and returns a *{T}, the Clean compiler can elide the copy and mutate the array in place; this is the mechanism that gives Clean programs Haskell-equivalent purity with C-equivalent in-place mutation performance for array algorithms.

Clean’s I/O model, module system, type classes, and the ABC machine

Clean’s I/O model is based on threading a unique *World value through every operation that interacts with the outside world. The type of an I/O function that reads a line from a file and returns it as a String: readLine :: *File -> (String, *File). The caller passes its unique *File reference in, and the function returns both the String result and a new unique *File reference representing the file after the read. The caller must use the returned *File for all subsequent operations on that file; the original *File binding is consumed. The entire I/O pipeline of a Clean program is therefore a chain of functions each receiving and returning unique values, with the top-level Start function receiving the initial *World from the runtime: Start :: *World -> *World. Any function that performs I/O must have *World (or a unique derivative like *File) in its type signature, and the unique value must be explicitly threaded through every function in the call chain. This is what makes the “parallel I/O” mistake impossible to compile: there is no way to give two functions the same *World because the type checker enforces that the binding has exactly one use.

Clean’s file I/O uses StdFile (or System.File in newer versions) for sequential file operations. openFile :: String Int *World -> (*File, *World) opens a file, returning a unique *File handle and the updated *World. The Int mode argument: FReadText, FWriteText, FAppendText, FReadData, FWriteData, FAppendData. After opening, the *File is threaded through read/write operations: freadline :: *File -> (String, *File) reads a line; fwrite :: String *File -> *File writes a string; fseek :: Int Int *File -> (Bool, *File) seeks to a position. fclose :: *File *World -> (Bool, *World) closes the file, consuming the *File handle and returning the updated *World. The pattern for a complete file-processing function: receive *World, call openFile to get (*File, *World), thread *File through all read operations, call fclose with the final *File and the *World, then continue with the returned *World. Every operation in the chain is sequentially ordered by the unique threading discipline; no runtime sequencing mechanism is needed because the type system enforces exactly one total order of I/O operations.

Clean’s module system separates interface from implementation using .dcl (definition module) and .icl (implementation module) file pairs. A .dcl file exports types, type synonyms, algebraic type names (with or without constructors), function type signatures, type class declarations, and type class instance declarations for types defined in the module. An .icl file contains the function bodies and type class instance implementations that correspond to the exported signatures. Algebraic type definitions: :: Tree a = Leaf | Node a (Tree a) (Tree a) defines a polymorphic binary tree. Exporting a type without its constructors (making it abstract): the .dcl file declares :: Tree a without the constructor list; clients can use the type but cannot pattern-match on its constructors. Exporting constructors: :: Tree a = Leaf | Node a (Tree a) (Tree a) in the .dcl exports all constructors. Record types: :: Point = { x :: Real, y :: Real }; field access with .x and .y; update syntax: { point & x = 1.0 } creates a copy of point with x replaced. Type synonyms: :: Name :== String. Import in .icl: import StdList, StdArray, StdString; from StdEnv import &, ||, ==, ... for qualified imports.

Clean’s type class system uses class and instance declarations. class Eq a where (==) :: a a -> Bool; (/=) :: a a -> Bool declares the equality class. instance Eq Int where (==) i j = i == j; (/=) i j = not (i == j) provides an instance. Type class constraints on function type signatures: member :: a [a] -> Bool | Eq a requires an Eq instance for the element type. Clean’s generic programming system allows type-indexed functions defined over all types: generic gEq a :: a a -> Bool declares a generic equality function; derive gEq Int, Real, Bool, {}, [], Tree derives instances for the listed types. The ABC machine (Abstract Base Computer) is the virtual machine that Clean targets; the Clean compiler generates ABC machine instructions, which are then compiled to native code by the ABC machine backend. The ABC machine is a graph reduction machine specialized for Clean’s lazy evaluation and uniqueness semantics. Compiler flags: -h N sets the heap size to N kilowords; -s N sets the stack size; -d enables dynamic typing support; -P enables profiling; -IL sets the intermediate language output path. The Clean profiler identifies heap allocation hotspots and reduction counts by function, enabling targeted strictness annotation addition to reduce thunk overhead in performance-critical paths.

How HourTab tracks Clean developer retainer hours

Clean retainer work shares the invisible-work problem with all functional programming retainers, with the additional challenge that Clean’s most common retainer tasks — uniqueness type annotation, *World pipeline restructuring, strictness annotation tuning, .dcl/.icl module interface design — produce diffs whose surface area is small relative to the analytical work required. Restructuring an I/O pipeline from a parallel-branch *World distribution to a sequential tuple thread is a diff that rewrites the top-level function structure; the value is zero uniqueness type errors and correct I/O sequencing enforced at compile time. Adding eight # strictness annotations to function arguments that were already being fully evaluated is a diff with eight modified type signatures; the value is zero stack overflows on inputs that previously overflowed the thunk evaluation stack at 10,000 elements. Changing a .dcl file to export a type without its constructors is a diff with one line removed; the value is an abstract type boundary that lets the implementation change without breaking downstream modules.

HourTab gives Clean developers a public retainer-hours URL they send to clients — typically scientific computing teams, academic institutions deploying Clean for formal verification or type-theory research, or system programming teams using Clean’s uniqueness types for in-place array mutation with functional purity guarantees — at the start of an engagement. For Clean retainers, each work log entry should name the mechanism (* uniqueness annotation audit; uniqueness violation diagnosis and *World threading restructuring; *File unique handle threading; *Array in-place mutation design; # strictness annotation addition; ! unboxed field annotation; /. coercion for read-only unique access; uselect/uupdate borrow-and-return patterns; .dcl/.icl module interface restructuring; algebraic type constructor export decisions; type class instance design; generic derive declaration; ABC machine heap/stack configuration; Clean profiler analysis), the specific function name and the uniqueness problem, and the before/after observable metric. Clean retainers are often compared to Haskell developer retainers for lazy functional programming work, to Idris developer retainers for dependently-typed functional programming, and to Rust developer retainers for ownership-typed systems programming. The distinction from Haskell is the uniqueness system: Clean enforces uniqueness at the type level with * annotations, giving the compiler permission to mutate in place without requiring the programmer to use IORef or STRef monad threading; the same array update that Haskell performs via STUArray in the ST monad, Clean performs with a direct arr.[i] := value update on a *{Int} unique array. HourTab’s work log makes the uniqueness annotation work visible to clients who would otherwise see only the function signatures and wonder why they look the way they do.

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 work 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 four principal service areas: uniqueness type annotation design (* annotation audit for unique types; uniqueness violation diagnosis and restructuring; *World I/O pipeline threading design; *File unique file handle threading; *{} unique array mutation design; # strictness annotation addition; ! unboxed field annotation; /. coercion for read-only unique access; uselect/uupdate borrow-and-return patterns); World monad I/O pipeline design (World monad I/O a *World -> (*a, *World) function design; sequential World threading using tuple threading; Task abstraction for compositional I/O; openFile/fclose/freadline/fwrite/fseek sequential file I/O); module system design (.dcl definition module authorship; .icl implementation module authorship; import cycle diagnosis; algebraic type constructor export decisions; type class declaration and instance design); and type class and algebraic type design (:: AlgType definition with constructor patterns; type class declaration and instance authorship; generic derive declaration for Eq/Ord/Show and custom generics; higher-order function design; ABC machine heap/stack configuration).

What Clean work is most commonly underlogged in a retainer?

Uniqueness type annotation redesign for *World state threading (program attempting to use *World in two parallel computation branches — uniqueness violation at branch point; restructured to thread *World sequentially through both paths using tuple (result1, result2, *World); runtime I/O failures: 3/day → 0; 14–22 hrs invisible in uniqueness violation diagnosis and pipeline restructuring), strictness annotation audit (8 functions missing # strictness annotations causing lazy thunk accumulation and stack overflow on inputs >10,000 elements; stack overflows: 8/week → 0; 10–16 hrs invisible in strictness analysis), and module interface restructuring (a .dcl file exporting concrete type constructors that should have been abstract; 3 downstream modules broken by constructor removal; 12–20 hrs invisible in interface minimization and downstream pattern-match migration). Each produces a small diff but a large correctness or performance improvement.

What are typical Clean developer retainer rates?

Entry-level Clean developers (1–2 years, basic algebraic type definitions, simple * uniqueness annotations, *World I/O threading, StdList/StdArray standard library, .dcl/.icl module pair authorship) bill at $65–$115/hr. Mid-level Clean engineers (2–4 years, complex uniqueness annotation design for multi-level data structure mutation, *File and *{} unique handle threading, Task abstraction for compositional I/O, type class design with generic derive declarations, strictness annotation tuning for the ABC machine, module hierarchy design for large Clean codebases) bill at $110–$200/hr. Senior Clean architects (4–8 years, full application architecture in Clean’s uniqueness model, ObjectIO or iTasks framework application design, ABC machine optimization for latency-critical systems, complex generic function design, Clean’s dynamic type system for runtime type reflection, and C-FFI integration via foreign function declarations) bill at $165–$300/hr. Monthly retainer ranges: $2,200–$5,200/mo advisory (15–25 hrs), $7,500–$20,000/mo for full Clean application platform engagements.

What should a Clean developer retainer agreement include?

A Clean developer retainer agreement should specify: uniqueness type scope (* annotation audit for all function type signatures; uniqueness violation diagnosis and restructuring; *World I/O pipeline threading design; *File unique file handle threading; *{} unique array mutation design; # strictness annotation addition; ! unboxed field annotation; /. coercion for read-only unique access; uselect/uupdate borrow-and-return patterns); I/O pipeline scope (World monad I/O function design; sequential World threading; Task abstraction; openFile/fclose/freadline/fwrite/fseek file I/O; StdIO console I/O); module system scope (.dcl/.icl module authorship; import cycle diagnosis; algebraic type constructor export decisions; type class declaration and instance design); type class and algebraic type scope (:: algebraic type definition; type class declaration and instance authorship; generic derive declaration; higher-order function design; list comprehension; array comprehension); ABC machine scope (clm/cpm compiler flag optimization; strict/lazy annotation tuning; heap/stack size configuration; profiler analysis); and hour logging format (function name; uniqueness violation type; *World threading depth before/after; Clean version and platform).

How should Clean developer retainer hours be logged?

Log each Clean retainer session with: advisory category (* uniqueness annotation audit; uniqueness violation diagnosis and *World threading restructuring; *File unique handle threading; *{} unique array mutation design; # strictness annotation addition; ! unboxed field annotation; /. coercion for read-only access; uselect/uupdate borrow-and-return; Task abstraction design; .dcl definition module interface restructuring; .icl implementation module authorship; import cycle resolution; algebraic type constructor export decisions; type class instance design; generic derive declaration; ABC machine compiler flag optimization; Clean profiler analysis session), the specific function name and the uniqueness problem (*World used in two parallel computation branches — uniqueness violation; restructured to thread sequentially using tuple (result1, result2, *World); runtime I/O failures: 3/day → 0), and the before/after metric (uniqueness type errors: N → 0; runtime I/O failures/day: 3 → 0; stack overflows/week: 8 → 0). Include Clean version, platform, and whether the fix required uniqueness annotation changes, *World threading restructuring, strictness annotation tuning, or .dcl/.icl interface revision.