Upgrade to Pro — share decks privately, control downloads, hide ads and more …

State Machines in Elixir

João Moura
September 22, 2018

State Machines in Elixir

Great software should be easy to understand and improve, but usually, developers tend to bring unnecessary complexity to the code base. Building a state machine is one of those things that usually brings a lot of boilerplate and overall complexity with it, but what if we had a better and simple way to build states machines while keeping our sanity, simplicity, and capacity to easily extend it?

In this talk we’ll understand state machines:
- how it’s a well-defined concept that developers all around have been using
- how implementing it without thinking about it can lead to some really nasty code smells
- what solutions we have for it in the Elixir ecosystem

But we won’t stop there, I’ll also share how I have been through it and solved this problem myself by building an opensource library called Machinery, and how it can help you to build nice, clean and expandable state machines while providing you with some great features.

João Moura

September 22, 2018
Tweet

More Decks by João Moura

Other Decks in Programming

Transcript

  1. State Machines State Machines in Elixir in Elixir with Machinery

    @joaomdmoura Avoiding Complexity in Software Design Software Design
  2. State Machines State Machines in Elixir in Elixir with Machinery

    @joaomdmoura That time I fell from a Stakeboard Stakeboard
  3. It is an abstract machine that can be in exactly

    one of a finite number of states at any given time.
  4. empty lock items add item in stock? no yes filled

    checkout leave payed? no payed yes abandoned unblock items
  5. Functions must not Functions must not depend on variables other

    depend on variables other than its parameters than its parameters
  6. defmodule YourProject.UserStateMachine do @behaviour :gen_statem @name :user_state_machine def start_link do

    :gen_statem.start_link({:local, @name}, __MODULE__, [], []) end def init([]) do {:ok, :created, %{name: "Joe Doe"}} end def callback_mode do :state_functions end def fill_data do :gen_statem.call(@name, :fill_data) end def completed({:call, from}, :fill_data, data) do {:next_state, :completed, data, [{:reply, from, :ok}]} end end
  7. defmodule YourProject.UserStateMachine do @behaviour :gen_statem @name :user_state_machine def start_link do

    :gen_statem.start_link({:local, @name}, __MODULE__, [], []) end def init([]) do {:ok, :created, %{name: "Joe Doe"}} end def callback_mode do :state_functions end def fill_data do :gen_statem.call(@name, :fill_data) end def completed({:call, from}, :fill_data, data) do {:next_state, :completed, data, [{:reply, from, :ok}]} end end
  8. defmodule YourProject.UserStateMachine do @behaviour :gen_statem @name :user_state_machine def start_link do

    :gen_statem.start_link({:local, @name}, __MODULE__, [], []) end def init([]) do {:ok, :created, %{name: "Joe Doe"}} end def callback_mode do :state_functions end def fill_data do :gen_statem.call(@name, :fill_data) end def completed({:call, from}, :fill_data, data) do {:next_state, :completed, data, [{:reply, from, :ok}]} end end
  9. defmodule YourProject.UserStateMachine do @behaviour :gen_statem @name :user_state_machine def start_link do

    :gen_statem.start_link({:local, @name}, __MODULE__, [], []) end def init([]) do {:ok, :created, %{name: "Joe Doe"}} end def callback_mode do :state_functions end def fill_data do :gen_statem.call(@name, :fill_data) end def completed({:call, from}, :fill_data, data) do {:next_state, :completed, data, [{:reply, from, :ok}]} end end
  10. defmodule YourProject.UserStateMachine do @behaviour :gen_statem @name :user_state_machine def start_link do

    :gen_statem.start_link({:local, @name}, __MODULE__, [], []) end def init([]) do {:ok, :created, %{name: "Joe Doe"}} end def callback_mode do :state_functions end def fill_data do :gen_statem.call(@name, :fill_data) end def completed({:call, from}, :fill_data, data) do {:next_state, :completed, data, [{:reply, from, :ok}]} end end
  11. defmodule YourProject.UserStateMachine do @behaviour :gen_statem @name :user_state_machine def start_link do

    :gen_statem.start_link({:local, @name}, __MODULE__, [], []) end def init([]) do {:ok, :created, %{name: "Joe Doe"}} end def callback_mode do :state_functions end def fill_data do :gen_statem.call(@name, :fill_data) end def completed({:call, from}, :fill_data, data) do {:next_state, :completed, data, [{:reply, from, :ok}]} end end
  12. defmodule YourProject.UserStateMachine do @behaviour :gen_statem @name :user_state_machine def start_link do

    :gen_statem.start_link({:local, @name}, __MODULE__, [], []) end def init([]) do {:ok, :created, %{name: "Joe Doe"}} end def callback_mode do :state_functions end def fill_data do :gen_statem.call(@name, :fill_data) end def completed({:call, from}, :fill_data, data) do {:next_state, :completed, data, [{:reply, from, :ok}]} end end
  13. fsm fsm Fsm is pure functional finite Fsm is pure

    functional finite state machine state machine
  14. defmodule BasicFsm do use Fsm, initial_state: :stopped defstate stopped do

    # opens the state scope defevent run do # defines event next_state(:running) # transition to next state end end defstate running do defevent stop do next_state(:stopped) end end end
  15. defmodule FakeProject.ShoppingCartMachine do use Machinery, states: ["empty", "filled", "payed", "abandoned"],

    transitions: %{ "empty" => "filled", "filled" => ["payed", "abandoned"] } def guard_function(cart, "filled") do Item.has_stock?(cart.item) end def guard_function(cart, "payed") do Payment.status(cart) == :confirmed end def before_transition(cart, "filled") do Item.lock_form_cart(cart) cart end def after_transition(cart, "abadonned") do Item.unlock_form_cart(cart) cart end end
  16. defmodule FakeProject.ShoppingCartMachine do use Machinery, states: ["empty", "filled", "payed", "abandoned"],

    transitions: %{ "empty" => "filled", "filled" => ["payed", "abandoned"] } def guard_function(cart, "filled") do Item.has_stock?(cart.item) end def guard_function(cart, "payed") do Payment.status(cart) == :confirmed end def before_transition(cart, "filled") do Item.lock_form_cart(cart) cart end def after_transition(cart, "abadonned") do Item.unlock_form_cart(cart) cart end end
  17. defmodule FakeProject.ShoppingCartMachine do use Machinery, states: ["empty", "filled", "payed", "abandoned"],

    transitions: %{ "empty" => "filled", "filled" => ["payed", "abandoned"] } def guard_function(cart, "filled") do Item.has_stock?(cart.item) end def guard_function(cart, "payed") do Payment.status(cart) == :confirmed end def before_transition(cart, "filled") do Item.lock_form_cart(cart) cart end def after_transition(cart, "abadonned") do Item.unlock_form_cart(cart) cart end end
  18. defmodule FakeProject.ShoppingCartMachine do use Machinery, states: ["empty", "filled", "payed", "abandoned"],

    transitions: %{ "empty" => "filled", "filled" => ["payed", "abandoned"] } def guard_function(cart, "filled") do Item.has_stock?(cart.item) end def guard_function(cart, "payed") do Payment.status(cart) == :confirmed end def before_transition(cart, "filled") do Item.lock_form_cart(cart) cart end def after_transition(cart, "abadonned") do Item.unlock_form_cart(cart) cart end end
  19. defmodule YourApp.Endpoint do # ... plug Machinery.Plug # ... end

    config :machinery, interface: true, repo: YourApp.Repo, model: YourApp.User, module: YourApp.UserStateMachine