Integrating Cizen with Phoenix
Do you know Cizen? It’s an Elixir library that helps you to develop applications that consist of automata who communicate by passing events to each other. The author of Cizen has published an article titled “Asynchronous Request and Connection Pooling in Elixir with Cizen”.
But, wait. Is Cizen a library for specific purposes? Absolutely NOT! Just like a meaning of Cizen (“自然” in Japanese, “Nature” in English), it can be utilized every scene in application development. In this article, I’d like to introduce an integration of Cizen and Phoenix Framework though building a tiny web application “Hello World” (github repo). 🚀
I assume you have already installed requirements below.
- Elixir 1.7.3 (Erlang/OTP 21)
- Phoenix 1.4.0
If not, please install Elixir and Phoenix before started. To upgrade Phoenix from 1.3.x to 1.4.0, don’t forget to uninstall project generator before install.
Hello World 🌐
Before diving into the Cizen world, I’d like to explain what I want to achieve with this example. “Hello World” is a simple web application that accepts HTTP GET request and returns an HTML response including a text “Hello World”. Uh…where is Cizen? Don’t worry, Cizen has a responsibility of a core application; this means that a content of the HTTP response is provided from a Cizen’s automaton.
Let’s create “Hello World” application with Phoenix’s project generator. In this application, we don’t use database backends (ecto) and JavaScript assets (webpack since 1.4.0, formerly brunch). Therefore we drop them on scaffolding.
mix phx.new hello_world --no-webpack --no-ecto
Run dev server and open in your browser to check it works fine.
cd hello_world
mix phx.server
Congrats! 🎉
To add Cizen to this project, open mix.exs
file with your favorite editor and add {:cizen, "~> 0.12.5"}
to dependencies section.
defp deps do
[
{:cizen, "~> 0.12.5"},
{:phoenix, "~> 1.4.0"},
{:phoenix_pubsub, "~> 1.1"},
{:phoenix_html, "~> 2.11"},
{:phoenix_live_reload, "~> 1.2", only: :dev},
{:gettext, "~> 0.11"},
{:jason, "~> 1.0"},
{:plug_cowboy, "~> 2.0"}
]
end
Save the file, run mix deps.get
to install new dependencies, and restart Phoenix dev server. To simplify the HTML template, remove unnecessary part of layout/app.html.eex
and page/index.html.eex
.
app.html.eex
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>HelloWorld</title>
</head>
<body>
<main role="main" class="container">
<%= render @view_module, @view_template, assigns %>
</main>
<script type="text/javascript" src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script>
</body>
</html>
index.html.eex
<h1>Hello World</h1>
Let’s dive into Cizen part. An automaton is a primitive entity of the world. In a Cizen way of design, we think what kind of automata is needed and how the automata behave through receiving and dispatching events.
Here is a specification for our automaton.
- The name is
GreetingAutomaton
- Receives
Greeting
event - Dispatches
Reply
event with a text message
That’s all! It’s time to write code. 😁 First, we define the Greeting
event.
defmodule HelloWorld.Greeting do
defstruct []
end
Cizen’s events are not special, it’s just an Elixir struct. Because of the Greeting
event doesn’t have a payload, defstruct
declaration leaves blank.
In contrast with the Greeting
event, the definition of Reply
event is a bit different. To represent communications such as a pair of request and response, Cizen provides a feature named “Requestive Events”. Use Cizen.Reuqest
behavior and define the Reply
response event using defresponse
macro. Notice that message
field is also defined in it.
defmodule HelloWorld.Greeting do
defstruct [] use Cizen.Request
defresponse Reply, :greeting_id do
defstruct [:greeting_id, :message]
end
end
message
field is used for containing a text message, but umm…what is greeting_id
field? Can we remove it? No. It’s important for the feature because Cizen needs to pick a response event up from tons of unrelated events in the world.
Define GreetingAutomaton
that responds the Greeting
event and returns the Reply
response event with a message.
alias Cizen.Effects.{Receive, Subscribe, Dispatch}
alias Cizen.{Event, Filter}defmodule HelloWorld.GreetingAutomaton do
use Cizen.Automaton defstruct [] @impl true
def spawn(id, _) do
# Subscribe "Greeting" event
perform id, %Subscribe{
event_filter: Filter.new(
fn %Event{body: %HelloWorld.Greeting{}} -> true end
)
} # No state in this automaton
:loop
end @impl true
def yield(id, :loop) do
# Wait for "Greeting" event
event = perform id, %Receive{} # Respond to "Greeting" event
perform id, %Dispatch{
body: %HelloWorld.Greeting.Reply{
greeting_id: event.id,
message: "Hello Cizen"
}
}
:loop
end
end
Oops…lots of code. Don’t be afraid, let’s take a look at the detail.
To define Cizen’s automata, use Cizen.Automaton
behavior and implement some lifecycle callbacks; spawn/2
and yield/2
. spawn/2
is called on start. It’s useful for an initialization such as subscribing events and determining an initial state of an automaton. By the way, defstruct
of an automaton is used for passing parameters to automaton on launch.
Cizen’s automata need to interact with others via performing “Effects”. To subscribe Greeting
events, we perform Subscribe
effect with a filter that passes only Greeting
events. yield/2
is called after returning an initial state from spawn/2
callback. Thanks to Elixir’s awesome pattern matching, we can also define multiple yield/2
based on the state (second argument of yield/2
).
yield/2
behaves like a general event loop; waits until arriving an event, then does something, yields new state, and loops infinitely. GreetingAutomaton
receives Greeting
event by performing Receive
effect, and then dispatches Greeting.Reply
response event that contains greeting_id
(comes from Greeting
event) and a message
“Hello Cizen” by performing Dispatch
effect.
Place above code in lib/hello_world/hello.ex
(source code). Don’t forget adding a bootstrap code to lib/hello_world/application.ex
(source code) in order to start the GreetingAutomaton
as part of a supervision tree.
defmodule HelloWorld.Application do
@moduledoc false use Application def start(_type, _args) do
children = [
HelloWorldWeb.Endpoint,
%{
id: HelloWorld.GreetingAutomaton,
start: {Cizen.Saga, :start_link,
[%HelloWorld.GreetingAutomaton{}]}
}
] opts = [strategy: :one_for_one, name: HelloWorld.Supervisor]
Supervisor.start_link(children, opts)
end [...]
end
By the way, where Greeting
event comes from? It’s Phoenix world. Let’s connect the both worlds. Open controllers/page_controller.ex
file (source code).
alias Cizen.Effects.Requestdefmodule HelloWorldWeb.PageController do
use HelloWorldWeb, :controller
use Cizen.Effectful def index(conn, _params) do
%{body: %{message: message}} = handle fn id ->
perform id, %Request{body: %HelloWorld.Greeting{}}
end render(conn, "index.html", message: message)
end
end
Cizen.Effectful
is useful to create a block which performs effects. In the block of handle
, perform Request
effect with Greeting
event and block until receiving Reply
response event from GreetingAutomaton
. This process runs every time Phoenix gets a request. To render a greeting message, update templates/page/index.html.eex
file.
<h1><%= @message %></h1>
Finally, we get a greeting from GreetingAutomaton
in the Cizen world.
As a conclusion, summarize what I wanted to present in this article.
- Automata drive only with Events
- Automata interact with others through performing Effects
- Use
Cizen.Effectful
to interact with the Cizen world
Thanks for reading. Enjoy Cizen! 🎉
Acknowledgment: Thank you for revising this article, Ryo33.