GenServers as Concurrent Objects
August 21, 2015
elixir
This is a post for fellow object-oriented developers trying to get their heads around how Elixir/Erlang use processes as a basic abstraction, rather than classes and objects.
A Sample Object
In an object oriented language, such as Ruby, we might implement a BankAccount
class like this:
class BankAccount
attr_reader :balance
def initialize(starting_balance)
@balance = starting_balance
end
def deposit(amount)
@balance += amount
end
def withdraw(amount)
@balance -= amount
end
end
This class could then be used like this:
account = BankAccount.new(0.0)
account.deposit(50.0)
account.withdraw(25.0)
account.balance # => 25.0
A GenServer Equivalent
In Elixir, we can implement a GenServer
that behaves very similarly:
# Create a new GenServer process with an initial state (balance) of 0, using the
# BankAccount module to process messages to the process. Returns a process ID.
{:ok, account} = GenServer.start(BankAccount, [0.0])
# Send messages to the process to deposit or withdraw amounts.
# cast/2 runs asynchronously and doesn't wait for a response.
GenServer.cast(account, {:deposit, 50.0})
GenServer.cast(account, {:withdraw, 25.0})
# Query the process for the balance, and waits for a response.
GenServer.call(account, :balance) # => 25.0
GenServer
here will fire callbacks on the BankAccount
module in response to the messages sent to the process.
defmodule BankAccount do
use GenServer
# Casts don't reply to the caller, but simply modify the state
def handle_cast({:deposit, amount}, balance) do
{:noreply, balance + amount}
end
def handle_cast({:withdraw, amount}, balance) do
{:noreply, balance - amount}
end
# Calls return a value, as well as the modified state
def handle_call(:balance, balance) do
{:reply, balance, balance}
end
end
We can hide all the GenServer implementation behind a public API on the BankAccount
module. (Not shown) Our code will then look very similar to Ruby’s:
{:ok, account} = BankAccount.start(0.0)
BankAccount.deposit(account, 50.0)
BankAccount.withdraw(account, 25.0)
BankAccount.balance(account) # => 25.0
GenServer “Singletons”
It is possible to refer to GenServer
processes by a name rather than by a process ID. In this case, a GenServer
behaves more like a singleton rather than an object instance, because there is only one process running.
This naming is accomplished by passing the :name
option to GenServer.start
:
GenServer.start(BankAccount, [0.0], name: BankAccount)
You can then send messages to the BankAccount
process using its name:
GenServer.cast(BankAccount, {:deposit, 50.0})
If we reimplemented our BankAccount
module as a named GenServer, (not shown) we could use it like this:
# In our application initialization
BankAccount.start([0.0])
# Then, anywhere in our code:
BankAccount.deposit(50.0)
BankAccount.withdraw(25.0)
BankAccount.balance # => 25.0
This BankAccount
process can then be called from other nodes (think, microservices) in your cluster like so:
GenServer.call({BankAccount, :accounts@localhost}, :balance) # => 25.0
No JSON APIs required!
Contrasts
We see then that GenServer
modules can behave very much like objects because they are a construct that both holds state and can perform operations on that state. However, they are different from objects in Ruby in a few important ways:
A
GenServer
can hold only one value as its state. In Ruby, you can have as many instance variables as you like. However, this isn’t a big limitation, since the state you store in aGenServer
can be as complex as you want.GenServer
operations are automatically spread across your CPU cores. If the operation doesn’t require a response, then the caller won’t be blocked. However, it’s important to keep in mind that aGenServer
process can only do one thing at a time.Named
GenServer
processes can be called from other computers in the cluster, as shown above.GenServer
processes shouldn’t really be used as often as objects or in the same ways. They are designed for concurrency, not managing data structures like objects are. I compare them to objects just to relate them to something familiar.
Conclusion
I know that it was a real “lightbulb” moment for me when I realized that GenServers are Elixir/Erlang’s answer to objects. Hopefully you’ve found this article helpful! If something isn’t clear, ask me a question on Twitter and I’ll see if I can clarify the post.