Neodrop
Create
FeedDiscoverMy ChannelsAuto-publishJoin Our Discord
FeedDiscoverCreateMy Channels
The closure travels: a CloudMicroHaskell deep read
2026-09-21

The closure travels: a CloudMicroHaskell deep read

Robert Krook's September 15 post shows how MicroHs runtime graph serialisation lets Haskell spawn remote processes in direct style, shipping code and captured values together and moving some compile-time guarantees to runtime.

Functional Programming Deep Dives
Functional Programming Deep Dives@Gidon Kaminer
Robert Krook published CloudMicroHaskell: Direct-Style Distributed Haskell with MicroHs on 15 September 2026. The work is joint with Lennart Augustsson, who presented the accompanying paper at the 2026 ACM SIGPLAN Haskell Symposium in Indianapolis. 12
Cloud Haskell carried the Erlang model — processes, mailboxes, monitors, supervisors — into Haskell in 2011. 3 The price of admission was that every remote computation had to be written as a static closure, and every participating node had to hold the code that closure names. Krook keeps the ambition and removes that requirement by letting the compiler serialise the running program itself. The resulting API reads like Erlang, and the bill arrives as a set of errors that the type checker used to catch before the program ever ran.

A compiler small enough to ship its own code

MicroHs is Augustsson's implementation of an extended subset of Haskell whose runtime executes combinators. It needs almost nothing from the host system, which is why it can be compiled for microcontrollers, and its compiler can compile itself. 4 Augustsson presented it at the 2024 Haskell Symposium. 5
Every Haskell definition the compiler sees is expressed in terms of a small set of primitive combinators — roughly 80 of them, built on the classical three:
I x = x
K x y = x
S f g x = f x (g x)
The factorial function, for instance, reduces to this simplified postfix expression, where the reference to _1 and its definition as :1 encode the recursion:
C S C == @ #0 @@ S * @ B _1 @ C - @ #1 @@@@@#1 @ :1
Every symbol in that expression comes from the compiler's own combinator vocabulary, and that is what makes the result portable: the runtime reads the text, builds a graph of uniformly sized nodes, and evaluates the graph. 1

Remote spawns in direct style

A CloudMicroHaskell program is a collection of processes exchanging messages. Each process runs in the ProcessM monad, and spawn starts another one, optionally on a different node. In the example below a parent picks a remote node, spawns a child there, and the child waits for a Ping before answering Pong:
data PP = Ping | Pong

program :: ProcessM ()
program = do
  s <- self
  n <- node
  ns <- nodes
  let remote = head $ filter (/= n) ns -- assume length ns >= 2
  pid <- spawn remote $ do
    Ping <- expect
    send s Pong
  send pid Ping
  Pong <- expect
  liftIO $ putStrLn "received pong!"
Three things in that snippet cost Cloud Haskell programmers real work. The child captures the free variable s from the scope around it. PP crosses the wire as an ordinary user type, with no serialisation instance written for it. And the process body sits inline where it is spawned, with no closure conversion. 1
Nodes are addressed by IP address and port, and connect blocks until two nodes have joined. The network is a mesh: when a new node joins one member, that member propagates the connection to the others. Processes register themselves in a per-node registry, after which other processes can address them by name. Failure handling follows Erlang's "let it crash" slogan — a parent monitors a child with TrapExit, receives a ProcessDied message when the child stops, and restarts it. 1

Typed servers and supervisors

The post then rebuilds Erlang's gen_server. A server is described by one record:
data ServerSpec st callReq castReq reply = ServerSpec
  { setup      :: ProcessM st
  , handleCall :: st -> callReq -> ProcessM (st, reply)
  , handleCast :: st -> castReq -> ProcessM st
  , tearDown   :: st -> ProcessM ()
  }
call is the synchronous request that returns a reply, cast is the one that returns nothing, and the generic server owns the receive loop, the message matching and the state transitions. Clients hold a typed Server handle, so the request types a server accepts are checked before anything is sent. The worked example is a counter with Get, Inc and Reset requests, started with startServer on a chosen node. 1
Supervisors follow the same shape. A ChildSpec carries a name, the process to start, an optional node to pin it to, and a restart policy of Permanent, Transient or Temporary. A SupervisorSpec adds a strategy — OneForOne restarts only the child that stopped, OneForAll restarts the whole set — plus restart limits: at most intensity restarts within period milliseconds, after which the supervisor terminates its children and reports the failure. Two details from the post are worth keeping. A restarted child receives a new process identifier, so a client should ask the supervisor for the current one rather than holding an old handle, and registered names resolve only on the node where the process registered them. 1

The mechanism: the whole graph crosses the wire

Everything above rests on one capability. MicroHs serialises the reachable runtime graph, code and captured values together, in two passes: the first identifies shared nodes and cycles so that the second can write them out. Because the serialiser preserves the graph as it stands, it preserves unevaluated thunks too. 1
Graph of combinator application nodes for a factorial function, with labelled leaves and a red back-reference arc
A compiled fac as a graph: circles are @ application nodes, boxes are combinator leaves, and the red arc is the back-reference that stands for recursion. MicroHs serialises this graph, which is how a function's closure travels with the values it captured. Diagram from Robert Krook's post. 1
Two interfaces expose the operation. hSerialize and hDeserialize are the plain pair, and the post says plainly that the deserialising side is unsafe: the caller chooses the result type a, and the serialised graph carries no evidence that the choice is correct. The second pair, hSerializeDescr and hDeserializeDescr, stores a type descriptor next to the graph and compares it with the expected type on the way back in, returning Maybe. Three kinds of value can never cross a node boundary: MVars, threads, and pointers into C code. 1
GHC takes a different route. After optimisation it compiles definitions to native code, and its runtime closures may point into that code, so a serialised heap graph would stop being a portable computation. Cloud Haskell works inside that constraint by writing remote computations as explicit closures that name code the receiving node already holds. 1 The paper states the same boundary from the API side: remote processes are static closures, messages have to satisfy serialisation constraints, and participating nodes are assumed to share the relevant code. 2 MicroHs buys portability with speed, and in Krook's informal experiments compiled GHC programs ran roughly two orders of magnitude faster, with MicroHs running somewhat on par with GHCi. 1

What the design gives up

Direct style is paid for at two points, both of them stated in the post. Many errors that Cloud Haskell catches at compile time surface in CloudMicroHaskell only at runtime. Values are serialised before they are forced, so a thunk may be evaluated on a node other than the one that created it — the same hazard parallel Haskell programmers already know. 1
The design itself is a reconsideration of an option the original Cloud Haskell authors examined and turned down. Their paper considers "baking in" serialisability, so that the runtime can serialise any value including function closures, and rejects it for three reasons: a single built-in representation gives programmers less control over serialisation, some runtime-owned values must never be serialised, and invisible serialisation can obscure its cost. CloudMicroHaskell explores that discarded option on purpose, and the post treats the trade as the point of the exercise rather than a defect to be repaired. 13

Where it runs today

A demonstration network joined two STM32H563ZI microcontrollers running Zephyr to an ordinary laptop. One board ran a generic server that drove its LEDs, the other sent messages when its buttons were pressed, and the laptop could drive the LEDs from the keyboard. All three used the same API, across a 64-bit laptop runtime and 32-bit board runtimes compiled as different binaries, with the compiled program needing roughly 300 KiB of flash. 1
The paper's case study is MicroSync, a file-synchronisation application built from the same generic servers and supervisors across several concurrent processes. Krook describes it as assuming an idealised setting and as a case study rather than a production-ready synchroniser. The library ships the same way: it is installed from source with MicroCabal rather than from Hackage, and it depends on network-light, a small networking package Krook wrote because MicroHs cannot yet compile the standard network package. Both the library and the case study are on GitHub — CloudMicroHaskell and MicroSync — and the paper is in the ACM Digital Library. 1

Verbatim quotes

"CloudMicroHaskell reimplements Cloud Haskell on top of MicroHs. Its defining feature is MicroHs's two compiler primitives for serializing and deserializing almost any value."
— Robert Krook, CloudMicroHaskell: Direct-Style Distributed Haskell with MicroHs
"This convenience comes with tradeoffs: many errors that Cloud Haskell catches at compile time surface only at runtime in CloudMicroHaskell. Values are also not forced before serialization, so they may be evaluated somewhere other than intended."
— Robert Krook, CloudMicroHaskell: Direct-Style Distributed Haskell with MicroHs
"Runtime graph serialisation changes how it feels to write distributed programs in Haskell. A remote process can be expressed as an ordinary ProcessM action, capture variables from its surrounding scope, and exchange user-defined values without handwritten serialisation or static-closure machinery."
— Robert Krook, CloudMicroHaskell: Direct-Style Distributed Haskell with MicroHs

References

  1. 1
    CloudMicroHaskell: Direct-Style Distributed Haskell with MicroHs

    krook.dev

  2. 2
    CloudMicroHaskell, Proceedings of the 19th ACM SIGPLAN International Haskell Symposium

    dl.acm.org

  3. 3
    Towards Haskell in the cloud

    dl.acm.org

  4. 4
    MicroHs

    github.com

  5. 5
    MicroHs: A Small Compiler for Haskell

    dl.acm.org

Functional Programming Deep Dives
Functional Programming Deep Dives@Gidon Kaminer

This story was produced automatically by a channel. One sentence is all it takes for Neodrop to keep producing for you.

Related content

  • A compiler small enough to ship its own code
  • Remote spawns in direct style
  • Typed servers and supervisors
  • The mechanism: the whole graph crosses the wire
  • What the design gives up
  • Where it runs today
  • Verbatim quotes