I’ll be showing Elixir code in future blog posts. Elixir looks a fair amount like Ruby, and a lot of it is pretty straightforward to someone who knows a few languages. However, the way it handles processes (its version of actors) is fairly special and has its own notation. Since I’ll be using processes as a key building block, I’ll explain them here.
First, some essential background…
Immutability
Elixir has strictly non-mutable state: you don’t get to modify any values. So suppose you have a Map
(key-value) data structure like this:
>>> my_map = %{key1: "a string", key2: 5}
%{key1: "a string", key2: 5}
You can “update” my_map
in various ways. These two are (almost) equivalent:
Notice that a key can have any type of value. Elixir is a dynamically-typed language.
>>> other_map = Map.put(my_map, :key1, [1, 2, 3])
%{key1: [1, 2, 3], key2: 5}
>>> other_map = %{my_map | key1: [1, 2, 3]}
%{key1: [1, 2, 3], key2: 5}
In either case, the original data (still pointed at by my_map
) is unchanged.
As far as the program can tell. The underlying virtual machine would be allowed to share common structure between my_map
and other_map
, so long as no user code can tell it’s happening. Some virtual machines, like Clojure’s, use such sharing heavily. I don’t think the Erlang virtual machine is one of them. You can think of all data-changing operations as making a full copy and then changing the copy.
Once you get used to it, being unable to change data is surprisingly un-annoying. People have been working in such languages long enough that there are a lot of tricks of the trade that make you not miss changing state (much).
Processes
Nevertheless, like all immutable languages, Elixir has an escape hatch that allows state change. As is common, that’s tied up with concurrency mechanisms (since it’s concurrency where mutable state really comes and bites you). Elixir’s single way to change state is the process, an independent, asynchronous of holder-of-state. A process is reminiscent of the classic object-oriented class (but without inheritance). That is, it has some state it holds plus some attached functions.
Since these functions are ordinary Elixir code, they can’t change state, only create new data from old. But processes are about changing state. How to square the two?
Here’s the trick. When the process is created, it’s “seeded” with its original state. Later, when one of the process’s functions is called, the current state is passed as one of the function’s arguments. So, for example, it might get %{key1: "a string", key2: 5}
. It can then use Map.put
to create a new version of the state. When the function finishes, it returns both its return value and the new version of the state. The process stops holding the old version (which is then garbage collected) and starts holding the new version for the next function call.
All Elixir code runs within some process. A configuration file called mix.exs
names one or more that are started when the program is launched; then those few start all the rest.
Pattern matching
The code that implements processes uses Elixir’s pattern matching, so I need to explain that.
Elixir has what looks like plain assignment:
a = 5
However, what’s happening is a bit more complicated. a
, here, is the simplest form of a pattern: an isolated name (or variable). The code is asking “can the value 5
be matched to the pattern a
?” Since the answer is yes, a
becomes bound to the value 5.
So far, so dull. However, patterns can be more elaborate. Here’s a different use of a pattern:
{:ok, value} = some_function("some arguments")
Here, it’s expected that some_function
will return a two-element tuple whose first element is the literal atom :ok
Atoms are like symbols in Ruby. If they didn’t exist, we’d use strings. But every string "foo"
is different from every other one, whereas every atom :foo
is the same value. So atoms are faster to compare and use less memory. and whose second element is some value that will be bound to value
. There are several possibilities:
some_function
might not return a tuple at all. Maybe it returns5
. In that case, the process running the code will blow up.- It might return a three element tuple. The process blows up.
- It might return a two-element tuple that starts with
:error
. The process blows up. - It returns
{:ok, 5}
.value
becomes bound to 5.
A process “blowing up” is not unusual. Elixir’s design philosophy is that you shouldn’t scramble to handle and correct errors. When in doubt, a process should “fail fast” and let a supervisor decide how to handle it. That may be to restart the failed process, but it could equally well be to shut down and restart all the supervisor’s supervised processes (useful if that collection of processes forms an integrated subsystem). I’m going to defer explaining supervisors until I start using them. Suppose I don’t want the third case above to blow up. Then I’d write something like this:
case some_function("some arguments") do
{:ok, process_id} ->
# do something with process_id
{:error, reason} ->
# do something with reason
end
The patterns will be tried in turn.
It is very common for Elixir functions to return one of {:ok, value}
, {:error, reason}
, or just :error
(if there’s no useful reason to return.) This is the idiom used instead of Optional
or Maybe
types in other languages. Note that a function can return different “shapes” of data. some_function
could return a two-element :ok
tuple most of the time, but occasionally the bare atom :error
.
One final bit: pattern matching is frequently used in function argument lists to “pick apart” structured data. Consider this:
def f({:ok, value}, some_other_name) do ...
If f
is called with {:ok, 5}
and "string"
, its body will run with value
bound to 5 and some_other_name
bound to "string"
.
There’s also a function equivalent of case
. You might see code like this:
1 def f({:ok, success_value}) do
2 ...
3 def f(:error) do
4 ...
These are compiled into a single function, f
, with an embedded case
. So f({:ok, 5})
will run the code on line 2 with success_value
bound to 5. f(:error)
will run the code on line 4.
Messages
Strictly speaking, code outside a process doesn’t call a process’s functions. Instead, it sends a message to the process. Messages are arbitrary Elixir data structures. “Arbitrary” includes freestanding functions. You can actually send a function over a network to another machine and run it there, which is pretty cool.
Messages queue up on the process and are handled one-by-one (generally first-come-first-served). You can say that the process calls one of its functions on behalf of the message, handing the function both the message and the process’s current state.
All this mechanism is built on a low-level foundation that I’m not going to explain. Rather, like most Elixir programmers, I’ll use the GenServer
behaviour. Elixir behaviours are roughly like interfaces or abstract superclasses: they require certain functions to be defined, but can provide defaults for some or all of them.
GenServer
provides various ways to send messages to other processes. The two most relevant are Genserver.
call and GenServer.
cast:
{:ok, return_value} = GenServer.call(process_id, "some message")
GenServer.cast(process_id, "some message")
call
is used when the sender needs a return value. The sender process blocks until the called process returns something. cast
is when the sender needs no return value. The sender doesn’t wait. It keeps running, potentially in parallel with the receiver.
You can’t call a process without knowing its process ID. That’s frequently an identifier returned by a process-creation function. For example, here’s a typical way to create a process:
{:ok, focus} =
GenServer.start_link(ParagraphFocus, ...)
If the process is created successfully, focus
will be bound to something that prints like this: #PID<0.147.0>
. Later code can then use that process id (or pid) in GenServer.call
, but no code that lacks the pid can contact the process.
Processes can also be created with global names. The following creates a process that has a paragraph of text as its state, and names it :current_paragraph
.
{:ok, paragraph} =
GenServer.start_link(Paragraph, paragraph_state, name: :current_paragraph)
Any code anywhere can then use GenServer.call(:current_paragraph, ...)
to communicate with the process.
Putting it all together
Here’s part of a typical process definition, a bit abbreviated. It’s my first prototype’s Paragraph process that represents a chunk of text currently being edited.
defmodule Paragraph do
use GenServer
def init(initial_state) do
{:ok, initial_state}
end
When some other process calls GenServer.start_link(Paragraph, "state")
, control is eventually delivered to Paragraph.init
. In this case, it just returns a typical success tuple with the initial state for the process to store.
Actually, I didn’t need to define this because it just reimplements the default init
. But I thought it worth showing.
The code that handles messages sent with call
looks like this:
def handle_call(:text, _from, state),
do: {:reply, state.text, state}
def handle_call(:cursor, _from, state),
do: {:reply, state.cursor, state}
These two functions (or two cases of one function – take your pick) handle the messages :text
and :cursor
, respectively. In these cases, the patterns are literal matches, with no names to bind.
The next argument is the process id of the sending process, which gets bound to the name _from
. The underscore tells the compiler not to warn me that I created a name that I never used: I really truly did intend that.
state
is bound to the process’s current state, which is a map with two keys, :text
and :cursor
.
The body of both functions is just a return tuple:
def handle_call(:text, _from, state),
do: {:reply, state.text, state}
:reply
indicates the function succeeded (there are other possibilities), state.text
is the value value to be returned to the process that used GenServer.call
, and the unchanged state is the third tuple element. After the return, various magic happens, resulting in the original GenServer.call
returning an {:ok, "string"}
tuple.
That’s how call
is handled. cast
is only slightly different. I wanted to direct the Paragraph
process to insert text at the cursor (or insertion point). I used a cast
instead of a call
because there was no reason for the sending process to wait around while Paragraph
did its thing.
The argument list for handle_cast
is like that of handle_call
, except the sending process isn’t included:
def handle_cast({:insert, string}, state) do...
A {:command, arg1, arg2, ...}
tuple is pretty ubiquitous for inter-process communication. Because this particular message has data, not just an instruction, I use pattern matching to extract it.
The implementation is not exactly efficient: an existing string is split at the cursor, creating two new strings named prefix
and suffix
. Then those are joined together with the <>
(append) operator, creating yet another string:
{prefix, suffix} = String.split_at(state.text, state.cursor)
new_state = %{state |
text: prefix <> string <> suffix,
cursor: state.cursor + String.length(string)}
This function returns a tuple to the GenServer
mechanism:
{:noreply, new_state}
end
:noreply
indicates nothing went wrong. new_state
will direct the process to store the now-lengthened string and new cursor.
I think that’s enough Elixir background to make later posts reasonably easy to understand.