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.

Fb8e986500c5059b2a6c0b2184bb0faf?s=128

Adelbert Chang

August 08, 2014
Tweet

Transcript

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

  2. How the next 40 minutes are going down Assumptions Parametricity

    Logic and Types Q & A
  3. Assumptions We will be subscribing to the thesis of functional

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

    programming Functions will be total - no throwing exceptions!
  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
  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
  7. Parametricity Parametric polymorphism In 1989 Philip Wadler wrote a popular

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

    paper called “Theorems for free!” Types are documentation
  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!
  10. Pop quiz: How many valid implementations are there for this?

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

  12. What about reducing down a collection? def sum(xs: List[Int]): Int

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

    = xs.foldLeft(0)(_ + _) Some problems:
  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!
  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?
  16. def sum[A](xs: List[A])(implicit A: Numeric[A]): A = xs.foldLeft(A.zero)(A.plus)

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

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

  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!
  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]] ?
  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:
  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
  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
  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 }
  26. def sum[A](xs: List[A])(implicit A: Monoid[A]): A = xs.foldLeft(A.zero)(A.append)

  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] = ???
  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.
  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]
  30. Parametricity sadness def absurd[A, B](a: A): B = ???

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

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

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

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

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

    Many more: null isInstanceOf and asInstanceOf side effects
  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
  37. def absurd[A, B](a: A): B = absurd(a)

  38. def absurd[A, B](a: A): B = absurd(a) This is the

    halting problem, undecideable in the presence of Turing completeness
  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
  40. A fancier example The idea of a “monoid” comes from

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

    the wonderful field of abstract algebra Many more cool things:
  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.
  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]
  44. In data mining and machine learning, dealing with vectors is

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

    fairly common What sort of things do we do with vectors?
  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)
  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)
  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)
  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)
  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 }
  51. Why specialize to Double? In fact, why specialize to Vector[_]?

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

    We only care we can add, subtract, and multiply the “inner” type!
  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] }
  54. What if we want scalar division?

  55. What if we want scalar division? Rngs support negation, addition,

    and multiplication, but not division
  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!
  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] }
  58. Logic and Types There is a profound relationship between proofs

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

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

    and computation Curry-Howard correspondence/isomorphism More expressive types = more expressive proofs
  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
  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 }
  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!
  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 }
  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..
  66. The evil that is null Tony Hoare calls null his

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

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

    “billion dollar mistake” null inhabits every type What this means for Curry-Howard:
  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
  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:
  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
  72. Example: Get the head of a list v.3 What are

    we trying to do?
  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
  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]
  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
  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”
  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) }
  78. Example: Get the head of a list v.4 “But I

    know my list will never be empty!”
  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
  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]]
  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
  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?
  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 }
  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]
  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 }
  86. def sum[A](as: List[A])(implicit A: Monoid[A]): A The implicit parameter Monoid[A]

    is analogous to proof search
  87. import scalaz.Monoid def sum[A](as: List[A])(implicit A: Monoid[A]): A sum(List(1, 2,

    3))
  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!)
  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))
  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
  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
  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
  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:
  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
  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
  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:
  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)
  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
  99. In the presence of a non-dependently typed type system, we

    cannot statically enforce laws
  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)
  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
  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
  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
  104. Summary Types are robust, a form of documentation, and not

    just “something for the compiler”
  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
  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
  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:
  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”
  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)”
  110. Questions? @adelbertchang