jonnifyfsharp

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.

C3.01.1 · part 1

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 send targets. 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; receive pattern-matches the next matching message.
C3.01.1 · part 2

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
F# meets this exact shape with the 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

sender {:incr, 5} one process · own heap mailbox receive private heap count = 0 send(from, count)
send {:incr, 5} · the process matches the clause, computes count = 0 + 5 and loops as count = 5. No reply.

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.

C3.01.1 · part 3

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

Related in this course