$30 off During Our Annual Pro Sale. View Details »

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.

Chris Keathley

March 11, 2016
Tweet

More Decks by Chris Keathley

Other Decks in Programming

Transcript

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

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

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

    • Types 101 & Dialyzer • Creating our own types • Type Composition • Putting it all together
  4. 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
  5. 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
  6. 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
  7. 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
  8. 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
  9. 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
  10. 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
  11. 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
  12. 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
  13. defmodule Queue do defstruct inbox: [], outbox: [] @type t

    :: %__MODULE__{ inbox: list(), outbox: list() } end
  14. 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
  15. 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
  16. 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
  17. 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
  18. def run do Queue.new |> Queue.push(:foo) |> Sizing.size # =>

    1 Tree.new({1, :foo}) |> Tree.insert({3, :bar}) |> Sizing.size # => 2 end
  19. defprotocol Sizing do @spec empty?(any()) :: boolean def empty?(structure) @spec

    size(any()) :: non_neg_integer def size(structure) end
  20. 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
  21. 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
  22. 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
  23. 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
  24. def run do Queue.new |> Queue.push(:foo) |> Presence.size # =>

    1 Tree.new({1, :foo}) |> Tree.insert({3, :bar}) |> Presence.size # => 2 end
  25. defmodule Alerts.Ticket.Email do defstruct from: nil, body: "", title: ""

    end defmodule Alerts.Ticket.SlackMessage do defstruct from: nil, message: "", channel: nil end
  26. 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
  27. 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
  28. 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
  29. 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