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

Contracts for building reliable systems

Contracts for building reliable systems

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