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. Contracts for building
    robust systems
    Chris Keathley / @ChrisKeathley / [email protected]

    View Slide

  2. Confession

    View Slide

  3. I like dynamic languages

    View Slide

  4. Contracts for building
    robust systems
    Chris Keathley / @ChrisKeathley / [email protected]

    View Slide

  5. Chris Keathley / @ChrisKeathley / [email protected]
    Contracts for building
    robust systems
    Correct

    View Slide

  6. Correctness: the
    quality or state of being
    free from error; accuracy.

    View Slide

  7. Robust: able to
    withstand or overcome
    adverse conditions.

    View Slide

  8. My life
    Service
    Service
    Service
    Service
    Service

    View Slide

  9. My life
    Service
    Service
    Service
    Service
    Service
    Teams

    View Slide

  10. My life
    Service
    Service
    Service
    Service
    Service
    Teams
    Me

    View Slide

  11. Session
    Types

    View Slide

  12. My life
    Service
    Service
    Service
    Service
    Service
    Teams
    Me

    View Slide

  13. Correctness

    View Slide

  14. Correctness(t)
    Correctness over time

    View Slide

  15. Change

    View Slide

  16. Change
    Growth Breakage

    View Slide

  17. Changes
    Service Service

    View Slide

  18. Changes
    Service Service
    Expects users

    View Slide

  19. Changes
    Service Service

    View Slide

  20. Changes
    Service Service
    User{}

    View Slide

  21. Changes
    Service Service
    User{}

    View Slide

  22. Changes
    Service Service

    View Slide

  23. Changes
    Service Service
    Now you can send me
    Users or Cats

    View Slide

  24. Changes
    Service Service
    Now you can send me
    Users or Cats
    User{}

    View Slide

  25. Changes
    Service Service
    Now you can send me
    Users or Cats
    User{}

    View Slide

  26. Changes
    Service Service

    View Slide

  27. Changes
    Service Service
    Now you can only send me Cats

    View Slide

  28. Changes
    Service Service
    Now you can only send me Cats
    User{}

    View Slide

  29. Changes
    Service Service
    Now you can only send me Cats
    User{}

    View Slide

  30. Breaking
    changes require
    coordination

    View Slide

  31. Why does software change?

    View Slide

  32. Why does software change?
    Aesthetics

    View Slide

  33. Why does software change?
    Aesthetics
    Refactors

    View Slide

  34. Refactor?

    View Slide

  35. Why does software change?
    Aesthetics
    Refactors

    View Slide

  36. Why does software change?
    Aesthetics Technology
    Refactors

    View Slide

  37. Why does software change?
    Aesthetics Technology
    Requirements
    Refactors

    View Slide

  38. Why does software change?
    Aesthetics Technology
    Requirements
    Tacitly expects no breaking changes
    Refactors

    View Slide

  39. We want to ensure
    that we didn’t break
    the api while allowing
    for growth

    View Slide

  40. Lets talk about…
    Contracts
    Data specification
    Testing strategies

    View Slide

  41. Lets talk about…
    Contracts
    Data specification
    Testing strategies

    View Slide

  42. function

    View Slide

  43. function
    Input

    View Slide

  44. function
    Input Output

    View Slide

  45. function
    Input Output
    Side-effects

    View Slide

  46. function
    Input Output
    Side-effects
    Enforce the callers are correct

    View Slide

  47. function
    Input Output
    Side-effects
    Enforce the callers are correct
    Ensure the function is correct

    View Slide

  48. function
    Input Output
    Side-effects

    View Slide

  49. function
    Input Output
    Side-effects
    Pre-conditions Post-conditions

    View Slide

  50. rgb_to_hex

    View Slide

  51. rgb_to_hex(0, 10, 32) => “#000A20”

    View Slide

  52. rgb_to_hex ::

    View Slide

  53. rgb_to_hex :: Integer -> Integer -> Integer

    View Slide

  54. rgb_to_hex :: Integer -> Integer -> Integer -> String

    View Slide

  55. rgb_to_hex :: Integer -> Integer -> Integer -> String
    0 <= x <= 255

    View Slide

  56. rgb_to_hex :: Integer -> Integer -> Integer -> String
    0 <= x <= 255
    Starts with a ‘#’

    View Slide

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

    View Slide

  58. Dependent
    Types

    View Slide

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

    View Slide

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

    View Slide

  61. Design by
    Contract
    github.com/JDUnity/ex_contract

    View Slide

  62. defmodule Contractual do
    use ExContract
    def rgb_to_hex(r, g, b) do
    #…
    end
    end

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

  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]

    View Slide

  68. Don’t we already have
    guard clauses?

    View Slide

  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

    View Slide

  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

    View Slide

  71. Contracts
    are powerful

    View Slide

  72. Isn’t this
    really slow?

    View Slide

  73. Yes!

    View Slide

  74. Lets talk about…
    Contracts
    Data specification
    Testing strategies

    View Slide

  75. Lets talk about…
    Contracts
    Data specification
    Testing strategies

    View Slide

  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

    View Slide

  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, "#")

    View Slide

  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, "#")

    View Slide

  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?

    View Slide

  80. Norm

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

  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, "#")))

    View Slide

  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, "#")))

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  89. Norm can match on
    atoms and tuples

    View Slide

  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.

    View Slide

  91. Conforming atoms and tuples
    {:ok, spec(is_binary())}

    View Slide

  92. Conforming atoms and tuples
    result_spec = one_of([
    {:ok, spec(is_binary())},
    {:error, spec(fn _ -> true end)},
    ])

    View Slide

  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)

    View Slide

  94. We can compose
    specs into schema’s

    View Slide

  95. Norm supports Schema’s
    user_schema = schema(%{
    user: schema(%{
    name: spec(is_binary()),
    age: spec(is_integer() and &(&1 > 0))
    })
    })

    View Slide

  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: “[email protected]"}

    View Slide

  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: “[email protected]"}
    conform!(input, user_schema)
    => %{user: %{name: "chris", age: 30}}

    View Slide

  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: “[email protected]"}
    conform!(input, user_schema)
    => %{user: %{name: "chris", age: 30}}
    Can start sending us
    new data whenever
    they need to

    View Slide

  99. Norm supports optionality
    user_schema = schema(%{
    user: schema(%{
    name: spec(is_binary()),
    age: spec(is_integer() and &(&1 > 0))
    })
    })

    View Slide

  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?

    View Slide

  101. Optionality is defined
    by the callsite. Not
    the data structure.

    View Slide

  102. Optionality
    plug
    plug
    plug
    assigns: %{}

    View Slide

  103. Optionality
    assigns: %{}
    plug
    plug
    plug

    View Slide

  104. Optionality
    assigns: %{}
    plug
    plug
    plug
    Assigns a
    user id

    View Slide

  105. Optionality
    assigns: %{user_id: 42}
    plug
    plug
    plug
    Assigns a
    user id

    View Slide

  106. Optionality
    assigns: %{user_id: 42}
    plug
    plug
    plug

    View Slide

  107. Optionality
    assigns: %{user_id: 42}
    plug
    plug
    plug
    Requires the
    user id

    View Slide

  108. Norm supports optionality
    user_schema = schema(%{
    user: schema(%{
    name: spec(is_binary()),
    age: spec(is_integer() and &(&1 > 0))
    })
    })

    View Slide

  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]])

    View Slide

  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}}

    View Slide

  111. Lets talk about…
    Contracts
    Data specification
    Testing strategies

    View Slide

  112. Lets talk about…
    Contracts
    Data specification
    Testing strategies

    View Slide

  113. test "rgb works correctly" do
    end

    View Slide

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

    View Slide

  115. test "rgb works correctly" do
    assert rgb_to_hex(0, 10, 32)
    assert rgb_to_hex(nil, [:cat], "hey!")
    end

    View Slide

  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

    View Slide

  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

    View Slide

  118. “Writing unit tests is a
    waste of time”
    - John Hughes

    View Slide

  119. “Writing unit tests is a
    waste of time”
    - John Hughes
    - Chris Keathley

    View Slide

  120. We already know what
    kind of data we want
    def rgb, do: spec(is_integer() and (& 0 <= &1 and &1 <= 255))

    View Slide

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

    View Slide

  122. def rgb, do: spec(is_integer() and (& 0 <= &1 and &1 <= 255))
    rgb_gen = Norm.gen(rgb())
    Lets generate it instead

    View Slide

  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]

    View Slide

  124. test "but good this time" do
    check all
    end
    end
    Generates values

    View Slide

  125. test "but good this time" do
    check all
    end
    end

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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?

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

  133. So now
    what?

    View Slide

  134. “Great artists steal”
    Ada
    Eiffel
    Haskell
    Python
    Clojure

    View Slide

  135. Links and references
    https://en.wikipedia.org/wiki/Design_by_contract
    https://www.clojure.org/about/spec
    https://www.cs.tufts.edu/~nr/cs257/archive/john-hughes/quick.pdf
    https://people.seas.harvard.edu/~chong/pubs/icfp17-whip.pdf
    https://www.youtube.com/watch?v=ROor6_NGIWU
    https://arxiv.org/abs/1607.05443

    View Slide

  136. Special Thanks!
    * Jeff Utter
    * Jason Stewart
    * Lance Halvorsen
    * Greg Mefford
    * Jeff Weiss
    * The Outlaws

    View Slide

  137. Thanks!
    Chris Keathley / @ChrisKeathley / [email protected]

    View Slide