par_map: Humanizing the Common Case
spawn and a channel are the assembly language of parallelism — correct, but not what most code should have to write. A parallel map is the shape people actually reach for, and Mere gets it on all four backends without writing it four times: par_map desugars into spawn, channel, and an ordinary map. It inherits every guarantee from the primitives it lowers to — safety, byte-identical parity — for free, and one subtlety from an earlier decision forces the desugaring to happen per call site.
spawn and channel now run safely on every backend, but they are the raw
primitives — the assembly language of parallelism. Writing a parallel computation
with them means manually fanning work out to spawned threads and fanning the results
back through channels, by hand, every time. That is fine for a primitive and hostile
as an everyday tool. This short episode is about the humane form most code actually
wants — a parallel map — and about getting it on all four backends without writing
it four times.
The shape people reach for
The everyday parallel operation is a map: apply a function to every element of a
list, but do the elements at the same time. par_map gives exactly that — take a
function and a list, and get back the list of results, computed in parallel:
par_map heavy_work items
One line replaces the hand-written fan-out and fan-in. A data-parallel program that had spelled out “spawn a worker per item, collect through channels in order” becomes a single call. That is the whole point of a combinator: the primitive gives you control, the combinator gives you the common case without ceremony.
Built by desugaring, not by four implementations
The tempting way to add par_map would be to implement it in each backend’s runtime
— four more pieces of concurrency code to write and keep in agreement. Mere did the
opposite. par_map is realized by desugaring: a saturated call is rewritten, as
the program is parsed, into a combination of things that already exist. Roughly, each
element gets a worker that sends its result down a fresh channel, the channels are
collected into a list in order, and then each channel is received from in that same
order — a fan-out map followed by a fan-in map, expressed with spawn, channel,
and the ordinary list map.
Because spawn, channel, and map already run on all four backends, this single
rewrite makes par_map run on all four backends for free. No new code generation,
no fourfold implementation, no new opportunity for the backends to disagree. This is
the general power of desugaring: express the new construct in terms of ones that
already work, and it inherits their working-ness whole. The ordering guarantee comes
from collecting channels into a list; the safety comes from the primitives; the
byte-identical parity comes from the primitives too. par_map did not have to earn
any of those — it was handed them by what it lowers into.
Why the desugaring happens per call site
There is a subtlety here, and it is a direct consequence of a decision made two
episodes ago. Why not write par_map once, as an ordinary polymorphic function in
the standard library, in Mere itself? Because the Send bound would run straight
into the monomorphism restriction: a definition whose type variable still carries an
unmet Send obligation is not generalized, so a par_map written as a plain library
function would be pinned to a single element type per program — you could map over a
list of one type, but not, elsewhere in the same program, over a list of another.
That is far too restrictive for the one operation everyone will reach for.
Desugaring sidesteps it exactly. Because each par_map call is lowered at its own
call site, with that site’s concrete types, every use gets its own specialized
expansion — and the operation is genuinely polymorphic across the program, one type
here and another type there, with no monomorphism restriction biting, because there
is no single generalized definition to restrict. The earlier choice — keep the type
system solver-less by refusing to generalize Send-constrained variables — had a
cost, and this is where the cost is paid and worked around: the polymorphic
combinator is delivered by lowering rather than by a library function. The decisions
compose; the shape of one answer is set by an answer three episodes back.
What comes along for free
Everything the primitives guarantee, the desugared par_map guarantees without extra
work. The Send requirement is still enforced — the results travel through a
channel, whose element type must be sendable, and a par_map whose function returns a
non-sendable value is rejected, exactly as the raw channel would reject it. Move
tracking needs no change either: the function passed to par_map is an ordinary,
possibly-many-times closure, so trying to move an owned capability into it is already
forbidden by the multi-run rule, which is exactly right because the function runs once
per element. And the four backends still produce the identical result, verified on a
real parallel program, because they are all running the same lowered primitives.
The method it establishes outlasts this one combinator: a high-level parallel
operation can be delivered by desugaring it into spawn and channel, inheriting
safety and parity from below, and future combinators can follow the same road rather
than being built four times each.
par_map is the humane surface over safe primitives — and its existence raises the
question the next episode is really about. The concurrency roadmap had more on it:
atomics, mutexes, the lower-level shared-state machinery a “complete” concurrency
story is supposed to include. Do they need to be built? The answer came not from a
checklist but from demand — from asking which programs actually require them. Next,
the close of Part VII: the decision not to build something.