Clean
The purely functional graph-rewriting language from Radboud University whose uniqueness type system made destructive updates and real-world I/O safe without monads.
Created by Rinus Plasmeijer, Marko van Eekelen, and the Software Technology Research Group at Radboud University Nijmegen
Clean is a purely functional, lazy programming language developed since 1987 by the Software Technology Research Group at Radboud University Nijmegen in the Netherlands. It looks, at a glance, like a cousin of Haskell – and it is – but it rests on two ideas that set it apart from every other language of its generation: computation is defined as graph rewriting rather than term rewriting, and side effects are made safe by a uniqueness type system rather than by monads. That second idea, invented for Clean, is the ancestor of the linear and ownership types that later reached far larger audiences.
History & Origins
From parallel graph rewriting to a language
In the mid-1980s, the Nijmegen group led by Rinus Plasmeijer was working on the Dutch Parallel Reduction Machine project, investigating how to execute functional programs efficiently on parallel hardware. That work produced Lean, an experimental intermediate language based on term graph rewriting – a computational model in which programs manipulate graphs rather than trees of terms, so that sharing of subexpressions is explicit and controllable rather than an accident of the implementation.
Clean grew directly out of that research. Development began in 1987, and the system was originally distributed as the Concurrent Clean System, reflecting its origins in parallel evaluation. Where most functional languages treat their internal graph representation as an implementation detail, Clean elevated it into the semantics: the language is defined by rewriting a graph, and the programmer can reason about sharing and copying because those things are visible in the model.
Plasmeijer and Marko van Eekelen documented the theory at length in Functional Programming and Parallel Graph Rewriting (Addison-Wesley, 1993), which remains the standard reference for Clean’s foundations.
Maturity and the Clean 2 era
Clean 1.0, released in May 1995, was the first version considered mature enough for general use. Over the following years the language accumulated a native-code compiler for several platforms, an integrated development environment on Windows, and the ObjectIO library for building graphical applications in a purely functional style.
The Concurrent Clean Language Report version 2.0, published by Plasmeijer and van Eekelen in December 2001 (revised as version 2.1 in November 2002), defined the Clean 2 language. This is the specification most Clean code still targets, and Clean 2 brought the language closer to Haskell in surface syntax – type classes, records, and a module system that a Haskell programmer can read – while keeping uniqueness typing and graph rewriting at its core.
Task-Oriented Programming
The most consequential thing built in Clean arrived in 2007, when Plasmeijer, Peter Achten, and Pieter Koopman presented iTasks at ICFP in Freiburg. iTasks is a combinator library for specifying interactive, multi-user workflow systems; from a declarative description of what work needs doing, by whom, and in what order, the system derives the web interface, the data storage, and the coordination between participants. It became the seed of an entire research programme – Task-Oriented Programming, or TOP – that continues at Radboud today.
Design Philosophy
Clean is built on a small number of commitments, held more strictly than in most languages:
- Purity is non-negotiable. Every function is referentially transparent. There is no escape hatch.
- Effects are a typing problem, not a plumbing problem. Rather than sequencing effects through a monad, Clean tracks in the type system whether a value is uniquely referenced, and permits destructive update exactly when it is.
- The graph is part of the language. Sharing, cycles, and lazy nodes are semantic entities the programmer can talk about, not compiler internals.
- Efficiency is a design goal, not an afterthought. Clean compiles to native machine code, and the uniqueness information the type checker computes is fed directly to the code generator so that unique data structures can be updated in place instead of copied.
Key Features
Uniqueness typing
This is Clean’s signature contribution. A type annotated with * is unique: the compiler has proven that at that point in the program, only one reference to the value exists. Because nobody else can observe it, the runtime is free to overwrite it destructively, and the program remains referentially transparent.
The world itself is a unique value. A Clean program’s entry point receives the world and returns a new one:
module hello
import StdEnv
Start :: *World -> *World
Start world
# (console, world) = stdio world
console = fwrites "Hello, World!\n" console
(_, world) = fclose console world
= world
Each step consumes the unique *World (or *File) and produces a new one. Using the old world twice would be a type error, which is precisely what prevents the program from observing the effect of a destructive update. The # notation is Clean’s let-before construct, designed for exactly this style: it lets a name be rebound in sequence, so the threading of unique state reads like a sequence of statements while remaining pure function application underneath.
Arrays and files work the same way. A unique array can be written in place with no copying, which is why Clean can express array-heavy numerical code without giving up purity.
Graph rewriting semantics
Clean’s evaluation model is term graph rewriting. A definition such as
ones :: [Int]
ones = [1 : ones]
builds a genuinely cyclic graph rather than an infinitely unfolding tree. Because sharing is explicit in the model, the programmer can reason about which computations are performed once and which are repeated – a level of control that lazy languages built on term rewriting tend to leave to the implementation’s whims.
Lazy by default, strict where you ask
Clean is lazy, but strictness is a first-class annotation rather than a pragma bolted on:
:: Point = { x :: !Real, y :: !Real }
sum :: !Int ![Int] -> Int
sum acc [] = acc
sum acc [x:xs] = sum (acc + x) xs
Strictness annotations on function arguments and record fields let the compiler avoid building thunks entirely, which matters both for performance and for avoiding space leaks in accumulating loops.
Familiar functional machinery
Everything a functional programmer expects is present: algebraic data types, pattern matching, guards, higher-order functions, currying, type classes, and list comprehensions. Clean’s comprehension syntax uses \\ where Haskell uses |:
primes :: [Int]
primes = sieve [2..]
where
sieve [p:xs] = [p : sieve [x \\ x <- xs | x rem p <> 0]]
Note also that Clean writes cons patterns as [x:xs] rather than (x:xs), and uses :: for type definitions where Haskell uses data.
Generic programming and dynamics
Clean was an early adopter of generic (datatype-generic) programming, where a function is defined once over the structure of types and then derived for any concrete type:
:: Tree a = Node a (Tree a) (Tree a) | Leaf
derive gEq Tree
derive gPrint Tree
The related dynamics feature allows values – including functions – to be packaged with their types, serialized, shipped to another machine or another program, and type-checked on arrival. This is what makes iTasks able to move live computation between server and client.
The ABC machine
Clean compiles through an intermediate abstract machine called the ABC machine, an imperative model with three stacks – the A(rgument) stack, the B(asic value) stack, and the C(ontrol) stack – plus a graph store. Native code generators translate ABC code to machine code. Keeping the ABC layer as a well-defined intermediate representation later paid off unexpectedly: in 2019, Camil Staps, John van Groningen, and Plasmeijer presented an ABC bytecode interpreter at IFL that runs in WebAssembly, so Clean expressions can be serialized and evaluated in a web browser alongside natively compiled code.
Evolution
| Milestone | When | Significance |
|---|---|---|
| Development begins | 1987 | Grows out of Lean and parallel graph rewriting research at Nijmegen |
| Clean 1.0 | May 1995 | First mature release |
| Language Report 2.0 | December 2001 | Defines the Clean 2 language |
| Language Report 2.1 | November 2002 | Refined Clean 2 specification; the 2.1 release followed in October 2003 |
| iTasks at ICFP | 2007 | Task-Oriented Programming begins |
| Clean 2.4 | December 23, 2011 | Last of the widely used 2.x line |
| Clean 3.0 | October 2, 2018 | Major version bump |
| Clean 3.1 | January 5, 2022 | Current stable release |
The pattern is characteristic of a research language: long, quiet stretches of stability punctuated by releases that consolidate years of work. The interesting motion since 2018 has been less in the core language than around it – the WebAssembly backend, the continuing iTasks line, and a modernised distribution story.
Current Relevance
The Clean System is distributed for Windows, Linux, and macOS; according to the official distribution, builds are offered for 32- and 64-bit Windows, 32- and 64-bit Intel Linux, 64-bit ARM Linux, and 64-bit Intel macOS. Availability of any particular build varies between releases, so it is worth checking the download page rather than assuming a target is covered. Alongside the classic distribution, the Clean and iTasks developers maintain clean-lang.org, a package registry paired with Nitrile, a package manager and build tool for Clean that resolves dependencies and drives builds in the style modern developers expect. Notably, the 3.1 stable release is reportedly not the version used with iTasks – iTasks development tracks the newer Clean releases distributed through that registry, which is worth knowing before installing.
Clean’s practical user base is small and centred on Radboud University and its collaborators. Activity is concentrated in the Task-Oriented Programming research line, the compiler and runtime, and teaching. Papers such as Clean for Haskell Programmers (Mart Lubbers and Peter Achten, 2024) exist precisely because the natural on-ramp to Clean today is from Haskell, and because the community is small enough that a short translation guide is the efficient way to bring newcomers in.
Why It Matters
Clean’s importance is out of all proportion to the number of people who write it.
It solved the effects problem differently, and first. When the lazy functional programming community was wrestling with how a pure language could do I/O, Clean’s answer was uniqueness typing – track the number of references in the type system, and destructive update becomes safe. Haskell went the monadic route and won the popularity contest, but Clean’s answer turned out to be the more general idea. Uniqueness types are the direct model for Idris’s uniqueness types, and the broader family of substructural type systems that Clean helped pioneer underlies the ownership and borrowing disciplines that later languages use to guarantee memory safety without a garbage collector.
It showed that purity need not cost performance. By feeding uniqueness information from the type checker into the code generator, Clean compiles purely functional array and file manipulation into in-place updates on native code. The argument that “pure means copying” was refuted by a working compiler decades ago.
It kept graph rewriting honest. Most lazy languages have a graph in the implementation and a tree in the semantics. Clean put the graph in the semantics, giving programmers a genuine handle on sharing.
Task-Oriented Programming is a real idea. iTasks demonstrated that a distributed, multi-user, interactive system can be specified declaratively – as a composition of tasks – and the interface, storage, and coordination derived rather than written. That line of work has drawn interest from Dutch defence-affiliated researchers – the Netherlands Coast Guard incident coordination prototypes came out of a collaboration involving the Netherlands Defence Academy – precisely because it shortens the path from a description of how coordination should work to a running system.
Clean is dormant in the sense that matters to industry: you will not find job postings for it, and its release cadence is measured in years. But it is very much alive in the sense that matters to language design. Its central invention escaped the language and is now load-bearing infrastructure elsewhere – which is arguably the most any research language can hope for.
Timeline
Notable Uses & Legacy
iTasks and Task-Oriented Programming
The iTasks framework, written in Clean, specifies multi-user, distributed workflow applications as composable tasks and generates the web interface, persistence, and coordination logic automatically. It remains the flagship demonstration of what Clean's type system makes possible.
Netherlands Coast Guard incident coordination (Incidone)
Bas Lijnse, Jan Martin Jansen, and Rinus Plasmeijer modelled the Netherlands Coast Guard's search-and-rescue workflow in iTasks and built Incidone, a task-oriented incident coordination tool written in Clean, presented at ISCRAM 2012. Lijnse and Jansen are affiliated with the Netherlands Defence Academy, and the work is the most concrete applied use of Task-Oriented Programming outside the university.
Radboud University teaching and research
Clean is the vehicle for functional programming courses and a long line of PhD research at Radboud University Nijmegen, covering uniqueness typing, generic programming, dynamic types, and compiler technology.
The Clean compiler itself
The Clean compiler, code generator, and much of its tooling are written in Clean, making the system a substantial self-hosted demonstration of the language handling state, file I/O, and code generation in a purely functional setting.
Programming language research on uniqueness and linear types
Clean is the canonical reference implementation of uniqueness typing, cited across the literature on linear types, ownership, and safe in-place update, and directly credited as the model for uniqueness types in Idris.