Dive C3.01.1
The BEAM you know
Name the runtime you already trust, exactly. The rest of the chapter measures F# against it.
Before mapping anything onto F#, this dive states the BEAM in its own terms — the runtime that executes Elixir. Four properties define it. The concurrency unit is a lightweight process: not an OS process and not a thread, but a runtime-managed entity that starts in a few kilobytes, so a single node holds hundreds of thousands of them. Each process has its own heap and is collected on its own, so one process's garbage collection does not pause the others. The scheduler is preemptive: it counts reductions (work units) and rotates processes so no single one starves the rest. And processes share nothing — they communicate only by sending messages, which lets OTP build supervision on top: when a process crashes, its supervisor restarts it, the "let it crash" stance. This is the baseline.
The process — the unit you spawn
The BEAM's unit of concurrency is the process. You create one with spawn, which
runs a function and hands back a process identifier — a pid. A process is cheap
enough that spawning one per connection, per job, or per piece of state is the normal design, not an
optimisation. Two processes share no memory: the only way one reaches another is to
send it a message, which the receiver picks up with receive.
# Elixir — spawn / send / receive parent = self() pid = spawn(fn -> receive do {:ping, from} -> send(from, :pong) end end) send(pid, {:ping, parent}) receive do :pong -> :got_it end # => :got_it
- process
- a runtime-scheduled entity with its own heap and mailbox — not an OS thread; it starts small and there can be very many.
- pid
- a process identifier; the handle
sendtargets. A process reaches another only through its pid. - send / receive
- the only channel between processes. The sent term is copied into the receiver's heap; nothing is shared.
- mailbox
- each process has one queue of incoming messages;
receivepattern-matches the next matching message.
State is a process that loops
Data is immutable, so a process holds state the functional way: by passing it as an argument to the next
turn of a recursive loop. The process receives a message, computes a new state, and calls itself with that
new state. A GenServer is this pattern packaged with callbacks — but the bare
version below shows the mechanism: state lives in the call stack of one looping process, reachable only by
message.
# Elixir — a counter held in a recursive loop defp loop(count) do receive do {:incr, n} -> loop(count + n) {:total, from} -> send(from, count); loop(count) end end counter = spawn(fn -> loop(0) end) send(counter, {:incr, 5}) send(counter, {:total, self()}) receive do total -> total end # => 5
MailboxProcessor — a mailbox and a
recursive async receive loop — which the next dive develops. The structure carries
over; what does not is the process model under it.Watch the counter process receive
Send the looping counter a message. {:incr, n} adds to its private state and
loops; {:total, from} sends the count back and loops unchanged. The state lives
inside the one process — no other process can read it without a message. The readout shows the message,
the new state, and what (if anything) the process sends back.
A process with its own heap and mailbox
The count is private to one looping process. Reaching it means sending a message and waiting for the reply — the only door in. That isolation is what makes a crash survivable: one process can fail without corrupting another's heap.
Preemptive scheduling, and "let it crash"
Two more properties finish the picture. First, scheduling is preemptive: the BEAM runs a small set of schedulers (by default one per CPU core) and gives each process a budget measured in reductions — roughly, function calls. When a process spends its budget, the scheduler sets it aside and runs another. A tight loop in one process cannot block the others, and there is no explicit yield to remember.
Second, the fault model is supervision. Because processes are isolated and linked, OTP can place a supervisor over a set of worker processes; when a worker crashes, the supervisor restarts it to a known state by a declared strategy. The slogan is "let it crash": a process that hits an unexpected state dies, and the supervisor brings back a clean one, rather than every function defending against every error. This is a property of the runtime and its libraries — it is the part C3.01.3 will show the CLR does not provide out of the box.
The BEAM gives
Cheap isolated processes, per-process GC, preemptive reduction-counted scheduling, and OTP supervision with "let it crash".
The question for F#
Which of these does the CLR keep, which does it answer differently, and which must an F# program rebuild? That is the next two dives.
Where this leads
That is the runtime an Elixir developer already trusts, named on its four axes. The next dive states the CLR on the same axes — managed execution, a shared generational collector, OS threads and the thread pool, and the type system — so the two can be read against each other. The third weighs what carries over.
References
Sources
- The F# Software Foundation — F# on .NET, the runtime this chapter maps the BEAM onto.
- Microsoft — F# documentation — the
MailboxProcessorthat meets the looping-process pattern. - Microsoft — .NET documentation — the CLR threading and GC models the next dive contrasts.
Related in this course
- Elixir — Processes & the actor model —
spawn,send,receive, in their own course. - Elixir — The BEAM's evolution — where this runtime came from.
- C3.01.2 — The CLR — the same four axes, the managed runtime.