On Processes
Erlang/Elixir is the kind of language you love more the more you use it.
A little over a month ago, in that Erlang article, I wrote this:
In the world of Erlang, everything is a process. Processes are like atoms — they compose into molecules, cells, tissues, organs, and ultimately a whole organism (see my article: boundary). So what exactly is a process?
In fact it is nearly identical to the OS concept of a process: a PCB (Process Control Block), plus the heap, stack, mailbox, and other memory space that surrounds it. The difference is that an Erlang process has a tiny memory footprint — a freshly initialized process occupies only a few hundred words (on a 32-bit CPU, 1 word = 4 bytes; on a 64-bit CPU, 1 word = 8 bytes). So the number of processes the Erlang VM can create depends in theory only on how much memory is available; we can easily create millions, even tens of millions, of processes (32-bit CPUs use 28 bits for the pid, 64-bit CPUs use 60 bits — you can work out the upper bound yourself). Erlang’s process derives from the actor model, one of several common concurrency models (for the others, see my article:). Processes handle all manner of tasks by sending and receiving messages.
A process is in fact much like an object in OOP: processes are isolated from one another — one process cannot access another’s internal state, which is the concept of encapsulation — and inheritance and polymorphism can be perfectly realized through parent/child processes. In fact, Erlang can be considered a legitimate heir of Smalltalk, the progenitor of OOP (the other heir, ObjC, is gradually fading from view).
Today let’s focus on processes, and on some of my immature understanding and usage of them.
On the surface, a process seems similar to an object in any OOP language. It has state, it has behavior, it can accept input from the outside and return a result for that input, or change its own state. Below is a very simple Python version of a kv store, for demo purposes only:
class KVStore(object):
def __init__(self):
self.store = {}
def put(self, k, v):
self.store[k] = v
@memoize
def get(self, k):
return self.store[k] if k in self.store else None
The elixir/OTP version:
defmodule KVStore do
use GenServer
# public interface
def start_link, do: GenServer.start_link(__MODULE__, nil, name: __MODULE__)
def put(k, v), do: GenServer.cast(__MODULE__, {:put, k, v})
def get(k), do: GenServer.call(__MODULE__, {:get, k})
# callback
def init(_), do: {:ok, %{}}
def handle_call({:get, k}, _from, state), do: {:reply, get(state, k), state}
def handle_cast({:put, k, v}, state), do: {:noreply, put(state, k, v)}
# private functions
defm get(state, k), do: Map.get(state, k)
def put(state, k, v), do: Map.put(state, k, v)
end
Note: never mind what @memoize and defm do for now — all will be revealed shortly.
The calling code for the two is much the same: the former produces an object, the latter produces a named process:
In [23]: store = KVStore()
In [24]: store.put("tyr", "chen")
In [25]: store.get("tyr")
Out[25]: 'chen'
iex(7)> KVStore.start_link()
{:ok, #PID<0.176.0>}
iex(8)> KVStore.put("tyr", "chen")
:ok
iex(9)> KVStore.get("tyr")
"chen"
Achieving the same functionality, in this particular case, takes far more Elixir code than Python. Yet the two run in utterly different states. The Elixir version of the KV Store (hereafter EKV) lives in its own world, with an independent personality; the Python version (hereafter PKV) depends heavily on its host — whatever the running context happens to be, it clings to it, be it a WSGI process or a CLI process.
(from: