jonnifyfsharp

Module C3.01

Two functional runtimes — the BEAM & the CLR

You already trust a runtime. F# asks you to trust a different one — and to know where the two part.

Elixir runs on the BEAM; F# runs on the .NET CLR. Both host functional code with immutable data and first-class functions, so most of the language transfers. The runtimes do not. The BEAM is a process-oriented virtual machine: cheap isolated processes, a heap and a garbage collector per process, preemptive reduction-based scheduling, and the OTP libraries with supervision and "let it crash". The CLR is a managed runtime for many languages: OS threads, a shared generational garbage collector, the thread pool, and async / Task for concurrency. F#'s actor is the MailboxProcessor — a mailbox and a recursive receive loop — but there is no built-in supervision tree. This module names the differences on four axes across three dives.

The same code, a different machine under it

A runtime fixes four things that shape how a program behaves under load and under failure: what the unit of concurrency is, how that unit is scheduled, how memory is reclaimed, and what happens when something faults. The BEAM and the CLR answer all four differently. Neither answer is the language's — both Elixir-on-the-BEAM and F#-on-the-CLR are functional, immutable, and pattern-matched. The difference is the host.

The first dive states the BEAM model an Elixir developer already lives inside, in the runtime's own terms. The second states the CLR model: managed execution, the JIT, a shared generational GC, threads and the thread pool, and the type system the CLR enforces. The third reads the two side by side — what carries over without change, what gains a static type, and the one thing the CLR does not hand you: OTP-style supervision.

The three dives · select one

C3.01.1 the BEAM you know C3.01.2 the CLR C3.01.3 what carries over the BEAM the CLR the seam
C3.01.1 · The BEAM you know

The runtime an Elixir developer already lives inside: lightweight isolated processes, a per-process heap and per-process GC, preemptive reduction-based scheduling, and OTP supervision with "let it crash".

You learn the BEAM model in its own terms, as the baseline

Open this dive · or pick another above

The language carries over almost whole. The runtime does not — and the runtime is what answers the four questions that decide how a system behaves when it is busy or breaking.

Four axes, two answers

Select an axis. The diagram marks which axis is in focus; the readout names the BEAM's answer and the CLR's answer for that axis, and the F# construct that meets it. Every value below is a documented fact about the two runtimes, not a benchmark — the point is the shape of each model, not a number.

BEAM vs CLR · select an axis

BEAM · Elixir process VM CLR · F# managed runtime concurrency unit scheduling garbage collection fault model process thread + pool
concurrency unit · BEAM = a lightweight process · CLR = an OS thread, usually via the thread pool · F#: MailboxProcessor over async
Without JavaScript: the four axes, both runtimes
axisBEAMCLR
concurrency unita lightweight isolated processan OS thread, usually via the thread pool
schedulingpreemptive, reduction-counted, per schedulerOS-scheduled threads; cooperative async over the pool
garbage collectiona heap and collector per processone shared generational collector
fault modelisolation + OTP supervision, "let it crash"exceptions; no built-in supervision tree

Four axes, two answers each. The dives that follow take them apart — first the BEAM's, then the CLR's, then the carry-over.

The stateful server, both ways

The clearest single contrast is the stateful server. On the BEAM you reach for a GenServer (or a hand-written receive loop): a process that holds state and answers messages. In F# the closest construct is the MailboxProcessor — an agent with a mailbox and a recursive async receive loop. The shape is the same; the difference is that the F# agent is one object on the shared runtime, not a supervised BEAM process.

Elixir — a GenServer holding a counter

# Elixir — GenServer
def handle_call(:total, _from, total), do: {:reply, total, total}
def handle_cast({:incr, n}, total), do: {:noreply, total + n}

GenServer.cast(pid, {:incr, 5})
GenServer.call(pid, :total)   # => 5

F# — a MailboxProcessor holding a counter (verified in FSI)

type Msg = Incr of int | Total of AsyncReplyChannel<int>

let counter = MailboxProcessor.Start(fun inbox ->
    let rec loop total = async {
        let! msg = inbox.Receive()
        match msg with
        | Incr n -> return! loop (total + n)
        | Total reply -> reply.Reply total; return! loop total }
    loop 0)

counter.Post(Incr 5)
counter.PostAndReply(fun ch -> Total ch)   // val it : int = 5

Elixir / BEAM

A GenServer is a supervised process with a mailbox; a crash is isolated and the supervisor restarts it.

F# / CLR

A MailboxProcessor is an agent with a mailbox on the shared runtime; the receive loop is the same shape, but supervision is not built in.

The three dives

Read them in order. Each stands on valid F# beside real Elixir: every // val it : echo below was produced by dotnet fsi, and the Elixir is the idiom an Elixir developer would write. Together they take a BEAM developer from trusting the runtime by habit to knowing which of those assumptions still hold on the CLR, and which one — supervision — has to be rebuilt.

live module hub built dive ready soon specified

References

Sources

Related in this course