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

Reasoning with Types

Reasoning with Types

Scala exhibits quite an expressive type system, and we should leverage this type system to it's full extent. In doing so, we gain the ability to statically and provably reason about our programs. In this talk we will explore various methods on how we can use types to our advantage, as well as some hoops we must jump through to not lose this advantage in the Scala language. We will also see examples of these methods in practice by delving into some Typelevel (http://typelevel.org/) projects.

Adelbert Chang

August 08, 2014
Tweet

More Decks by Adelbert Chang

Other Decks in Programming

Transcript

  1. Reasoning with Types
    Adelbert Chang
    Software Engineer @ Box Inc.

    View Slide

  2. How the next 40 minutes are going down
    Assumptions
    Parametricity
    Logic and Types
    Q & A

    View Slide

  3. Assumptions
    We will be subscribing to the thesis of functional programming

    View Slide

  4. Assumptions
    We will be subscribing to the thesis of functional programming
    Functions will be total - no throwing exceptions!

    View Slide

  5. Assumptions
    We will be subscribing to the thesis of functional programming
    Functions will be total - no throwing exceptions!
    Every input will relate to exactly one output

    View Slide

  6. Assumptions
    We will be subscribing to the thesis of functional programming
    Functions will be total - no throwing exceptions!
    Every input will relate to exactly one output
    We will require expressions to be referentially transparent - we
    can replace expressions with their value without altering the
    behavior of the program

    View Slide

  7. Parametricity
    Parametric polymorphism
    In 1989 Philip Wadler wrote a popular paper called “Theorems
    for free!”

    View Slide

  8. Parametricity
    Parametric polymorphism
    In 1989 Philip Wadler wrote a popular paper called “Theorems
    for free!”
    Types are documentation

    View Slide

  9. Parametricity
    Parametric polymorphism
    In 1989 Philip Wadler wrote a popular paper called “Theorems
    for free!”
    Types are documentation
    Docstrings erode, types are forever and compiler checked!

    View Slide

  10. Pop quiz: How many valid implementations are there for this?
    def reverse(xs: List[Int]): List[Int]

    View Slide

  11. How about now?
    def reverse[A](xs: List[A]): List[A]

    View Slide

  12. What about reducing down a collection?
    def sum(xs: List[Int]): Int =
    xs.foldLeft(0)(_ + _)

    View Slide

  13. What about reducing down a collection?
    def sum(xs: List[Int]): Int =
    xs.foldLeft(0)(_ + _)
    Some problems:

    View Slide

  14. What about reducing down a collection?
    def sum(xs: List[Int]): Int =
    xs.foldLeft(0)(_ + _)
    Some problems:
    We can still cheat with our knowledge of Int!

    View Slide

  15. What about reducing down a collection?
    def sum(xs: List[Int]): Int =
    xs.foldLeft(0)(_ + _)
    Some problems:
    We can still cheat with our knowledge of Int!
    Not generic - what if we wanted Double? Long? BigInt?

    View Slide

  16. def sum[A](xs: List[A])(implicit A: Numeric[A]): A =
    xs.foldLeft(A.zero)(A.plus)

    View Slide

  17. View Slide

  18. def sum[A](xs: List[A])(implicit A: Numeric[A]): A =
    xs.foldLeft(A.zero)(A.plus)

    View Slide

  19. def sum[A](xs: List[A])(implicit A: Numeric[A]): A =
    xs.foldLeft(A.zero)(A.plus)
    Some problems:

    View Slide

  20. def sum[A](xs: List[A])(implicit A: Numeric[A]): A =
    xs.foldLeft(A.zero)(A.plus)
    Some problems:
    Still can manifest terms of A with Numeric#fromInt!

    View Slide

  21. def sum[A](xs: List[A])(implicit A: Numeric[A]): A =
    xs.foldLeft(A.zero)(A.plus)
    Some problems:
    Still can manifest terms of A with Numeric#fromInt!
    Still not quite generic - what if we reduce List[String] ?
    List[Set[A]] ?

    View Slide

  22. def sum[A](xs: List[A])(implicit A: Numeric[A]): A =
    xs.foldLeft(A.zero)(A.plus)
    Some problems:
    Still can manifest terms of A with Numeric#fromInt!
    Still not quite generic - what if we reduce List[String] ?
    List[Set[A]] ?
    What do we actually need:

    View Slide

  23. def sum[A](xs: List[A])(implicit A: Numeric[A]): A =
    xs.foldLeft(A.zero)(A.plus)
    Some problems:
    Still can manifest terms of A with Numeric#fromInt!
    Still not quite generic - what if we reduce List[String] ?
    List[Set[A]] ?
    What do we actually need:
    A “zero” value of type A for the empty list case

    View Slide

  24. def sum[A](xs: List[A])(implicit A: Numeric[A]): A =
    xs.foldLeft(A.zero)(A.plus)
    Some problems:
    Still can manifest terms of A with Numeric#fromInt!
    Still not quite generic - what if we reduce List[String] ?
    List[Set[A]] ?
    What do we actually need:
    A “zero” value of type A for the empty list case
    A binary operation (A, A) => A to accumulate

    View Slide

  25. /** In Scalaz and Spire */
    trait Monoid[A] {
    def append(x: A, y: A): A
    def zero: A
    }
    /** Example of a Monoid[Int] */
    new Monoid[Int] {
    def append(x: Int, y: Int): Int = x + y
    def zero: Int = 0
    }

    View Slide

  26. def sum[A](xs: List[A])(implicit A: Monoid[A]): A =
    xs.foldLeft(A.zero)(A.append)

    View Slide

  27. We can go further (up the ivory tower)..
    This argument applies to functors, applicatives, monads, etc.
    def foo[A, B, C](as: List[A])
    (f: A => B)(g: B => C): List[C] = ???

    View Slide

  28. This argument applies to functors, applicatives, monads, etc.
    def foo[A, B, C](as: List[A])
    (f: A => B)(g: B => C): List[C] =
    List.empty[C]
    as.map(f andThen g)
    as.reverse.map(f andThen g)
    etc.

    View Slide

  29. This argument applies to functors, applicatives, monads, etc.
    trait Functor[F[_]] {
    def map[A, B](fa: F[A])(f: A => B): F[B]
    }
    def foo[F[_] : Functor, A, B, C]
    (fa: F[A])(f: A => B)(g: B => C): F[C]

    View Slide

  30. Parametricity sadness
    def absurd[A, B](a: A): B = ???

    View Slide

  31. def absurd[A, B](a: A): B =
    a.asInstanceOf[B]
    throw new Exception("oops")

    View Slide

  32. def absurd[A, B](a: A): B =
    a.asInstanceOf[B]
    throw new Exception("oops")
    Many more:

    View Slide

  33. def absurd[A, B](a: A): B =
    a.asInstanceOf[B]
    throw new Exception("oops")
    Many more:
    null

    View Slide

  34. def absurd[A, B](a: A): B =
    a.asInstanceOf[B]
    throw new Exception("oops")
    Many more:
    null
    isInstanceOf and asInstanceOf

    View Slide

  35. def absurd[A, B](a: A): B =
    a.asInstanceOf[B]
    throw new Exception("oops")
    Many more:
    null
    isInstanceOf and asInstanceOf
    side effects

    View Slide

  36. def absurd[A, B](a: A): B =
    a.asInstanceOf[B]
    throw new Exception("oops")
    Many more:
    null
    isInstanceOf and asInstanceOf
    side effects
    java.lang.Object methods

    View Slide

  37. def absurd[A, B](a: A): B =
    absurd(a)

    View Slide

  38. def absurd[A, B](a: A): B =
    absurd(a)
    This is the halting problem, undecideable in the presence of
    Turing completeness

    View Slide

  39. def absurd[A, B](a: A): B =
    absurd(a)
    This is the halting problem, undecideable in the presence of
    Turing completeness
    Many (theorem proving) languages give up Turing
    completeness in favor of totality checking

    View Slide

  40. A fancier example
    The idea of a “monoid” comes from the wonderful field of
    abstract algebra

    View Slide

  41. A fancier example
    The idea of a “monoid” comes from the wonderful field of
    abstract algebra
    Many more cool things:

    View Slide

  42. A fancier example
    The idea of a “monoid” comes from the wonderful field of
    abstract algebra
    Many more cool things:
    semigroup, group, semiring, ring, field, etc.

    View Slide

  43. Welcome to Spire
    trait Semigroup[A]
    trait Monoid[A] extends Semigroup[A]
    trait Group[A] extends Monoid[A]
    trait Semiring[A] extends ...
    trait Rig[A] extends Semiring[A]
    trait Rng[A] extends Semiring[A]
    trait Ring[A] extends Rig[A] with Rng[A]
    trait Field[A] extends Ring[A]

    View Slide

  44. In data mining and machine learning, dealing with vectors is
    fairly common

    View Slide

  45. In data mining and machine learning, dealing with vectors is
    fairly common
    What sort of things do we do with vectors?

    View Slide

  46. In data mining and machine learning, dealing with vectors is
    fairly common
    What sort of things do we do with vectors?
    addition, subtraction, scalar multiplication (module)

    View Slide

  47. In data mining and machine learning, dealing with vectors is
    fairly common
    What sort of things do we do with vectors?
    addition, subtraction, scalar multiplication (module)
    scalar division (vector space)

    View Slide

  48. In data mining and machine learning, dealing with vectors is
    fairly common
    What sort of things do we do with vectors?
    addition, subtraction, scalar multiplication (module)
    scalar division (vector space)
    normalization (normed vector space)

    View Slide

  49. In data mining and machine learning, dealing with vectors is
    fairly common
    What sort of things do we do with vectors?
    addition, subtraction, scalar multiplication (module)
    scalar division (vector space)
    normalization (normed vector space)
    dot product (inner product space)

    View Slide

  50. The naive way of doing things
    case class Module(repr: Vector[Double]) {
    def +(other: Module): Module
    def -(other: Module): Module
    def *:(s: Double): Module
    }

    View Slide

  51. Why specialize to Double? In fact, why specialize to
    Vector[_]?

    View Slide

  52. Why specialize to Double? In fact, why specialize to
    Vector[_]?
    We only care we can add, subtract, and multiply the “inner”
    type!

    View Slide

  53. Why specialize to Double? In fact, why specialize to
    Vector[_]?
    We only care we can add, subtract, and multiply the “inner”
    type!
    Put another way, we care it forms a Rng!
    trait Rng[A] {
    def negate(x: A): A
    def plus(x: A, y: A): A
    def times(x: A, y: A): A
    def zero: A
    }
    trait Module[V, R] {
    implicit abstract def scalar: Rng[R]
    }

    View Slide

  54. What if we want scalar division?

    View Slide

  55. What if we want scalar division?
    Rngs support negation, addition, and multiplication, but not
    division

    View Slide

  56. What if we want scalar division?
    Rngs support negation, addition, and multiplication, but not
    division
    Divison is more power - rationals are closed over division,
    integers are not!

    View Slide

  57. What if we want scalar division?
    Rngs support negation, addition, and multiplication, but not
    division
    Divison is more power - rationals are closed over division,
    integers are not!
    Abstract algebra and Spire give us Field
    trait Field[A] extends Rng[A] { ... }
    trait VectorSpace[V, R] extends Module[V, R] {
    implicit abstract def scalar: Field[R]
    }

    View Slide

  58. Logic and Types
    There is a profound relationship between proofs and
    computation

    View Slide

  59. Logic and Types
    There is a profound relationship between proofs and
    computation
    Curry-Howard correspondence/isomorphism

    View Slide

  60. Logic and Types
    There is a profound relationship between proofs and
    computation
    Curry-Howard correspondence/isomorphism
    More expressive types = more expressive proofs

    View Slide

  61. Logic and Types
    There is a profound relationship between proofs and
    computation
    Curry-Howard correspondence/isomorphism
    More expressive types = more expressive proofs
    How the correspondence works:
    Logic Computation
    proposition P type P
    P is provable P is inhabited
    proof of P term of type P
    P and Q product type (P, Q)
    P or Q sum type P | Q
    if P then Q P → Q

    View Slide

  62. Example: Get the head of a list v.1
    def head[A](as: List[A]): A =
    as match {
    case Nil => throw new Exception("oops")
    case a :: _ => a
    }

    View Slide

  63. Example: Get the head of a list v.1
    def head[A](as: List[A]): A =
    as match {
    case Nil => throw new Exception("oops")
    case a :: _ => a
    }
    Problem: head is partial!

    View Slide

  64. Example: Get the head of a list v.2
    def head[A](as: List[A]): A =
    as match {
    case Nil => null
    case a :: _ => a
    }

    View Slide

  65. Example: Get the head of a list v.2
    def head[A](as: List[A]): A =
    as match {
    case Nil => null
    case a :: _ => a
    }
    Problem: null is evil..

    View Slide

  66. The evil that is null
    Tony Hoare calls null his “billion dollar mistake”

    View Slide

  67. The evil that is null
    Tony Hoare calls null his “billion dollar mistake”
    null inhabits every type

    View Slide

  68. The evil that is null
    Tony Hoare calls null his “billion dollar mistake”
    null inhabits every type
    What this means for Curry-Howard:

    View Slide

  69. The evil that is null
    Tony Hoare calls null his “billion dollar mistake”
    null inhabits every type
    What this means for Curry-Howard:
    Any proposition can be proved, trivially

    View Slide

  70. The evil that is null
    Tony Hoare calls null his “billion dollar mistake”
    null inhabits every type
    What this means for Curry-Howard:
    Any proposition can be proved, trivially
    What this means computationally:

    View Slide

  71. The evil that is null
    Tony Hoare calls null his “billion dollar mistake”
    null inhabits every type
    What this means for Curry-Howard:
    Any proposition can be proved, trivially
    What this means computationally:
    Developers are left to their own devices on handling null =
    very brittle code

    View Slide

  72. Example: Get the head of a list v.3
    What are we trying to do?

    View Slide

  73. Example: Get the head of a list v.3
    What are we trying to do?
    Given a List[A], I may or may not be able to prove A

    View Slide

  74. Example: Get the head of a list v.3
    What are we trying to do?
    Given a List[A], I may or may not be able to prove A
    How do we deal with failure if not with exceptions and nulls?
    sealed abstract class Option[A]
    final case class None[A]() extends Option[A]
    final case class Some[A](a: A) extends Option[A]

    View Slide

  75. Example: Get the head of a list v.3
    What are we trying to do?
    Given a List[A], I may or may not be able to prove A
    How do we deal with failure if not with exceptions and nulls?
    sealed abstract class Option[A]
    final case class None[A]() extends Option[A]
    final case class Some[A](a: A) extends Option[A]
    The number of inhabitants of Option[A] is precisely the
    number of inhabitants of A + 1

    View Slide

  76. Example: Get the head of a list v.3
    What are we trying to do?
    Given a List[A], I may or may not be able to prove A
    How do we deal with failure if not with exceptions and nulls?
    sealed abstract class Option[A]
    final case class None[A]() extends Option[A]
    final case class Some[A](a: A) extends Option[A]
    The number of inhabitants of Option[A] is precisely the
    number of inhabitants of A + 1
    This “+ 1” represents our “I couldn’t prove it”

    View Slide

  77. Example: Get the head of a list v.3
    def head[A](xs: List[A]): Option[A] =
    as match {
    case Nil => None
    case a :: _ => Some(a)
    }

    View Slide

  78. Example: Get the head of a list v.4
    “But I know my list will never be empty!”

    View Slide

  79. Example: Get the head of a list v.4
    “But I know my list will never be empty!”
    Really? Curry-Howard says prove it!
    case class NonEmptyList[A](head: A, tail: List[A])
    def head[A](as: NonEmptyList[A]): A =
    as.head

    View Slide

  80. A use case for NonEmptyList
    /** Both NonEmptyList and Validation are in Scalaz */
    case class NonEmptyList[A](head: A, tail: List[A])
    type ValidationNel[E, A] = Validation[NonEmptyList[E], A]
    def groupBy[A, B](as: List[A])
    (f: A => B): Map[B, NonEmptyList[A]]

    View Slide

  81. Curry-Howard and type classes
    def sum[A](as: List[A])(implicit A: Monoid[A]): A
    Given a List[A] where A forms a monoid, I can give you
    (prove) A

    View Slide

  82. Curry-Howard and type classes
    def sum[A](as: List[A])(implicit A: Monoid[A]): A
    Given a List[A] where A forms a monoid, I can give you
    (prove) A
    How do you prove some type A forms a monoid?

    View Slide

  83. Curry-Howard and type classes
    def sum[A](as: List[A])(implicit A: Monoid[A]): A
    Given a List[A] where A forms a monoid, I can give you
    (prove) A
    How do you prove some type A forms a monoid?
    Propositions are types!
    trait Monoid[A] {
    def append(x: A, y: A): A
    def zero: A
    }

    View Slide

  84. Curry-Howard and type classes
    def sum[A](as: List[A])(implicit A: Monoid[A]): A
    Given a List[A] where A forms a monoid, I can give you
    (prove) A
    How do you prove some type A forms a monoid?
    Propositions are types!
    trait Monoid[A] {
    def append(x: A, y: A): A
    def zero: A
    }
    Prove A is a monoid by creating a term of type Monoid[A]

    View Slide

  85. Curry-Howard and type classes
    /** Proposition of a monoid */
    trait Monoid[A] {
    def append(x: A, y: A): A
    def zero: A
    }
    /** Proof Int forms a monoid */
    new Monoid[Int] {
    def append(x: Int, y: Int): Int = x + y
    def zero: Int = 0
    }

    View Slide

  86. def sum[A](as: List[A])(implicit A: Monoid[A]): A
    The implicit parameter Monoid[A] is analogous to proof search

    View Slide

  87. import scalaz.Monoid
    def sum[A](as: List[A])(implicit A: Monoid[A]): A
    sum(List(1, 2, 3))

    View Slide

  88. import scalaz.Monoid
    def sum[A](as: List[A])(implicit A: Monoid[A]): A
    sum(List(1, 2, 3))
    error: could not find implicit value for evidence parameter
    sum(List(1,2,3))
    ^
    Read: Could not find proof Int forms a Monoid (no
    Monoid[Int] in scope!)

    View Slide

  89. import scalaz.Monoid
    import scalaz.std.int._ // import proof for Int
    def sum[A](as: List[A])(implicit A: Monoid[A]): A
    sum(List(1, 2, 3))

    View Slide

  90. Following the law
    new Monoid[Int] {
    def append(x: Int, y: Int): Int = x + y
    def zero: Int = 0
    }
    Many type classes in libraries such as Scalaz and Spire are
    inspired by mathematics

    View Slide

  91. Following the law
    new Monoid[Int] {
    def append(x: Int, y: Int): Int = x + y
    def zero: Int = 0
    }
    Many type classes in libraries such as Scalaz and Spire are
    inspired by mathematics
    Mathematics is very much governed by laws

    View Slide

  92. Following the law
    new Monoid[Int] {
    def append(x: Int, y: Int): Int = x + y
    def zero: Int = 0
    }
    Many type classes in libraries such as Scalaz and Spire are
    inspired by mathematics
    Mathematics is very much governed by laws
    Said type classes should have instances which abide by these
    laws

    View Slide

  93. Following the law
    new Monoid[Int] {
    def append(x: Int, y: Int): Int = x + y
    def zero: Int = 0
    }
    Many type classes in libraries such as Scalaz and Spire are
    inspired by mathematics
    Mathematics is very much governed by laws
    Said type classes should have instances which abide by these
    laws
    Example: monoids have:

    View Slide

  94. Following the law
    new Monoid[Int] {
    def append(x: Int, y: Int): Int = x + y
    def zero: Int = 0
    }
    Many type classes in libraries such as Scalaz and Spire are
    inspired by mathematics
    Mathematics is very much governed by laws
    Said type classes should have instances which abide by these
    laws
    Example: monoids have:
    An associative binary operation

    View Slide

  95. Following the law
    new Monoid[Int] {
    def append(x: Int, y: Int): Int = x + y
    def zero: Int = 0
    }
    Many type classes in libraries such as Scalaz and Spire are
    inspired by mathematics
    Mathematics is very much governed by laws
    Said type classes should have instances which abide by these
    laws
    Example: monoids have:
    An associative binary operation
    A zero that is an identity in relation to the binary operation

    View Slide

  96. Following the law
    new Monoid[Int] {
    def append(x: Int, y: Int): Int = x + y
    def zero: Int = 0
    }
    Many type classes in libraries such as Scalaz and Spire are
    inspired by mathematics
    Mathematics is very much governed by laws
    Said type classes should have instances which abide by these
    laws
    Example: monoids have:
    An associative binary operation
    A zero that is an identity in relation to the binary operation
    This is useful because:

    View Slide

  97. Following the law
    new Monoid[Int] {
    def append(x: Int, y: Int): Int = x + y
    def zero: Int = 0
    }
    Many type classes in libraries such as Scalaz and Spire are
    inspired by mathematics
    Mathematics is very much governed by laws
    Said type classes should have instances which abide by these
    laws
    Example: monoids have:
    An associative binary operation
    A zero that is an identity in relation to the binary operation
    This is useful because:
    It further restricts the behavior of the type class (aids
    parametricity)

    View Slide

  98. Following the law
    new Monoid[Int] {
    def append(x: Int, y: Int): Int = x + y
    def zero: Int = 0
    }
    Many type classes in libraries such as Scalaz and Spire are
    inspired by mathematics
    Mathematics is very much governed by laws
    Said type classes should have instances which abide by these
    laws
    Example: monoids have:
    An associative binary operation
    A zero that is an identity in relation to the binary operation
    This is useful because:
    It further restricts the behavior of the type class (aids
    parametricity)
    Allows us to exploit these laws

    View Slide

  99. In the presence of a non-dependently typed type system, we
    cannot statically enforce laws

    View Slide

  100. In the presence of a non-dependently typed type system, we
    cannot statically enforce laws
    However, laws are perfect for property-based testing
    (ScalaCheck and Discipline)

    View Slide

  101. In the presence of a non-dependently typed type system, we
    cannot statically enforce laws
    However, laws are perfect for property-based testing
    (ScalaCheck and Discipline)
    ∀(x : Int, y : Int, z : Int), x + (y + z) = (x + y) + z

    View Slide

  102. In the presence of a non-dependently typed type system, we
    cannot statically enforce laws
    However, laws are perfect for property-based testing
    (ScalaCheck and Discipline)
    ∀(x : Int, y : Int, z : Int), x + (y + z) = (x + y) + z
    ∀(x : Int), x + 0 = 0 + x = x

    View Slide

  103. In the presence of a non-dependently typed type system, we
    cannot statically enforce laws
    However, laws are perfect for property-based testing
    (ScalaCheck and Discipline)
    ∀(x : Int, y : Int, z : Int), x + (y + z) = (x + y) + z
    ∀(x : Int), x + 0 = 0 + x = x
    Our code now becomes even more strictly specified by types
    and their properties

    View Slide

  104. Summary
    Types are robust, a form of documentation, and not just
    “something for the compiler”

    View Slide

  105. Summary
    Types are robust, a form of documentation, and not just
    “something for the compiler”
    Types hint at what a function can do, but more importantly
    tell us what it cannot do

    View Slide

  106. Summary
    Types are robust, a form of documentation, and not just
    “something for the compiler”
    Types hint at what a function can do, but more importantly
    tell us what it cannot do
    Types are a static property of your program

    View Slide

  107. Summary
    Types are robust, a form of documentation, and not just
    “something for the compiler”
    Types hint at what a function can do, but more importantly
    tell us what it cannot do
    Types are a static property of your program
    Recognize the difference:

    View Slide

  108. Summary
    Types are robust, a form of documentation, and not just
    “something for the compiler”
    Types hint at what a function can do, but more importantly
    tell us what it cannot do
    Types are a static property of your program
    Recognize the difference:
    “Trust me I’m a nice person I will/won’t do that”

    View Slide

  109. Summary
    Types are robust, a form of documentation, and not just
    “something for the compiler”
    Types hint at what a function can do, but more importantly
    tell us what it cannot do
    Types are a static property of your program
    Recognize the difference:
    “Trust me I’m a nice person I will/won’t do that”
    “It (did not) compile(d)”

    View Slide

  110. Questions?
    @adelbertchang

    View Slide