Concurrency
The redefinition: shared-nothing, with isolated actors talking through typed channels, under supervision. The same
Channel[T]serves local and network.
The idea
Section titled “The idea”Mainstream concurrency is threads + shared memory + locks. You create threads, they share the heap, and mutexes/atomics protect what is shared. It works, but the bug category “I forgot to synchronize” is endemic, and the model does not cross the network (a mutex does not work between machines).
How Makoto redefines it
Section titled “How Makoto redefines it”The inheritance is the BEAM/Erlang one: the unit is the process, isolated, with its own heap, cheap. Processes
do not share memory (there is no @shared). They talk through a typed channel,
Channel[<-T, ->U], where the notation is the perspective of whoever holds the handle: <-T = “I send T”,
->T = “I receive T” (it eliminates the type inversion in the other side’s signature). Operators: chan <- data (send), data := -> chan (receive), and <-> (synchronous request-reply).
And the turn that unifies two worlds: the network channel is the same Channel[T] as the local one. There is no
“local channel” vs “remote RPC”; there is one type, two contexts. The physics of the network only expresses itself through two
requirements that already exist in the language: the payload has to become bytes (T + Serializable) and
establishing can fail (Result + timeout). That is native typed RPC: both sides
share T at compile time, without an IDL or a generated stub.
The reasoning
Section titled “The reasoning”Why shared-nothing? Because it is what makes the rest of the design possible. Without concurrent aliasing,
the borrow checker becomes unnecessary (ch. 02); memory can be colorless; a panic in one process
does not corrupt another’s heap. The isolation is not an imposed restriction; it is the foundation from which
memory safety without lifetimes and fault tolerance sprout.
Why does <-> require a timeout? Because two processes doing <-> to each other would give guaranteed
deadlock, and a silent deadlock is the worst possible failure: the system freezes waiting for a response
that never comes, with no warning. The mandatory deadline is the structural way out: it makes the timing visible and forces
the honest question “what if it does not respond?”. (The asynchronous <- does not need it, because it does not wait.)
Why does supervision emerge from the structure? The BEAM has link/monitor/trap_exit as raw
primitives. Makoto does not expose them: a process’s death is an ordinary error (the defers run,
the @mm is freed, the reason is produced), and observing the death without dying along with it is literally what the catch |e| of
a supervised spawn already does; a monitor is not a new construct, it is the name of that. Coupling fates
(links) is the tree: processes in the same spawn-block live and die together because the structure says
so. The three strategies (one_for_one, one_for_all, rest_for_one) fall out of the shape of the code, not
of a separately declared behaviour.
Why not the alternatives
Section titled “Why not the alternatives”- Threads + mutex + shared memory (Rust/C++/Java). It reintroduces the concurrent aliasing that would force the borrow checker, spreads the “I forgot the lock” category, and does not scale to distributed (a mutex does not cross the network). Shared-nothing eliminates the entire bug category.
- The pure Go model (goroutines +
sync). Go has channels, but it also allows shared memory and locks, and the aliasing door stays open. Makoto closes it: only channels, zero shared memory. - Raw
link()/monitor()(Erlang). Death cascade through loose links is non-local: “who killed whom?” stays implicit. The supervision tree makes the coupled fate visible in the structure. - A separate channel type for the network / an RPC framework. It would reopen the “local vs
remote” asymmetry. Reusing
Channel[T]withT + Serializablelets the physics (serialization, failure) express itself through the constraint and throughResult, without a second communication model.
The concrete pain
Section titled “The concrete pain”The data race that only appears in production under load, because someone forgot a lock. The system that
freezes waiting for a network response that never arrives, with no timeout, no log. The death cascade in
Erlang where a forgotten link() brings down half the system and nobody knows the path. Each one is a
failure that isolation + mandatory-timeout + supervision-tree make impossible or visible.
The mental model
Section titled “The mental model”Isolated processes talking through channels, supervised. Death is a value that rises to the supervisor; it restarts, brings down the group, or escalates, all emerging from the code’s tree.
The accepted friction
Section titled “The accepted friction”Shared-nothing costs copy/message-passing between processes: you pay in throughput what you gain
in correctness and in native-distribution. The supervision tree is more rigid than raw link(): you only
monitor the children that you spawned; cross-tree observation goes through a normal channel. Less flexible
than Erlang, more structured on purpose.
Next: 05 · Errors