Elixir Processes and State - Recommended
Taking the “happy path”
In Elixir and Erlang, a process is where we keep our state.
The story goes like this:
- a process starts with an initial state
- the process waits for messages
- send messages to the process to change it’s state
- process modifies it’s state and passes itself the new version of it’s state
- waits for messages (rinse and repeat)
That’s it.
There are a number of different interfaces to do this, but underneath it all, they are all doing the same thing.
The built-in ways look like this:
- spawn a process
- Use a GenServer behavior
- Use an Agent behavior (a subset of
GenServer
functionality)
Why is this a recommended pattern?
- it is efficient
- it is easy to write pure functions that test the behavior (given the same input, the same output results)
- it is common convention, others will understand what you are doing
Examples of Recommended Style
I setup a simple Github project that makes it easy to play with these approaches.
https://github.com/brainlid/meetup_process_state
Basic Spawn Example
When we call start
, it spawns a new process. That process sets up it’s initial
state and calls main_loop
. There is nothing special about this function name.
I just picked something. The process then calls receive
. This blocks the
process until it receives a message. This example only responds to a message of
{:add, number}
. When that message is received, it modifies the state and uses
“tail recursion” to call itself again with the new state.
Experiment by sending messages and seeing the log output in the console. The
Logger config was customized to output the pid
as part of the metadata. This
makes it easier to see which process is running which commands.
You might see something like this…
If you open Observer and inspect the process (in this example it would be 0.116.0) the “State” tab will show you “Information could not be retrieved, system messages may not be handled by this process.” This basic spawn works as intended, but it doesn’t support being inspected and handling system-level messages. That’s one reason it’s better to use a GenServer.
GenServer Example
The GenServer approach is preferred. It makes it easier to supervise the process and it has built-in support for handling system messages.
Here’s an example of how to interact with it in IEx.
You might see something like this…
Note the pid
responsible for running each line? All the same things are
happening here that are happening in the spawn example. The GenServer approach
makes it less explicit and adds support for handling other system messages.
Running Observer, I found the pid (0.117.0 in my case) in the “Processes” tab. Inspecting that process, the “State” tab looks like this.
The last line shows the “state” structure in the GenServer process.
Conclusion
Using the GenServer or Agent approach for storing runtime state in your Elixir application is the standard, recommended way. They have built-in support for being supervised. There is built-in support for handling system-level events. Observer understands them and can work with them natively. Most importantly perhaps, the functions can be “pure” and predictable. Given the same input, the same output is returned.
This is the “clean” version of managing state in a process.