Railway Programming in Elixir with with

9ffad8bbc282b748763697965f27b3c8?s=47 Chris Bell
October 27, 2016

Railway Programming in Elixir with with

Slides from my talk at the Empire City Halloween event.

9ffad8bbc282b748763697965f27b3c8?s=128

Chris Bell

October 27, 2016
Tweet

Transcript

  1. EMPEX HALLOWEEN Railway Programming in Elixir with with Chris Bell

    • @cjbell_
  2. <3 |>

  3. Allows us to easily express a set of combined steps

    and operations on data
  4. def process_checkout(order) do order |> update_order() |> capture_payment() |> send_notification_email()

    |> update_stock_levels()
 end
  5. But what about errors?

  6. We use tagged tuples to represent success and error states.

  7. {:ok, _} | {:error, _}

  8. How do we stop the execution of these functions if

    one fails?
  9. Update Order Capture Payment Send Email Update Stock process_checkout Order

    {:ok, order} {:error, reason}
  10. On success we pass through to the next function, on

    error we switch.
  11. Scott Wlaschin’s Railway Orientated Programming http://fsharpforfunandprofit.com/rop/

  12. So how do we implement this in Elixir?

  13. defp update_order({:ok, order}) do # Do logic
 {:ok, order} end


    defp update_order({:error, reason}) do
 # Pass through {:error, reason} end
 # Repeat for every method
  14. Lets do this a different way…

  15. This is Haskell the dog and Monad the bear

  16. defmodule Result do def bind({:ok, value}, func) do func.(value) end

    def bind({:error, _} = failure,_) do 
 failure
 end end
  17. import Result
 
 def process_checkout(order) do order |> bind(&update_order/1) |>

    bind(&capture_payment/1) |> bind(&send_notification_email/1) |> bind(&update_stock_levels/1)
 end
  18. import Result
 
 def process_checkout(order) do order |> bind(&update_order/1) |>

    bind(&capture_payment/1) |> bind(&send_notification_email/1) |> bind(&update_stock_levels/1)
 end Put down that Monad, theres another way.
  19. with

  20. Allows us to execute a set of clauses only if

    the previous clause matches
  21. def process_checkout(order) do with {:ok, order} <- update_order(order), {:ok, order}

    <- capture_payment(order), {:ok, order} <- send_notification_email(order), {:ok, order} <- update_stock_levels(order), do: {:ok, order}
 end
  22. If the clause doesn’t match, the non matching value is

    returned and all subsequent steps are skipped.
  23. Update Order Capture Payment Send Email Update Stock process_checkout Order

    {:ok, order} {:error, reason}
  24. As of Elixir 1.3 we can also use an else

    block to handle non- matching clauses
  25. def process_checkout(order) do with {:ok, order} <- update_order(order), {:ok, order}

    <- capture_payment(order), {:ok, order} <- send_notification_email(order), {:ok, order} <- update_stock_levels(order) do 
 {:ok, order} else {:error, %Changeset{} = cs} -> handle_error(cs) {:error, reason} -> handle_other_error(reason) end
 end
  26. Ensign Bell’s top tip: 
 Usage with Ecto.Repo.transaction

  27. def process_checkout(order) do Repo.transaction(fn ->
 do_process_checkout(order)
 |> case do
 {:ok,

    order} -> order
 {:error, cs} -> Repo.rollback(cs)
 end
 end)
 end
 
 def do_process_checkout(order) do
 # Everything here returns a Ecto.Changeset with {:ok, order} <- update_order(order), {:ok, order} <- capture_payment(order), {:ok, order} <- send_notification_email(order), {:ok, order} <- update_stock_levels(order),
 do: {:ok, order}
 end
  28. Thank you. Questions? chris@madebymany.com