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

Contracts for building reliable systems

Contracts for building reliable systems

Avatar for Chris Keathley

Chris Keathley

August 30, 2019
Tweet

More Decks by Chris Keathley

Other Decks in Programming

Transcript

  1. rgb_to_hex :: Integer -> Integer -> Integer -> String 0

    <= x <= 255 Starts with a ‘#’ How do we type this?
  2. rgb_to_hex :: Integer -> Integer -> Integer -> String 0

    <= x <= 255 Starts with a ‘#’ How do we type this?
  3. rgb_to_hex :: Integer -> Integer -> Integer -> String 0

    <= x <= 255 Starts with a ‘#’ How do we type this? (today)
  4. 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
  5. 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
  6. 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
  7. 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]
  8. 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
  9. 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
  10. 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. 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, "#")
  12. 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, "#")
  13. 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?
  14. 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
  15. def rgb, do: spec(is_integer() and (& 0 <= &1 and

    &1 <= 255)) is_binary(result) && String.starts_with?(result, "#") Norm
  16. def rgb, do: spec(is_integer() and (& 0 <= &1 and

    &1 <= 255)) def hex, do: spec(is_binary() and (&String.starts_with?(&1, "#"))) Norm
  17. 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, "#")))
  18. 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, "#")))
  19. 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
  20. 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
  21. 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
  22. 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.
  23. 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)
  24. 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: “[email protected]"}
  25. 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: “[email protected]"} conform!(input, user_schema) => %{user: %{name: "chris", age: 30}}
  26. 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: “[email protected]"} conform!(input, user_schema) => %{user: %{name: "chris", age: 30}} Can start sending us new data whenever they need to
  27. 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?
  28. 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]])
  29. 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}}
  30. 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
  31. 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
  32. We already know what kind of data we want def

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

    &1 <= 255)) rgb_gen = Norm.gen(rgb()) Lets generate it instead
  34. 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]
  35. test "but good this time" do check all r <-

    gen(rgb()), g <- gen(rgb()), end end
  36. test "but good this time" do check all r <-

    gen(rgb()), g <- gen(rgb()), b <- gen(rgb()) do end end
  37. 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?
  38. 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
  39. test "but good this time" do check all r <-

    gen(rgb()), g <- gen(rgb()), b <- gen(rgb()) do end end
  40. 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
  41. Special Thanks! * Jeff Utter * Jason Stewart * Lance

    Halvorsen * Greg Mefford * Jeff Weiss * The Outlaws