State Machines in Elixir

98195776df79590269541395c699f816?s=47 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.

98195776df79590269541395c699f816?s=128

João Moura

September 22, 2018
Tweet

Transcript

  1. 2.

    State Machines State Machines in Elixir in Elixir with Machinery

    @joaomdmoura Avoiding Complexity in Software Design Software Design
  2. 3.

    State Machines State Machines in Elixir in Elixir with Machinery

    @joaomdmoura That time I fell from a Stakeboard Stakeboard
  3. 4.
  4. 6.
  5. 7.
  6. 8.
  7. 9.
  8. 10.
  9. 13.
  10. 18.

    It is an abstract machine that can be in exactly

    one of a finite number of states at any given time.
  11. 19.
  12. 23.
  13. 27.

    empty lock items add item in stock? no yes filled

    checkout leave payed? no payed yes abandoned unblock items
  14. 29.
  15. 31.
  16. 32.
  17. 33.
  18. 35.
  19. 36.
  20. 37.
  21. 38.
  22. 41.

    Functions must not Functions must not depend on variables other

    depend on variables other than its parameters than its parameters
  23. 46.

    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
  24. 47.

    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
  25. 48.

    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
  26. 49.

    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
  27. 50.

    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
  28. 51.

    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
  29. 52.

    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
  30. 53.

    fsm fsm Fsm is pure functional finite Fsm is pure

    functional finite state machine state machine
  31. 54.

    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
  32. 57.
  33. 58.
  34. 59.
  35. 60.
  36. 69.

    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
  37. 70.

    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
  38. 71.

    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
  39. 72.

    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
  40. 74.

    defmodule YourApp.Endpoint do # ... plug Machinery.Plug # ... end

    config :machinery, interface: true, repo: YourApp.Repo, model: YourApp.User, module: YourApp.UserStateMachine
  41. 75.
  42. 76.
  43. 80.