GenServer
What? Why?
GenServer’s are one of the first interesting departures from other programming approaches that people new to Elixir will encounter.
From the HexDocs we get the description
> it (GenServer) can be used to keep state, execute code asynchronously and so on
This is a little vague and I hope to provide a reason why you would want to do this.
Lets start out with a simple example:
- A counter with an inc and dec method.
- A way of getting the current value.
- A simple interface to set and get these values.
- A print function to log the state to the console.
In an object orientated approach to this problem we would let an object manage it’s own state, so we would have a Counter
object with an internal value to track the current count and methods to access this value.
As we are not working in an OOP paradigm we have something a bit different.
The state is not tied to an object and it is not restricted to the objects internal structure, we can store any data structure in a GenServer and we can provides a mixture of both async and synchronous data access methods and provide a simple interface.
defmodule GenCounter do
use GenServer
# Public interfaces
# Calling GenServer cast / call will pass a message to our Counter which it will respond to
# in the methods below.
def inc(pid, n \\ 1) do
1..n
|> Enum.map(fn _x -> GenServer.cast(pid, :inc) end)
end
def dec(pid, n \\ 1) do
1..n
|> Enum.map(fn _x -> GenServer.cast(pid, :dec) end)
end
def val(pid) do
GenServer.call(pid, :val)
end
def print(pid) do
val(pid) |> IO.inspect()
end
# ---------------------
# Initialise
def start_link(initial) do
GenServer.start_link(__MODULE__, initial)
end
def init(init_arg) do
{:ok, init_arg}
end
# ----------------------
# Handle Events - We receive a massge and either expecting a result (call) or not (cast)
def handle_cast(:inc, state) do
{:noreply, state + 1}
end
# Async function - ther state gets changed, but we do not wait for that to happen
def handle_cast(:dec, state) do
{:noreply, state - 1}
end
# Sunc function - we want a result and need to wait for that to be calculated
def handle_call(:val, _from, state) do
{:reply, state, state}
end
end
# the initial value for the count
{:ok, counter_pid} = GenCounter.start_link(0)
GenCounter.print(counter_pid)
GenCounter.inc(counter_pid, 10)
GenCounter.print(counter_pid)
GenCounter.dec(counter_pid, 4)
GenCounter.print(counter_pid)
GenServers as Objects?
A lot of ink and sanity is spilt when discussing objects in a programming context. Object-orientation is considered by some the pinnacle of programming abstraction. But what is it?
> Object-oriented programming (OOP) is a programming paradigm based on the concept of “objects”, which can contain data and code: data in the form of fields (often known as attributes or properties), and code, in the form of procedures (often known as methods). > A feature of objects is that an object’s own procedures can access and often modify the data fields of itself (objects have a notion of this or self). In OOP, computer programs are designed by making them out of objects that interact with one another.
Ultimately the ultimate question is how to structure code so that we can think about it as it grows. We do this be making sure we can reson about it in ‘chunks’, that changing a unrelated part of the code does not break anything else.
If we keep in mind the goal of keeping a box around parts of code and making sure we don’t have to think about the how
, just the what
then I think there is an argument that GenServers can be thought of as Objects
Not only can we provide a way of modifying state we can protect that state and implement logic to control how it is updated.
defmodule User do
defstruct name: nil, age: nil, can_access: false
use GenServer
def start_link(name, age) do
GenServer.start_link(__MODULE__, %User{name: name, age: age, can_access: can_access?(age)})
end
def init(init_arg) do
{:ok, init_arg}
end
def inc_age(pid) do
GenServer.cast(pid, :inc_age)
end
def get_user(pid) do
GenServer.call(pid, :get_user)
end
def handle_call(:get_user, _from, state) do
{:reply, state, state}
end
def handle_cast(:inc_age, %User{name: name, age: age, can_access: _can_access}) do
age = age + 1
{:noreply, %User{name: name, age: age, can_access: can_access?(age)}}
end
defp can_access?(age) when age >= 18 and age < 50 do
true
end
defp can_access?(_age) do
false
end
end
{:ok, user} = User.start_link("Bob", 49)
User.get_user(user) |> IO.inspect()
User.inc_age(user)
User.inc_age(user)
User.get_user(user)
Genserver as an object?
Lets compare a GenServer to object orientated model proposed by Alan Kay - the forgotten history of OOP
> “OOP to me means only messaging, local retention and protection and hiding of state-process, and extreme late-binding of all things.” > ~ Alan Kay
So we have:
- Messaging - we can add both synchronous and async interfaces to talk to our ‘object’
- Local retention - the state is stored and can be modified or retrived
- Protection - we can restirct the interface to only state we want to be made accessible
- Hiding of state-process - we do not see how the state is managed, only the interface to it.
The last one - extreme late-binding of all things, I don’t fully understand. If you want to get into it further Wiki on late binding (of course) and a blog on late binding in elixir
> The phrase “extreme late-binding of all things” is a way of saying something like, “I want the computer to sort out what code to actually run as the need arises.” In some language that might mean two different kind of objects could have print() methods and the computer decides which one to invoke based on which object the message is sent to.
# As a quick test lets create a couple of users and ensure that there is independance
{:ok, usera} = User.start_link("Bob", 49)
{:ok, userb} = User.start_link("Zork", 20)
User.get_user(usera) |> IO.inspect()
User.get_user(userb) |> IO.inspect()
User.inc_age(usera)
User.inc_age(usera)
User.inc_age(userb)
User.inc_age(userb)
IO.puts("-------------")
User.get_user(usera) |> IO.inspect()
User.get_user(userb)