Contracts for building reliable systems

Contracts for building reliable systems

06f8b41980eb4c577fa40c41d5030c19?s=128

Chris Keathley

August 30, 2019
Tweet

Transcript

  1. 15.
  2. 34.
  3. 39.
  4. 42.
  5. 56.
  6. 57.

    rgb_to_hex :: Integer -> Integer -> Integer -> String 0

    <= x <= 255 Starts with a ‘#’ How do we type this?
  7. 59.

    rgb_to_hex :: Integer -> Integer -> Integer -> String 0

    <= x <= 255 Starts with a ‘#’ How do we type this?
  8. 60.

    rgb_to_hex :: Integer -> Integer -> Integer -> String 0

    <= x <= 255 Starts with a ‘#’ How do we type this? (today)
  9. 63.

    defmodule Contractual do use ExContract requires is_integer(r) and 0 <=

    r and r <= 255 requires is_integer(g) and 0 <= g and g <= 255 requires is_integer(b) and 0 <= b and b <= 255 def rgb_to_hex(r, g, b) do #… end end
  10. 64.

    defmodule Contractual do use ExContract requires is_integer(r) and 0 <=

    r and r <= 255 requires is_integer(g) and 0 <= g and g <= 255 requires is_integer(b) and 0 <= b and b <= 255 ensures is_binary(result) ensures String.starts_with?(result, "#") def rgb_to_hex(r, g, b) do #… end end
  11. 65.

    defmodule Contractual do use Vow requires is_integer(r) and 0 <=

    r and r <= 255 requires is_integer(g) and 0 <= g and g <= 255 requires is_integer(b) and 0 <= b and b <= 255 ensures is_binary(result) ensures String.starts_with?(result, "#") def rgb_to_hex(r, g, b) do #… end end This is your invariant
  12. 67.

    What happens when we get an error? rgb_to_hex(-10, 300, 0.5)

    ** (ExContract.RequiresException) Pre-condition ["is_integer(r) and 0 <= r and r <= 255"] violated. Invalid implementation of caller to function [rgb_to_hex/3]
  13. 69.

    We can also check side-effects requires !check_in_db?(user), "user should not

    be stored" ensures check_in_db?(user), "user should be stored" def store_user(user) do # ... end
  14. 70.

    We can also check side-effects requires !check_in_db?(user), "user should not

    be stored" ensures check_in_db?(user), "user should be stored" def store_user(user) do # ... end Shouldn’t be in the db afterwords it should
  15. 73.
  16. 76.

    defmodule Contractual do use ExContract requires is_integer(r) and 0 <=

    r and r <= 255 requires is_integer(g) and 0 <= g and g <= 255 requires is_integer(b) and 0 <= b and b <= 255 ensures is_binary(result) ensures String.starts_with?(result, "#") def rgb_to_hex(r, g, b) do #… end end
  17. 77.

    requires is_integer(r) and 0 <= r and r <= 255

    requires is_integer(g) and 0 <= g and g <= 255 requires is_integer(b) and 0 <= b and b <= 255 ensures is_binary(result) ensures String.starts_with?(result, "#")
  18. 78.

    is_integer(r) and 0 <= r and r <= 255 is_integer(g)

    and 0 <= g and g <= 255 is_integer(b) and 0 <= b and b <= 255 is_binary(result) String.starts_with?(result, "#")
  19. 79.

    is_integer(r) and 0 <= r and r <= 255 is_integer(g)

    and 0 <= g and g <= 255 is_integer(b) and 0 <= b and b <= 255 is_binary(result) String.starts_with?(result, "#") Is the data correct?
  20. 80.
  21. 81.

    is_integer(r) && 0 <= r and r <= 255 is_integer(g)

    && 0 <= g and g <= 255 is_integer(b) && 0 <= b and b <= 255 is_binary(result) && String.starts_with?(result, "#") Norm
  22. 82.

    def rgb, do: spec(is_integer() and (& 0 <= &1 and

    &1 <= 255)) is_binary(result) && String.starts_with?(result, "#") Norm
  23. 83.

    def rgb, do: spec(is_integer() and (& 0 <= &1 and

    &1 <= 255)) def hex, do: spec(is_binary() and (&String.starts_with?(&1, "#"))) Norm
  24. 84.

    Norm conform!(123, rgb()) 123 def rgb, do: spec(is_integer() and (&

    0 <= &1 and &1 <= 255)) def hex, do: spec(is_binary() and (&String.starts_with?(&1, "#")))
  25. 85.

    Norm conform!(123, rgb()) 123 conform!(-10, rgb()) ** (Norm.MismatchError) Could not

    conform input: val: -10 fails: &(0 <= &1 and &1 <= 255) (norm) lib/norm.ex:385: Norm.conform!/2 def rgb, do: spec(is_integer() and (& 0 <= &1 and &1 <= 255)) def hex, do: spec(is_binary() and (&String.starts_with?(&1, "#")))
  26. 86.

    defmodule Contractual do use ExContract requires is_integer(r) and 0 <=

    r and r <= 255 requires is_integer(g) and 0 <= g and g <= 255 requires is_integer(b) and 0 <= b and b <= 255 ensures is_binary(result) ensures String.starts_with?(result, "#") def rgb_to_hex(r, g, b) do #… end end
  27. 87.

    defmodule Contractual do use ExContract def rgb, do: spec(is_integer() and

    &(0 <= &1 and &1 <= 255)) def hex, do: spec(is_binary() and (&String.starts_with?(&1, "#"))) requires is_integer(r) and 0 <= r and r <= 255 requires is_integer(g) and 0 <= g and g <= 255 requires is_integer(b) and 0 <= b and b <= 255 ensures is_binary(result) ensures String.starts_with?(result, "#") def rgb_to_hex(r, g, b) do #… end end
  28. 88.

    defmodule Contractual do use ExContract def rgb, do: spec(is_integer() and

    &(0 <= &1 and &1 <= 255)) def hex, do: spec(is_binary() and (&String.starts_with?(&1, "#"))) requires valid?(r, rgb()) and valid?(g, rgb()) and valid?(b, rgb()) ensures valid?(result, hex()) def rgb_to_hex(r, g, b) do #… end end
  29. 90.

    Conforming atoms and tuples :atom = conform!(:atom, :atom) {1, "hello"}

    = conform!( {1, "hello"}, {spec(is_integer()), spec(is_binary())}) conform!({1, 2}, {:one, :two}) ** (Norm.MismatchError) val: 1 in: 0 fails: is not an atom. val: 2 in: 1 fails: is not an atom.
  30. 93.

    Conforming atoms and tuples result_spec = one_of([ {:ok, spec(is_binary())}, {:error,

    spec(fn _ -> true end)}, ]) {:ok, "alice"} = conform!(User.get_name(123), result_spec) {:error, "user does not exist"} = conform!(User.get_name(-42), result_spec)
  31. 96.

    Norm supports Schema’s user_schema = schema(%{ user: schema(%{ name: spec(is_binary()),

    age: spec(is_integer() and &(&1 > 0)) }) }) input = %{user: %{name: "chris", age: 30, email: “c@keathley.io"}
  32. 97.

    Norm supports Schema’s user_schema = schema(%{ user: schema(%{ name: spec(is_binary()),

    age: spec(is_integer() and &(&1 > 0)) }) }) input = %{user: %{name: "chris", age: 30, email: “c@keathley.io"} conform!(input, user_schema) => %{user: %{name: "chris", age: 30}}
  33. 98.

    Norm supports Schema’s user_schema = schema(%{ user: schema(%{ name: spec(is_binary()),

    age: spec(is_integer() and &(&1 > 0)) }) }) input = %{user: %{name: "chris", age: 30, email: “c@keathley.io"} conform!(input, user_schema) => %{user: %{name: "chris", age: 30}} Can start sending us new data whenever they need to
  34. 100.

    Norm supports optionality user_schema = schema(%{ user: schema(%{ name: spec(is_binary()),

    age: spec(is_integer() and &(&1 > 0)) }) }) How do you mark a key as optional?
  35. 109.

    Norm supports optionality user_schema = schema(%{ user: schema(%{ name: spec(is_binary()),

    age: spec(is_integer() and &(&1 > 0)) }) }) just_age = selection(user_schema, [user: [:age]])
  36. 110.

    Norm supports optionality user_schema = schema(%{ user: schema(%{ name: spec(is_binary()),

    age: spec(is_integer() and &(&1 > 0)) }) }) just_age = selection(user_schema, [user: [:age]]) conform!(%{user: %{name: "chris", age: 31}}, just_age) => %{user: %{age: 31}}
  37. 116.

    test "rgb works correctly" do assert rgb_to_hex(0, 10, 32) assert

    rgb_to_hex(nil, [:cat], "hey!") assert rgb_to_hex(-10, 300, 0.5) end
  38. 117.

    test "rgb works correctly" do assert rgb_to_hex(0, 10, 32) assert

    rgb_to_hex(nil, [:cat], "hey!") assert rgb_to_hex(-10, 300, 0.5) end Known Knowns
  39. 120.

    We already know what kind of data we want def

    rgb, do: spec(is_integer() and (& 0 <= &1 and &1 <= 255))
  40. 122.

    def rgb, do: spec(is_integer() and (& 0 <= &1 and

    &1 <= 255)) rgb_gen = Norm.gen(rgb()) Lets generate it instead
  41. 123.

    def rgb, do: spec(is_integer() and (& 0 <= &1 and

    &1 <= 255)) rgb_gen = Norm.gen(rgb()) rgb_gen |> Enum.take(5) [124, 4, 172, 217, 162]
  42. 127.

    test "but good this time" do check all r <-

    gen(rgb()), g <- gen(rgb()), end end
  43. 128.

    test "but good this time" do check all r <-

    gen(rgb()), g <- gen(rgb()), b <- gen(rgb()) do end end
  44. 129.

    test "but good this time" do check all r <-

    gen(rgb()), g <- gen(rgb()), b <- gen(rgb()) do end end What test do we write?
  45. 130.

    defmodule Contractual do use ExContract requires is_integer(r) and 0 <=

    r and r <= 255 requires is_integer(g) and 0 <= g and g <= 255 requires is_integer(b) and 0 <= b and b <= 255 ensures is_binary(result) ensures String.starts_with?(result, "#") def rgb_to_hex(r, g, b) do #… end end
  46. 131.

    test "but good this time" do check all r <-

    gen(rgb()), g <- gen(rgb()), b <- gen(rgb()) do end end
  47. 132.

    test "but good this time" do check all r <-

    gen(rgb()), g <- gen(rgb()), b <- gen(rgb()) do assert rgb_to_hex(r, g, b) end end
  48. 136.

    Special Thanks! * Jeff Utter * Jason Stewart * Lance

    Halvorsen * Greg Mefford * Jeff Weiss * The Outlaws