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

Well Typed Elixir

Well Typed Elixir

As developers, types are a part of our lives. Far too often types are seen as annoyances instead of as design tools. This leads many developers to cast aside their types often to their detriment.

In this talk we’ll explore powerful type systems present in other languages like Haskell and discuss novel ways that we can leverage Elixir's type system to produce safer and more correct software.

06f8b41980eb4c577fa40c41d5030c19?s=128

Chris Keathley

March 11, 2016
Tweet

Transcript

  1. Well Typed Elixir Chris Keathley / @ChrisKeathley / keathley.io

  2. Lets talk about:

  3. Lets talk about: • Why should we care about types?

  4. Lets talk about: • Why should we care about types?

    • Types 101 & Dialyzer
  5. Lets talk about: • Why should we care about types?

    • Types 101 & Dialyzer • Creating our own types
  6. Lets talk about: • Why should we care about types?

    • Types 101 & Dialyzer • Creating our own types • Type Composition
  7. Lets talk about: • Why should we care about types?

    • Types 101 & Dialyzer • Creating our own types • Type Composition • Putting it all together
  8. Why should we care about types?

  9. Monads

  10. Lets not talk about monads

  11. But here’s the gist if you’re interested https://git.io/v2jzJ

  12. “This is Erlang. Just Let it Crash™”

  13. Types are not a replacement for OTP

  14. Mistake Proofing

  15. Complexity

  16. Types 101

  17. defmodule ElixirTypes do def run do calculate_balance(50, 30) end def

    calculate_balance(a, b), do: a - b end
  18. defmodule ElixirTypes do def run do calculate_balance(50, :some_atom) end def

    calculate_balance(a, b), do: a - b end
  19. defmodule ElixirTypes do def run do calculate_balance(50, :some_atom) end def

    calculate_balance(a, b), do: a - b end LOL
  20. Dialyzer

  21. github.com/jeremyjh/dialyxir github.com/fishcakez/dialyze

  22. $ mix dialyzer.plt

  23. None
  24. defmodule ElixirTypes do def run do balance = 50 transaction

    = pay(:andra, 20) calculate_balance(balance, money(transaction)) end def pay(name, amount), do: {name, amount} def money({amount, _name}), do: amount def calculate_balance(a, b), do: a - b end
  25. defmodule ElixirTypes do def run do balance = 50 transaction

    = pay(:andra, 20) calculate_balance(balance, money(transaction)) end def pay(name, amount), do: {name, amount} def money({amount, _name}), do: amount def calculate_balance(a, b), do: a - b end Oops
  26. Dialyzer

  27. None
  28. ಠ_ಠ

  29. Why is this not an error?

  30. A bit of history

  31. def send(pg, mess) when is_atom(pg) do :global.send(pg, {:send, self(), mess})

    end def send(pg, mess) when is_pid(pg) do send(pg, self(), mess) end
  32. Success Types

  33. def and(true, true), do: true def and(_, false), do: false

    def and(false, _), do: false
  34. and :: bool() -> bool() -> bool()

  35. and :: any() -> any() -> bool()

  36. A Type is a Proposition

  37. and(true, true) # => true and(“foo", false) # => false

    and(false, 3) # => false
  38. Dialyzer is optimistic

  39. If Dialyzer complains there’s almost certainly a problem

  40. We need to help dialyzer out

  41. @typespecs

  42. @type balance :: integer() @type transaction :: {atom(), integer()}

  43. defmodule ElixirTypes do @type balance :: integer() @type transaction ::

    {atom(), integer()} def run do balance = 50 transaction = pay(:andra, 20) calculate_balance(balance, money(transaction)) end def pay(name, amount), do: {name, amount} def money({amount, _name}), do: amount def calculate_balance(a, b), do: a - b end
  44. defmodule ElixirTypes do @type balance :: integer() @type transaction ::

    {atom(), integer()} def run do balance = 50 transaction = pay(:andra, 20) calculate_balance(balance, money(transaction)) end @spec pay(atom, integer) :: transaction def pay(name, amount), do: {name, amount} @spec money(transaction) :: integer def money({amount, _name}), do: amount @spec calculate_balance(integer, integer) :: balance def calculate_balance(a, b), do: a - b end
  45. None
  46. (˽°□°҂˽Ɨ ˍʓˍ

  47. dialyzer: [ flags: ["-Woverspecs"] ]

  48. None
  49. defmodule ElixirTypes do @type balance :: integer @type transaction ::

    {atom, integer} def run do balance = 50 transaction = pay(:andra, 20) calculate_balance(balance, money(transaction)) end @spec pay(atom, integer) :: transaction def pay(name, amount), do: {name, amount} @spec money(transaction) :: integer def money({amount, _name}), do: amount @spec calculate_balance(integer, integer) :: balance def calculate_balance(a, b), do: a - b end
  50. defmodule ElixirTypes do @type balance :: integer @type transaction ::

    {atom, integer} def run do balance = 50 transaction = pay(:andra, 20) calculate_balance(balance, money(transaction)) end @spec pay(atom, integer) :: transaction def pay(name, amount), do: {name, amount} @spec money(transaction) :: integer def money({amount, _name}) when is_integer(amount) do amount end @spec calculate_balance(integer, integer) :: balance def calculate_balance(a, b), do: a - b end
  51. None
  52. defmodule ElixirTypes do @type balance :: integer @type transaction ::

    {atom, integer} def run do balance = 50 transaction = pay(:andra, 20) calculate_balance(balance, money(transaction)) end @spec pay(atom, integer) :: transaction def pay(name, amount), do: {name, amount} @spec money(transaction) :: integer def money({_name, amount}) when is_integer(amount) do amount end @spec calculate_balance(integer, integer) :: balance def calculate_balance(a, b), do: a - b end
  53. @type balance :: integer @type transaction :: {name, integer} @type

    name :: atom | String.t
  54. defmodule ElixirTypes do @type name :: atom | String.t @type

    transaction :: {name, integer} @type ledger(type) :: {:ledger, list(type), list(type)} @spec new :: ledger(transaction) def new() do {:ledger, [pay(:andra, 20)], [pay(:chris, 10)]} end @spec pay(atom, integer) :: transaction def pay(name, amount), do: {name, amount} end
  55. None
  56. Creating our own data types

  57. Structs

  58. defmodule Queue do defstruct inbox: [], outbox: [] end

  59. defmodule Queue do defstruct inbox: [], outbox: [] @type t

    :: %__MODULE__{ inbox: list(), outbox: list() } end
  60. defmodule Queue do defstruct inbox: [], outbox: [] @type t

    :: %__MODULE__{ inbox: list(), outbox: list() } @spec push(Queue.t, any()) :: Queue.t @spec pop(Queue.t) :: {any(), Queue.t} @spec new :: Queue.t end
  61. def run do Queue.new |> Queue.push(:foo) |> Queue.push(:bar) |> Queue.pop

    end
  62. defmodule Queue do defstruct inbox: [], outbox: [] @spec size(Queue.t)

    :: non_neg_integer def size(%Queue{inbox: i, outbox: o}) do Enum.count(i) + Enum.count(o) end end
  63. defmodule Queue do defstruct inbox: [], outbox: [] @spec size(Queue.t)

    :: non_neg_integer def size(%Queue{inbox: i, outbox: o}) do Enum.count(i) + Enum.count(o) end end defmodule Tree do defstruct key: nil, left: nil, right: nil @spec size(Tree.t) :: non_neg_integer def size(%Tree{left: left, right: right}) do size(left) + size(right) + 1 end def size(nil), do: 0 end
  64. defprotocol Sizing do @spec size(any()) :: integer def size(structure) end

  65. defimpl Sizing, for: Queue do def size(%Queue{inbox: i, outbox: o})

    do Enum.count(i) + Enum.count(o) end end defimpl Sizing, for: Tree do def size(%Tree{left: left, right: right}) do size(left) + size(right) + 1 end def size(nil), do: 0 end
  66. def run do Queue.new |> Queue.push(:foo) |> Sizing.size # =>

    1 Tree.new({1, :foo}) |> Tree.insert({3, :bar}) |> Sizing.size # => 2 end
  67. Protocols are opt in polymorphism

  68. We gain more use from our common apis

  69. defprotocol Sizing do @spec empty?(any()) :: boolean def empty?(structure) @spec

    size(any()) :: non_neg_integer def size(structure) end
  70. defimpl Sizing, for: Tree do def empty?(t), do: size(t) >

    0 def size(%Tree{left: left, right: right}) do size(left) + size(right) + 1 end def size(nil), do: 0 end defimpl Sizing, for: Queue do def empty?(q), do: size(q) > 0 def size(%Queue{inbox: i, outbox: o}) do Enum.count(i) + Enum.count(o) end end
  71. defmodule Presence do def empty?(thing), do: size(thing) == 0 def

    size(thing), do: Presence.Sizing.size(thing) end defprotocol Presence.Sizing do @spec size(any()) :: non_neg_integer def size(structure) end
  72. defimpl Presence.Sizing, for: Tree do def size(%Tree{left: left, right: right})

    do size(left) + size(right) + 1 end def size(nil), do: 0 end defimpl Presence.Sizing, for: Queue do def size(%Queue{inbox: i, outbox: o}) do Enum.count(i) + Enum.count(o) end end
  73. defmodule Presence do def present?(thing), do: size(thing) > 0 def

    empty?(thing), do: size(thing) == 0 def size(thing), do: Presence.Sizing.size(thing) end defprotocol Presence.Sizing do @spec size(any()) :: non_neg_integer def size(structure) end
  74. def run do Queue.new |> Queue.push(:foo) |> Presence.size # =>

    1 Tree.new({1, :foo}) |> Tree.insert({3, :bar}) |> Presence.size # => 2 end
  75. Type composition

  76. f2 :: B() -> C() f1 :: A() -> B()

  77. B() -> C() A() -> B()

  78. A() -> B() -> C()

  79. A() -> (Class b) -> C()

  80. A() -> (Presence any()) -> C()

  81. Putting it together

  82. Email Slack Ticketing system Ticket processing Notification

  83. defmodule Alerts.Ticket.Email do defstruct from: nil, body: "", title: ""

    end defmodule Alerts.Ticket.SlackMessage do defstruct from: nil, message: "", channel: nil end
  84. defmodule Alerts.Adapters.SlackMessage do def receive(json) when is_binary(json) do json |>

    Poison.decode(as: %Slack{}) |> Ticket.process end end defmodule Alerts.EmailAdapter do def receive(email) when is_binary(email) do email |> Email.parse_from_email_string(email) |> Ticket.process end end
  85. defmodule Alerts.Ticket do @type requester :: atom | String.t @type

    message :: String.t @type severity :: 1..5 @type t :: %{ requester: requester, message: message, severity: severity } defstruct requester: nil, message: nil, severity: 1 def process(data) do ticket = to_ticket(data) TicketQueue.push(ticket) Alerts.Notifier.notify_on_call_staff(ticket) end @spec to_ticket(any()) :: t def to_ticket(data) do Alerts.Ticket.Fields.to_ticket(data) end end
  86. defmodule Alerts.Notifier do alias Alerts.Ticket @spec notify_on_call_staff(Ticket.t) :: Email.t def

    notify_on_call_staff(%Ticket{}) do # ... end end defmodule Alerts.TicketQueue do alias Alerts.Ticket @spec push(Ticket.t) :: TicketQueue.t def push(%Ticket{}) do # ... end end
  87. defprotocol Alerts.Ticket.Fields do @spec to_ticket(any()) :: Alerts.Ticket.t def to_ticket(thing) end

  88. defimpl Alerts.Ticket.Fields, for: Alerts.Ticket.Email do def to_ticket(email) do %Alerts.Ticket{ requester:

    email.from, message: email.body, severity: severity(email.title) } end def severity(title), do: # ... end defimpl Alerts.Ticket.Fields, for: Alerts.Ticket.SlackMessage do def to_ticket(msg) do %Alerts.Ticket{ requester: msg.from, message: msg.message, severity: severity(msg.message) } end def severity(message), do: #... end
  89. Future work

  90. Something that understands elixir a bit better

  91. Building the PLT sucks

  92. Better errors

  93. Resources • Success Types - http://user.it.uu.se/~kostis/Papers/succ_types.pdf • http://blog.plataformatec.com.br/2014/09/writing-assertive-code-with- elixir/ •

    http://learnyousomeerlang.com/dialyzer • http://learnyousomeerlang.com/types-or-lack-thereof
  94. Thanks Chris Keathley / @ChrisKeathley / keathley.io