03 March 2017
I love Elixir and it’s functional style.
That’s probably not much of a secret to you at this point, but one of the questions I often get is about transitioning to a functional style from an object oriented one.
Luckily, I have some really good experience with this, coming from Ruby, where everything is an object.
More to the point, the question really comes down to managing some sort of state in a software system beyond simple data structures being passed around, a replacement for the objects they previously used.
Agent is built on top of
GenServer so let’s start exploring there.
GenServer is exactly what it kinda sounds like, a general server.
In this case, it’s not a web server or an application server, but more like a “state” server.
GenServer process can hold and manage state, but it can also perform calculations, wait for completion, process incoming messages, and do things completely on it’s own.
It runs as a separate process (on the BEAM virtual machine, not the OS), so it is often used to handle non-blocking processing of things.
In fact, another abstraction is used specifically for non-blocking processing, in the
Task module in the Elixir standard library.
In my experience, it’s very common to back a GenServer with a module written for it with a nicer API.
That’s because GenServers are super simple, responding to only a few calls, namely
A “call” is used when you want to send a message to the GenServer and then wait for the response.
In contrast, a “cast” is made when you want to send a message, but don’t need a response and the processing can be done asynchronously.
A really basic example module looks something like this:
Without writing the nicer API, calls to your imaginary queue GenServer look ugly:
Commonly, you’ll define extra functions in your module that handle the ugly parts for you:
That’s much nicer, because now your calls look like:
It’s less typing and much more intuitive.
Agent module lets you easily create a process to hold state without implementing a bunch of stuff for a GenServer.
It will usually require writing less code, but it is still common to back an Agent with a module.
Building off our
We wrote less code and we can still use the same syntax for accessing our queue.
Since we have two tools here at our disposal, what tasks are they better suited to? The examples are available on Github, so you can follow along.
I ran some benchmarks on my poor computer to see about how fast the same, simple functions run in both a GenServer and an Agent:
## CreateBench benchmark iterations average time Agent 100000 11.54 µs/op GenServer 100000 12.03 µs/op ## PlainBench benchmark iterations average time add 1000000000 0.01 µs/op get 1000000000 0.01 µs/op ## QueueAgentBench benchmark iterations average time set 500000 6.45 µs/op add 200000 8.40 µs/op get 100000 12.84 µs/op ## QueueAsyncBench benchmark iterations average time add 1000000 2.01 µs/op get sync 200000 7.66 µs/op ## QueueBench benchmark iterations average time set 500000 6.25 µs/op add 500000 7.57 µs/op get 100000 12.57 µs/op
The first set is just to create a process using
Agent.start_link and here you see that GenServer is slightly slower.
When it comes to the messages you’ll be passing to the Queue process, look at
QueueBench (GenServer version) and
QueueAgentBench (Agent version).
You’ll see that time difference between the two functions are very slight, but the GenServer version is slightly faster.
This makes sense considering that
Agent is built on top of
Compare these against the
PlainBench results, where I had similar basic operations, but not run in a separate process and waiting for a response.
With these results, it’s pretty clear that the parts that take the longest are related to message passing and communication between the processes.
I added the
QueueAsyncBench just for fun, showing you that an async call to add a item to the queue is blazing fast.
After all, it’s only waiting to receive the
:ok message, confirming that the Queue process has been sent the message.
It’s probably good to note here that the
:ok message doesn’t ensure that the Queue process received the message, just that it was sent to the process.
Also, make sure to note that these benchmarks are measured in microseconds, so the one microsecond difference between a
GenServer and an
Agent is imperceptible and won’t matter except under very heavy load.
Since both state management tools are fast, it’s more up to our preference. I tend to optimize for developer ergonomics and code maintainability.
For me, Agents are easier to write for simple things, like maintaining a simple queue or collecting results from multiple, concurrent tasks. In these cases, the Agent is just accepting some data and adding it to a list or iterating over a set of data to return a simple result.
If you’re new to Elixir or functional programming in general, you might find the anonymous-function-as-argument part of calls to Agent actually harder to write. Keep reading and writing them and it’ll come naturally pretty quickly.
Part of this comes from simply having to separate functions to have a nice interface and handle callbacks. I have more opportunities for code documentation and the functions are likely to be more well thought out and single-purposed as a result.
Additionally, if the GenServer you’re writing is part of a package you’re building, GenServer makes it harder to crack open, hopefully preventing those bugs that come up from someone doing something their not supposed to. Personally, I’m very much against this philosophy, but not everyone agrees with me. I love that Ruby and its community is very trusting of developers and things are designed so you can do whatever you want with whatever you want. I think it leads to innovative use cases.
You’re the boss and it’s also good to note that there isn’t a wrong choice here.
If you choose to use Agent now and then, halfway through development, realize that it’s getting clunky and you’d prefer it to be a GenServer, extracting the calls to
Agent is pretty straightforward.