Getting Started
Cizen is a library to build applications with automata and events.
Cizen Tutorial
See Primality Testing in Elixir using Cizen
Automaton Tutorial
For our getting started, we are going to create an automaton works like a stack.
The automaton will work like the following implementation with GenServer
:
defmodule Stack do
use GenServer
@impl true
def init(stack) do
{:ok, stack}
end
@impl true
def handle_call(:pop, _from, [item | tail]) do
{:reply, item, tail}
end
@impl true
def handle_cast({:push, item}, state) do
{:noreply, [item | state]}
end
end
GenServer.start_link(Stack, [:a], name: Stack)
item = GenServer.call(Stack, :pop)
IO.puts(item) # => :a
GenServer.cast(Stack, {:push, :b})
GenServer.cast(Stack, {:push, :c})
item = GenServer.call(Stack, :pop)
IO.puts(item) # => :c
item = GenServer.call(Stack, :pop)
IO.puts(item) # => :b
Define Events
At first, define two events, Push
and Pop
:
defmodule Push do
defstruct [:item]
end
defmodule Pop do
defstruct []
end
Push
is an event to push an item to a stack,
and Pop
is an event to pop an item from a stack.
To receive the popped item for a Pop
event, we define Pop.Item
,
which is an event to return the popped item.
Notes: This example uses struct but you can also use just maps or values.
Define an Automaton
Next, we define an automaton which handles the Push
and Pop
.
defmodule Stack do
use Cizen.Automaton
defstruct [:stack]
use Cizen.Effects # to use All, Subscribe, Receive, and Dispatch
alias Cizen.Pattern
require Pattern
@impl true
def spawn(%__MODULE__{stack: stack}) do
perform %All{effects: [
%Subscribe{pattern: Pattern.new(%Push{})},
%Subscribe{pattern: Pattern.new(%Pop{})}
]}
stack # next state
end
@impl true
def yield(stack) do
event = perform %Receive{}
case event do
%Push{item: item} ->
[item | stack] # next state
%Call{from: from, request: %Pop{}} ->
[item | tail] = stack
Saga.reply(item)
tail # next state
end
end
end
There are two callbacks spawn/1
and yield/1
,
and they'll called with the following lifecycle:
- First,
Cizen.Automaton.spawn/1
is called with a struct on start. - Then,
Cizen.Automaton.yield/1
is repeatedly called with a state.
Cizen.Automaton.perform/1
performs the given effect synchronously and returns the result of the effect.
See Effect for details.
The following code in spawn/1
subscribes two event types Push
and Pop
:
perform %All{effects: [
%Subscribe{pattern: Pattern.new(%Push{})},
%Subscribe{pattern: Pattern.new(%Pop{})}
]}
Based on the subscriptions, events are stored in a event queue, which all automata have,
and Receive
effect dequeues the first event from the queue.
%Receive{}
is the same as%Receive{pattern: Pattern.new(_)}
, andPattern.new(_)
returns an event pattern that matches to all events. Actually,Receive
effect dequeues the first event which matches with the given filter from the queue.
Interact with Automata
Now, we can interact with the automaton and events like this:
defmodule Main do
def main do
use Cizen.Effectful
use Cizen.Effects
handle fn ->
# start stack
stack = perform %Start{
saga: %Stack{stack: [:a]}
}
# TODO use Call effect (not implemented yet).
item = Saga.call(stack, %Pop{})
IO.puts(item) # => a
perform %Dispatch{
body: %Push{item: :b}
}
perform %Dispatch{
body: %Push{item: :c}
}
item = Saga.call(stack, %Pop{})
IO.puts(item) # => c
item = Saga.call(stack, %Pop{})
IO.puts(item) # => b
end
end
end
Normally, Cizen.Automaton.perform/1
only works in automaton callbacks,
so we use Cizen.Effectful.handle/1
to interact with the automaton from outside of the automata world.
Multiple Stacks
Our code works well only with just one stack.
It's broken if we have multiple stacks because all stacks receive Push
or Pop
event when we dispatch.
To avoid it, let's introduce more complex pattern.
First, add :stack_id
and definitions of event body filters in the events:
defmodule Push do
defstruct [:stack_id, :item]
end
defmodule Pop do
defstruct []
end
Next, use the filters on subscribe in Stack.spawn/2
:
def spawn(%__MODULE__{stack: stack}) do
perform %All{effects: [
%Subscribe{pattern: Pattern.new(%Push{stack_id: ^id})},
%Subscribe{pattern: Pattern.new(%Pop{})}
]}
stack # next state
end
Finally, we can handle multiple stacks like this:
defmodule Main do
def main do
use Cizen.Effectful
use Cizen.Effects
handle fn ->
# start stack A
stack_a = perform %Start{saga: %Stack{stack: []}}
# start stack B
stack_b = perform %Start{saga: %Stack{stack: []}}
# push to the stack A
perform %Dispatch{
body: %Push{stack_id: stack_a, item: :a}
}
# push to the stack B
perform %Dispatch{
body: %Push{stack_id: stack_b, item: :b}
}
# push to the stack B
perform %Dispatch{
body: %Push{stack_id: stack_b, item: :c}
}
# pop from the stack A
item = Saga.call(stack_a, %Pop{})
IO.puts(item) # => a
# pop from the stack B
item = Saga.call(stack_b, %Pop{})
IO.puts(item) # => c
# pop from the stack B
item = Saga.call(stack_b, %Pop{})
IO.puts(item) # => b
end
end
end