Blog › ICP guides

Miranda developer on retainer: lazy evaluation, seq I/O ordering, list comprehensions, algebraic types, and Miranda functional programming on monthly retainer

November 5, 2026 · ~18 min read

A Miranda program computing a Fibonacci stream using the lazy list definition fib = 1 : 1 : [a+b | (a,b) <- zip fib (tl fib)] was writing the wrong string representation to an output file three times per computation run. A function consuming elements from the stream used seq to force evaluation order before a file write, but the seq call was placed after the file-write action in the action sequence rather than before it. Miranda’s lazy evaluation proceeds left-to-right only when something demands a value; in a sequence of I/O actions, the runtime executes the file-write action first and then encounters the seq. The file-write action needed the value of the stream element to serialize it as a string. Since the seq had not yet executed, the stream element was still an unevaluated thunk — a closure representing the computation a+b for some deferred pair (a,b). Miranda’s show function, which serializes values to string for output, wrote the thunk’s pre-evaluation string representation rather than the numeric value. The file received the wrong pre-evaluation closure representation for the stream element rather than the integer sum. Three file-write operations per run produced wrong output from the placement error. The Miranda developer on retainer diagnosed the lazy evaluation ordering: seq a b evaluates a to weak head normal form and then returns b, but the b in this case was the file-write action that was supposed to write the forced value — placing seq after the file-write means b had already executed by the time seq was evaluated. The fix moved seq elem (write_file elem) to make the file-write part of the b expression that seq returns after forcing the thunk. Wrong file outputs per run: 3 → 0.

The work log entry read “fixed Fibonacci stream file output ordering, 14h.” It names the symptom and the duration. It cannot explain to a client why Miranda’s lazy evaluation defers thunk computation until demand (Miranda uses call-by-need evaluation: every expression is evaluated at most once, and only when its value is demanded by a computation that cannot proceed without it; the Fibonacci stream definition fib = 1 : 1 : [a+b | (a,b) <- zip fib (tl fib)] is an infinite list whose elements are computed one by one only as they are consumed; before seq forces an element to WHNF, the element is represented in memory as a thunk — a closure capturing the computation a+b and its environment including the already-computed values of a and b), why seq a b is the correct tool for forcing thunk evaluation before I/O (seq evaluates a to WHNF — a value whose outermost constructor is known and not a thunk — and returns b; in seq elem (write_file elem), elem is forced to WHNF first, ensuring the integer sum is computed, and then the file-write action write_file elem serializes the already-computed integer), why placing seq after the file-write produces the wrong behavior even though seq is present in the program (when the runtime executes the action sequence, the file-write executes first with the unforced thunk; seq then forces the thunk after the file has already been written, which updates no output; the forcing is a no-op for I/O purposes once the serialization has already happened), or why show serializes an unevaluated thunk as a closure representation rather than computing the value first (Miranda’s show function uses normal-order evaluation internally, but when called on an expression that the type system has already committed to as an integer, the thunk’s representation depends on whether the runtime’s normal-order evaluation of show triggers thunk forcing before string construction; in some Miranda implementations, the show of a thunk produces an internal closure representation string rather than the integer value). The 14 hours of lazy evaluation order analysis, WHNF forcing semantics audit across all I/O action sequences, and seq placement redesign across all dependent output operations are not visible in the diff beyond a changed expression structure around the file-write.

Miranda’s lazy evaluation: thunks, WHNF, and seq

Miranda (designed by David Turner at the University of Kent in 1985) was the first widely-used purely functional programming language with pervasive lazy evaluation. Miranda’s evaluation strategy is call-by-need (also called lazy evaluation): every expression in a Miranda program is evaluated at most once, and only when its value is actually needed to produce a result. Before evaluation, expressions are represented as thunks — closures that capture the expression to be evaluated and all the variables in its lexical environment. When a thunk is first demanded, Miranda evaluates it to at least weak head normal form (WHNF), updates the thunk in place with the resulting value (sharing: the value is computed at most once), and returns the value. WHNF is a partially-evaluated form: a value is in WHNF if its outermost constructor is not a thunk. For integers, WHNF means the integer value itself. For lists, WHNF means either the empty list [] or a cons cell h:t where h is a value (possibly a thunk) and t is a value (possibly a thunk). Evaluating a list to WHNF forces the spine one step, revealing whether the list is empty or a cons, but does not force the head or tail elements themselves.

Lazy evaluation enables infinite data structures: a definition like ones = 1 : ones defines an infinite list of 1s. The list is represented in memory as a thunk initially; when the first element is demanded, the spine is forced one step, revealing a cons with head 1 and tail another thunk for the rest of ones. The second element demand forces the tail thunk, revealing another cons, and so on. Since each cons cell is only forced when demanded, the program can process infinite lists without diverging, as long as it consumes only a finite prefix (using take n to take the first n elements) or applies a function that terminates on the stream (like hd for the first element). The Fibonacci stream fib = 1 : 1 : [a+b | (a,b) <- zip fib (tl fib)] defines the infinite Fibonacci sequence using self-reference and a list comprehension: each element is the sum of the two preceding elements, generated lazily by zipping the stream with its own tail and summing the pairs. This definition works because Miranda’s lazy evaluation ensures that when the definition references fib in the right-hand side, it accesses the already-computed prefix of the stream (the first two elements are given as the literal 1 : 1 : prefix), and each new element is computed only when demanded.

The seq function: seq :: * -> ** -> **. seq a b evaluates a to WHNF and returns b. The purpose of seq is to force the evaluation of a lazy expression at a specific point in the program’s execution, before an operation that depends on the forced value. The canonical use case: ensuring that a value is forced before an I/O operation that will serialize it. Without seq, a value might remain a thunk when the I/O operation executes, causing the I/O operation to serialize the thunk representation rather than the computed value. The correct pattern: seq value (write_file value) — evaluate value to WHNF first, then execute the file-write on the forced value. The incorrect pattern: writing the file-write action first and then seq — the file-write executes with the unforced thunk, and the subsequent seq forces the thunk into a value that is no longer connected to any output operation. The difference between the correct and incorrect pattern is purely the order of evaluation in the action sequence, which Miranda’s lazy evaluation makes invisible in the source code unless the programmer explicitly understands the demand semantics. The Miranda developer retainer task of I/O ordering analysis: for every file-write or output action, identify all expressions that will be serialized; trace the evaluation of each expression back through the program to its definition; verify that seq forces each expression to at least WHNF before the output action; and restructure the action sequence to guarantee the correct forcing order.

The distinction between WHNF and full normal form (NF): WHNF forces the outermost constructor of a value, leaving inner thunks potentially unevaluated. Full normal form forces every thunk in every position in the value, producing a completely evaluated structure. For integers and booleans, WHNF and NF are the same (there are no inner constructors). For lists, WHNF forces the spine one step but leaves elements as thunks; NF forces the entire list including all elements. The Miranda show function requires full normal form of its argument to produce a complete string representation, because it must serialize every element of a list, every field of a structure, and every argument of a constructor. Using seq to force a list to WHNF before calling show only forces the first spine step; inner elements may still be thunks that show must force during serialization. For simple values (integers, booleans, characters), one seq is sufficient. For structured values (lists of integers, nested data structures), a deep forcing function or a full-NF evaluation strategy is required before the final output. This is the deeper version of the lazy I/O ordering retainer problem: not just seq placement, but seq depth.

Miranda list comprehensions, infinite streams, and hd/tl operations

Miranda introduced list comprehensions (independently of SETL, which had a similar notation) as the primary syntax for constructing lists by transforming and filtering other lists. The list comprehension syntax: [expression | generator, generator, ..., guard, guard, ...]. A generator has the form pattern <- list and introduces a variable (or pattern) that ranges over the elements of the list. A guard is a boolean expression that filters elements: only combinations of generator values for which all guards are true appear in the output list. Example: [x*x | x <- [1..n]] produces the list of squares of 1 through n. With a guard: [x | x <- [1..n], x mod 2 = 0] produces the list of even numbers from 1 to n. With multiple generators: [(x,y) | x <- [1..n], y <- [1..x]] produces all pairs (x, y) where 1 ≤ yxn, with x as the outer loop and y as the inner loop. The generators are nested loops; each generator ranges over all its values for each combination of the outer generators.

List comprehensions over infinite lists: since Miranda uses lazy evaluation, a list comprehension can take an infinite list as a generator and produce another (potentially infinite) lazy list. [x | x <- [1..], x mod 3 = 1] generates an infinite list of positive integers that are congruent to 1 modulo 3: 1, 4, 7, 10, .... The comprehension is lazy: elements are produced only as demanded, and the guard condition is checked one element at a time. The important constraint: for a list comprehension over an infinite generator to terminate when used with take n, the guard must pass infinitely many elements of the generator. If the guard passes only finitely many elements (or none), a program attempting to take more elements than pass the guard will diverge, consuming the infinite generator looking for more passing elements. The Miranda developer retainer task of infinite stream comprehension design: for every list comprehension with an infinite generator, verify that the guard condition passes infinitely many elements; add a take n bound before any comprehension that filters an infinite generator with a potentially-exhausting guard; and document the mathematical property that ensures the guard passes infinitely many elements.

List operations in Miranda: hd returns the first element of a list (hd [1,2,3] = 1); tl returns the list without its first element (tl [1,2,3] = [2,3]); both are partial functions that raise an error on the empty list. take n xs returns the first n elements of xs (or all elements if fewer than n exist). drop n xs returns xs without the first n elements. zip xs ys produces a list of pairs, one pair per corresponding position in xs and ys, stopping at the shorter list. zip2 f xs ys is a generalized zip that applies function f to corresponding elements rather than pairing them. unzip xys splits a list of pairs into a pair of lists. The arithmetic sequence notation: [1..n] generates the list of integers from 1 to n; [1..] generates an infinite arithmetic sequence starting at 1; [1,3..n] generates the arithmetic sequence 1, 3, 5, ... up to n (with step determined by the first two elements); [1,3..] generates an infinite arithmetic sequence with step 2. The string type in Miranda: a string is a list of characters (string = [char]), so all list operations apply to strings. hd "hello" = 'h'; tl "hello" = "ello"; #"hello" = 5 (the length operator # applied to a string).

Miranda algebraic types, pattern matching, and where clauses

Miranda’s algebraic type declarations: typename ::= Constructor1 | Constructor2 Type | Constructor3 Type Type | .... Each constructor is a data constructor that takes zero or more arguments of the specified types. Example: a binary tree type: tree * ::= Leaf | Node (tree *) * (tree *) declares a polymorphic tree type with a Leaf constructor (no arguments) and a Node constructor taking a left subtree, a value of type *, and a right subtree. The type variable * makes the tree polymorphic: a tree num is a tree of numbers; a tree bool is a tree of booleans. Function definitions over algebraic types use pattern matching on the left-hand side: depth Leaf = 0; depth (Node l v r) = 1 + max (depth l) (depth r). Each equation of a function definition matches a specific constructor pattern; Miranda tries the equations in order and uses the first one whose left-hand side pattern matches the argument. The wildcard pattern _ matches any value and binds nothing: is_leaf Leaf = True; is_leaf _ = False.

Guards in pattern matching: a function equation can include a boolean guard condition: f x | condition1 = result1 | condition2 = result2 | otherwise = default. Guards are evaluated in order; the first guard that is true determines the result. The otherwise guard is always true (it is defined as otherwise = True) and serves as a catch-all. Guards and patterns are orthogonal: a function equation can have both a pattern on the left-hand side and a guard: f (Node l v r) | depth l = depth r = balanced_result | otherwise = unbalanced_result matches only Node constructors and then further discriminates by the depth equality condition. Where clauses: a function equation can include local definitions that are visible only within that equation: f x = result where result = complex_expression; complex_expression = .... Where clauses in Miranda are mutually recursive by default: all definitions in a where clause can reference each other. This is ALGOL 60’s nested procedure declaration pattern in a functional syntax.

Pattern matching completeness: Miranda does not statically check that pattern equations cover all constructor cases of an algebraic type. A function with incomplete patterns will raise a runtime error when applied to an unmatched constructor. The runtime error message names the function and the unmatched value. The Miranda developer retainer task of pattern matching completeness analysis: for each function definition over an algebraic type, verify that the pattern equations cover all constructors of the type, including the recursive and empty cases; add a catch-all equation with a meaningful error message if the omitted cases are genuinely impossible; and add the missing constructor cases if they represent valid inputs that the function should handle. Common incomplete-pattern scenarios: a list-processing function that handles the cons case (f (x:xs) = ...) but omits the empty list case (f [] = ...), diverging on empty-list input; a tree function that handles the Node case but omits Leaf; a union type function that handles two out of three constructors. These omissions are structurally similar to Java’s missing default case in a switch statement, but Miranda’s dynamic pattern matching makes them runtime errors rather than compile-time warnings.

Miranda’s type system: Hindley-Milner inference and type signatures

Miranda uses the Hindley-Milner type inference algorithm, which infers the most general (most polymorphic) type for every expression without requiring type annotations. A function like identity x = x is inferred to have the polymorphic type identity :: * -> * without any annotation. The type variable * is universally quantified (for all types T, identity :: T -> T). A function that computes the length of a list: len [] = 0; len (x:xs) = 1 + len xs is inferred to have type len :: [*] -> num (for all element types, takes a list and returns a number). Type signatures: a programmer can optionally annotate a function with its type using the :: notation: len :: [*] -> num. Miranda checks the annotation against the inferred type; a mismatch is a type error. Type signature annotations serve as documentation, as executable type assertions, and as a way to constrain polymorphism when a more specific type is intended. Miranda’s type system is strong and static: all type errors are detected at compile time, and no runtime type errors occur (except for incomplete patterns, which are a pattern-matching failure, not a type error).

The show and shownum functions: show converts any value to its Miranda source representation as a string; shownum converts a number to a string (equivalent to Python’s str() for numeric values). The error function: error :: string -> * takes an error message and terminates the program with the message displayed. The sys_message function provides a way to write a string to standard output as a side effect within an otherwise pure Miranda computation. Miranda’s I/O model uses a dialogue approach: a Miranda program is a function from a list of system responses (sys_message outputs) to a list of requests (read/write actions). This is the continuation-passing I/O style that Haskell replaced with monadic I/O in the early 1990s. The dialogue style makes I/O ordering explicit in the list structure but makes lazy evaluation ordering bugs particularly dangerous: the list of requests is evaluated lazily, so a request that depends on a forced value must appear after the seq that forces the value in the request list.

Miranda’s design legacy: the direct predecessor of Haskell

Miranda’s historical significance is its direct influence on Haskell. Before Haskell, Miranda was the dominant lazy purely functional language in academic use, and the Haskell committee explicitly designed Haskell to supersede Miranda while incorporating its core design decisions. Miranda’s specific contributions to Haskell: lazy evaluation as the default evaluation strategy (Haskell adopted this from Miranda, in contrast to the strict evaluation of ML and the concurrent Haskell predecessor languages); algebraic data types with constructor pattern matching (adopted by Haskell, then by Scala, Rust, Swift, and Kotlin); list comprehensions (adopted by Haskell, Python, and many subsequent languages); the Hindley-Milner type inference system (adopted by Haskell, extending it with type classes); the where clause for local definitions (adopted by Haskell identically); and the string-as-list-of-characters representation (adopted by Haskell, later identified as a performance weakness and supplemented with Data.Text and Data.ByteString). Haskell’s type class mechanism (Wadler and Blott, 1989) was the primary extension beyond Miranda: type classes provide ad-hoc polymorphism (what object-oriented languages call interfaces) while preserving the Hindley-Milner inference guarantee. Miranda does not have type classes; overloading in Miranda is handled by separate function names or by explicit type dispatch.

Miranda remains in use in two principal contexts: academic courses on functional programming at UK universities (Miranda’s clean syntax and purely functional semantics make it pedagogically effective for teaching functional programming concepts without the complexity of Haskell’s type class system and IO monad); and programming language theory research where Miranda’s simpler design is preferable to Haskell’s more complex feature set. The official Miranda implementation (mira) is distributed by Research Software Ltd and continues to be maintained. Miranda programs run in the mira interactive environment, which provides a read-eval-print loop for interactive development and a batch compilation mode for production programs. The % directive system in Miranda scripts: %include "filename" includes another Miranda script; %free { typename ::= ... } declares a free type parameter for a module; %export name1 name2 ... controls which definitions are visible to including scripts.

How HourTab tracks Miranda developer retainer hours

Miranda retainer work shares the invisible-work problem common to all functional programming language retainers, with the additional challenge that Miranda’s most important retainer tasks — lazy evaluation I/O ordering analysis with seq placement, infinite stream consumption bounding for comprehensions with potentially-exhausting guards, algebraic type pattern matching completeness audit, Hindley-Milner type inference debugging for polymorphic function designs, and dialogue-style I/O action sequence ordering — produce diffs whose surface area is small relative to the analytical work required. Moving seq elem (write_file elem) to place the file-write as the second argument of seq rather than a separate preceding action is a diff with a changed expression structure; the value is correct forcing of the stream element to WHNF before the file-write action, elimination of thunk-representation serialization for all three wrong-output cases per run, and an I/O ordering design that correctly separates the forcing phase from the output phase. Adding a take 1000 bound before an infinite stream comprehension with an uncertain guard is a diff with one function application; the value is bounded computation time for all guard outcomes, elimination of potential infinite computation for guards that pass few elements, and a stream consumption design that is safe to test and deploy without risk of non-termination.

HourTab gives Miranda developers a public retainer-hours URL they send to clients — typically academic computing groups teaching functional programming at UK universities where Miranda has maintained a pedagogical presence since the 1980s, programming language research groups studying lazy evaluation semantics and type inference algorithms, and Haskell developers who work with Miranda implementations for historical comparison or porting legacy code — at the start of an engagement. For Miranda retainers, each work log entry should name the mechanism (seq placement analysis for lazy I/O ordering; WHNF forcing semantics audit across I/O action sequences; lazy stream consumption bounding with take/drop; thunk evaluation order for file-write dependencies; list comprehension design over finite and infinite lazy streams; guard condition design with boolean predicates; zip/unzip/zip2 stream operation design; hd/tl selector with termination conditions; typename ::= Constructor1 | Constructor2 Type algebraic type declaration; function equation pattern matching with left-hand patterns and wildcard _; guard condition | clause design; where clause local definition design; show/shownum value display; error/sys_message I/O effects; Hindley-Milner type inference for polymorphic function design; :: type signature annotation; forall quantification in polymorphic types), the specific function name and the lazy evaluation order or pattern matching completeness problem, and the before/after observable metric. Miranda retainers are often compared to Haxe developer retainers for cross-target functional language work, to Scheme developer retainers for functional programming language engineering with explicit evaluation order concerns, and to Elm developer retainers for purely functional language systems with algebraic types and exhaustive pattern matching. The distinction from Elm is the evaluation strategy: Miranda’s lazy evaluation creates I/O ordering bugs that are structurally impossible in Elm’s strict-evaluation purely functional model, and the Miranda developer retainer task of seq placement analysis has no direct equivalent in Elm work. HourTab’s work log makes the lazy evaluation order analysis, seq placement restructuring, and infinite stream bounding design visible to clients who would otherwise see only the symptom — wrong string representations in file output or non-terminating computations — and not understand why the fix required understanding Miranda’s thunk semantics and the demand-driven evaluation that makes seq placement the critical ordering invariant.

Track Miranda developer retainer hours without the status emails

HourTab gives Miranda 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: Miranda developer retainers

What does a Miranda developer on retainer typically do?

A Miranda developer on monthly retainer covers four principal service areas: lazy evaluation and I/O ordering analysis (seq placement for WHNF forcing before file-write actions; lazy stream consumption bounding with take/drop; thunk evaluation order audit for all output operations; dialogue-style I/O action sequence design); list comprehension and stream design (list comprehensions over infinite lazy streams with generator and guard clauses; zip/unzip/zip2 stream operations; hd/tl/take/drop selectors; arithmetic sequence [a..b]/[a,b..] notation); algebraic type and pattern matching design (typename ::= Constructor1 | Constructor2 Type declaration; function equation pattern matching with left-hand patterns and wildcard _; guard condition | clauses; where clause local definitions; show/shownum/error/sys_message I/O); and type system design (Hindley-Milner type inference; :: type signature annotation; polymorphic type variables; %include/%export module system).

What Miranda work is most commonly underlogged in a retainer?

Lazy evaluation I/O ordering repair (seq placed after file-write action instead of before; file received unevaluated thunk string representation; 3 wrong file outputs/run; moved seq elem (write_file elem) to force before write; wrong outputs: 3/run → 0; 14–25 hrs invisible in lazy evaluation order analysis, WHNF forcing semantics audit, and seq placement redesign), infinite stream comprehension bounding (comprehension guard exhausted infinite generator seeking next True element; non-termination before fix; added take 1000 bound; 10–18 hrs invisible in lazy stream consumption analysis), and algebraic type pattern matching completeness repair (function missing Empty constructor case equation; 4 runtime errors/session; added Empty case; runtime errors: 4/session → 0; 8–15 hrs invisible in pattern exhaustiveness analysis).

What are typical Miranda developer retainer rates?

Entry-level Miranda developers (1–2 years, list comprehensions, basic algebraic type declarations, pattern matching equations, hd/tl/take/drop stream operations) bill at $60–$110/hr. Mid-level Miranda engineers (2–4 years, lazy evaluation I/O ordering analysis, seq placement for WHNF forcing, infinite stream consumption bounding, Hindley-Milner type inference, algebraic type pattern matching completeness) bill at $105–$190/hr. Senior Miranda architects (4–8 years, complete Miranda program architecture, complex lazy stream pipeline design, type system edge cases in polymorphic inference, % directive script organization, Miranda compiler and runtime behavior) bill at $160–$290/hr. Monthly retainer ranges: $1,800–$4,500/mo advisory (15–25 hrs), $6,500–$16,000/mo for full Miranda functional programming engagements.

What should a Miranda developer retainer agreement include?

A Miranda developer retainer agreement should specify: lazy evaluation scope (seq placement analysis for I/O ordering; WHNF forcing semantics audit; lazy stream consumption bounding; thunk evaluation order for file-write dependencies; infinite stream termination condition design); list comprehension scope (list comprehension design over finite and infinite lazy streams; guard condition design; zip/unzip/zip2/take/drop stream operations; arithmetic sequence notation); algebraic type scope (typename ::= Constructor1 | Constructor2 Type declaration; function equation pattern matching; guard condition | clauses; where clause local definitions; show/shownum/error/sys_message I/O); type system scope (Hindley-Milner type inference; :: type signature annotation; polymorphic type variables; %include/%export module system); and hour logging format (function name; operation type — lazy evaluation order, list comprehension, algebraic type, or type system; before/after error metric; Miranda version).

How should Miranda developer retainer hours be logged?

Log each Miranda retainer session with: advisory category (seq placement analysis for lazy I/O ordering; WHNF forcing semantics audit; lazy stream consumption bounding; thunk evaluation order for file-write dependencies; list comprehension design over infinite lazy streams; guard condition design; zip/unzip/zip2 stream operations; hd/tl/take/drop selectors; typename ::= Constructor1 | Constructor2 Type algebraic type declaration; function equation pattern matching; guard condition | clauses; where clause local definitions; show/shownum value display; error/sys_message I/O effects; Hindley-Milner type inference; :: type signature annotation; %include/%export module directives), the specific function name and the lazy evaluation order or pattern matching completeness problem (seq placed after file-write action; file received unevaluated thunk string representation; 3 wrong file outputs/run; moved seq elem (write_file elem); wrong outputs: 3/run → 0), and the before/after metric. Include Miranda version, implementation, and whether the fix required seq placement change, stream consumption bounding, pattern equation completion, or type annotation correction.