Introduction to Protean
Mix.install([
{:protean, git: "https://github.com/zachallaun/protean.git"}
])
A very simple machine
Let’s start with a very simple machine: a counter.
defmodule Counter do
use Protean
defmachine [
context: %{
count: 0
},
initial: :active,
states: [
active: [
on: [
INC: [
actions: [
Protean.Action.assign(fn _state, %{count: count}, _event ->
%{count: count + 1}
end)
]
]
]
]
]
]
end
{:module, Counter, <<70, 79, 82, 49, 0, 0, 15, ...>>, :ok}
This machine has a single :active
state that it will automatically enter when the machine starts (set by :initial
). It also has some extended state, it’s :context
. While it’s :active
, it will listen for "INC"
events and perform a pretty simple action: updating its context by incrementing the current count.
Let’s start the machine as a process and poke around a bit. When we defined our counter with use Protean
, some functions were defined to start up our machine separately (or under a supervisor).
{:ok, counter} = Counter.start_link()
{:ok, #PID<0.255.0>}
Under the hood, this started up a GenServer
to encapsulate our running machine state. We can get the current state:
state = Protean.current(counter)
%Protean.State{context: %{count: 0}, event: nil, private: %{actions: []}, value: [["active", "#"]]}
Protean.current/1
returns a %Protean.State{}
that represents the current state of our machine. Notice that we can access the current context.
state.context
%{count: 0}
And though we know that our counter only has a single state that it can be in, we can check anyways.
Protean.matches?(state, :active)
true
Let’s send an event to our machine and see it increment that counter.
state = Protean.send(counter, "INC")
state.context
%{count: 1}
Protean.send/2
sends an event syncronously and returns the updated state. We can also send events asyncronously:
:ok = Protean.send_async(counter, "INC")
:ok = Protean.send_async(counter, "INC")
:ok = Protean.send_async(counter, "INC")
Protean.current(counter).context
%{count: 4}
One last thing to note is that Protean machines don’t mind receiving events they don’t care about. Our counter doesn’t know how to decrement, but we can send that event anyway and watch the counter do nothing.
Protean.send(counter, "DEC").context
%{count: 4}
Changing state
Let’s define a slightly more interesting machine, one that models a request that can either succeed or fail.
defmodule Request do
use Protean
defmachine [
initial: :pending,
states: [
pending: [
on: [
SUCCEEDED: :success,
FAILED: :fail
]
],
success: [],
fail: []
]
]
end
{:module, Request, <<70, 79, 82, 49, 0, 0, 14, ...>>, :ok}
{:ok, request} = Request.start_link()
Protean.matches?(request, :pending)
true
request
|> Protean.send("SUCCEEDED")
|> Protean.matches?(:success)
true