layout: true
--- background-image: url(img/fp-tower/website-background.svg) class: center, middle, white .title[Types vs Tests] --- # Julien Truffaut
.thirty-seven-left[
] .fifty-seven-right[
## Backend Scala developer ## Author of a Scala FP course: Foundations ## Maintainer of Monocle 🧐 ] --- background-image: url(img/types-vs-tests/cardinality-1.svg) # Types are set --- background-image: url(img/types-vs-tests/cardinality-2.svg) # Types are set --- background-image: url(img/types-vs-tests/cardinality-nothing-unit.svg) # Types are set --- background-image: url(img/types-vs-tests/cardinality-list-unit.svg) # Types are set --- background-image: url(img/types-vs-tests/cardinality-io-unit.svg) # Types are set --- # Types are constraints
```scala val x: Int = 5 ``` ```scala val y: Int = false // error: type mismatch; // found : Boolean(false) // required: Int ``` --- # Types are constraints
.forty-two-left[ ```scala def increment(x: Int): Int = x + 1 ``` ```tut increment(2) ``` ```scala increment("hello") // error: type mismatch; // found : String("hello") // required: Int ``` ] -- .fifty-two-right[ ```scala def increment(x: Int): Int = x.toString // error: type mismatch; // found : String // required: Int ``` ] --- # But there are limited
.forty-seven-left[ ```scala def increment(x: Int): Int = x + 1 ```
```scala def increment(x: Int): Int = x + 2 ```
```scala def increment(x: Int): Int = 0 ``` ] .forty-seven-right[.center[
## Which one ise correct? ]] --- class: center, middle # Tests --- class: center, middle # Unit Tests --- # Unit tests are constraints .forty-two-left[
```scala "increment at -3" in { assert(increment(-3) == -2) } "increment at 0" in { assert(increment(0) == 1) } "increment at 1" in { assert(increment(1) == 2) } ``` ] .fifty-two-right[.center[
]] --- # Unit tests are constraints .forty-two-left[
```scala "increment at -3" in { assert(increment(-3) == -2) } "increment at 0" in { assert(increment(0) == 1) } "increment at 1" in { assert(increment(1) == 2) } ``` ] .fifty-two-right[.center[
]] --- # But they are limited .forty-two-left[
```scala "increment at -3" in { assert(increment(-3) == -2) } "increment at 0" in { assert(increment(0) == 1) } "increment at 1" in { assert(increment(1) == 2) } ``` ] .fifty-two-right[.center[
]] --- class: center, middle # Both types and tests constrain the implementation of a function --- class: center, middle # How much? --- # A => B is a type
.forty-seven-left[ ```scala val isEven: Int => Boolean = x => x % 2 == 0 val increment: Int => Int = x => x + 1 ``` ] --- class: center, middle # Type is a set # && # A => B is a type --- background-image: url(img/types-vs-tests/function-set-1.svg) # A function is a set --- background-image: url(img/types-vs-tests/function-set-2.svg) # A function is a set --- background-image: url(img/types-vs-tests/function-set-3.svg) # Unit tests --- class: center, middle # Valid Implementation Count (VIC) --- class: middle .forty-two-left[.center[
# VIC(f: A => B) = ]] .fifty-seven-left[.center[ # number of possible implementations
-
implementations made invalid because of tests ]] --- background-image: url(img/types-vs-tests/vic.svg) --- class: center, middle # The smaller VIC is, the more constrained is f --- background-image: url(img/types-vs-tests/vic-of-1.svg) # VIC(f) = 1 --- background-image: url(img/types-vs-tests/vic-1.svg) --- background-image: url(img/types-vs-tests/vic-2.svg) --- background-image: url(img/types-vs-tests/function-cardinality-1.svg) # Function is a mapping --- # |A => B| .thirty-seven-left[
```scala |{x} => B| = 5 ``` ] .sixty-two-right[.center[
]] --- # |A => B| .thirty-seven-left[
```scala |{x} => B| = |B| ``` ] .sixty-two-right[.center[
]] --- # |A => B| .thirty-seven-left[
```scala |{x, y} => B| = ??? ``` ] .sixty-two-right[.center[
]] --- # |A => B| .thirty-seven-left[
```scala |{x, y} => B| = |{x} => B| + // y -> a |{x} => B| + // y -> b |{x} => B| + // y -> c |{x} => B| + // y -> d |{x} => B| + // y -> e ``` ] .sixty-two-right[.center[
]] --- # |A => B| .thirty-seven-left[
```scala |{x, y} => B| = 5 * |{x} => B| ``` ] .sixty-two-right[.center[
]] --- # |A => B| .thirty-seven-left[
```scala |{x, y} => B| = 5 * |B| ``` ] .sixty-two-right[.center[
]] --- # |A => B| .thirty-seven-left[
```scala |{x, y} => B| = |B| * |B| ``` ] .sixty-two-right[.center[
]] --- # |A => B| .thirty-seven-left[
```scala |{x, y} => B| = |B| ^ 2 ``` ] .sixty-two-right[.center[
]] --- # |A => B| .thirty-seven-left[
```scala |{x, y} => B| = |B| ^ |{x, y}| ``` ] .sixty-two-right[.center[
]] --- # |A => B| .thirty-seven-left[
```scala |A => B| = |B| ^ |A| ``` ] .sixty-two-right[.center[
]] --- # Curry–Howard correspondence
.center[ | Type | Algebra | Logic | |:-----------:| :--------:|:-----:| |Nothing | 0 | ⊥ | |Unit | 1 | ⊤ | |Either[A, B] | A + B | A ∨ B | |(A, B) | A * B | A ∧ B | | A => B | B ^ A | A → B | | Isomorphism | A == B | A ⇔ B | ] --- class: center, middle ```scala |increment: Int => Int| = ? ``` --- class: center, middle ```scala |Int => Int| = |Int| ^ |Int| = (2^32) ^ (2^32) ``` --- class: center, middle ```scala |Int => Int| = |Int| ^ |Int| = (2^32) ^ (2^32) =~ 41 billion digit long number ``` --- # Small Types
```scala enum DayOfWeek { case Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday } ```
```scala |DayOfWeek => Boolean| = |Boolean| ^ |DayOfWeek| = 2 ^ 7 = 128 ``` --- class: middle .forty-two-left[.center[
# VIC(f: A => B) = ]] .fifty-seven-left[.center[ # number of possible implementations
-
.hl[implementations made invalid because of tests] ]] --- background-image: url(img/types-vs-tests/unit-test-impact-1.svg) # How much a single unit test impacts VIC? --- # Unit test impact on VIC
```scala def someFunction(day: DayOfWeek): Boolean = ??? ``` ## such as ```scala assert(someFunction(Wednesday) == false) ``` --- # Unit test impact on VIC .forty-two-left[
```scala VIC(someFunction and 1 unit test) = ... ``` ] .fifty-seven-right[.center[
]] --- # Unit test impact on VIC .forty-two-left[
```scala VIC(someFunction and 1 unit test) = 2 * ... ``` ] .fifty-seven-right[.center[
]] --- # Unit test impact on VIC .forty-two-left[
```scala VIC(someFunction and 1 unit test) = 2 * 2 * ... ``` ] .fifty-seven-right[.center[
]] --- # Unit test impact on VIC .forty-two-left[
```scala VIC(someFunction and 1 unit test) = 2 * 2 * 1 * ... ```
```scala assert( someFunction(Wednesday) == false ) ``` ] .fifty-seven-right[.center[
]] --- # Unit test impact on VIC .forty-two-left[
```scala VIC(someFunction and 1 unit test) = 2 * 2 * 1 * 2 * ... ``` ] .fifty-seven-right[.center[
]] --- # Unit Test .center[ | # Unit tests | VIC | | :--------: |:----:| | 0 | 128 | | 1 | 64 | | 2 | 32 | | 3 | 16 | | 4 | 8 | | 5 | 4 | | 6 | 2 | | 7 | 1 | ] --- class: center, middle # |Input| unit tests are required to have VIC = 1 --- class: center, middle # Every unit test divides VIC by |Output| ---
# One unit test .center[ ```scala VIC(f: A => B) = |B| ^ |A| / |B| = |B| ^ (|A| - 1) ``` ] -- # Two unit tests .center[ ```scala VIC(f: A => B) = |B| ^ |A| / |B| / |B| = (|B| ^ |A|) / (|B| ^ 2) = |B| ^ (|A| - 2) ``` ] --- class: center, middle # `VIC(f: A => B) = |B| ^ (|A| - n)` ### where `n` is the number of unit tests --- class: center, middle ```scala VIC(increment) = |B| ^ (|A| - n) = |Int| ^ (|Int| - n) = (2 ^ 32) ^ (2 ^ 32 - n) ``` --- # Exponential sucks .center[
] --- class: center, middle # Logarithmic Valid Implementation Count (LVIC) --- class: center, middle ```scala LVIC(increment) = Log_2 (|B| ^ |A|) = (|A| - n) * Log_2 |B| = (2 ^ 32 - n) * 32 = 2 ^ 37 - 32 * n =~ 137 billions - 32 * n ``` --- # Example
```scala def getDialCode(country: String): Int = ??? ``` ## such as ```scala assert(getDialCode("United Kingdom") == 44) assert(getDialCode("France") == 33) ``` --- # Example
```scala def getDialCode(country: String): Option[Int] = ??? ``` ## such as ```scala assert(getDialCode("United Kingdom") == Some(44)) assert(getDialCode("France") == Some(33)) assert(getDialCode("foo") == None) ``` --- # Example
```scala def getDialCode(country: String): Option[Int] = ??? ``` ```scala LVIC(getDialCode) = (|A| - n) * log_2 |B| = (|String| - 3) * log_2 |Option[Int]| ``` --- # Example
```scala def getDialCode(country: String): Option[Int] = ??? ``` ```scala LVIC(getDialCode) = (|A| - n) * log_2 |B| = (|String| - 3) * log_2 |Option[Int]| = (|String| - 3) * log_2 (|Int| + 1) = (|String| - 3) * log_2 (2 ^ 32 + 1) =~ |String| * 32 ``` -- .center[ ## Unit tests have a negligible impact on VIC ] --- # More precise types
```scala enum Country { // |Country| = 50 case UnitedKingdom, France, Italy, Belgium, ... China } ``` -- .forty-seven-left[ ```scala def getDialCode(country: Country): Int = ??? assert(getDialCode(UnitedKingdom) == 44) assert(getDialCode(France) == 33) ``` ] -- .forty-seven-right[ ```scala def parseCountry(x: String): Option[Country] = ??? assert( parseCountry("France") == Some(France) ) assert( parseCountry("foo") == None ) ``` ] --- # More precise types ```scala def getDialCode(country: Country): Int = ??? ``` ```scala LVIC(getDialCode) = (|Country| - 2) * log_2 |Int| ``` --- # More precise types ```scala def getDialCode(country: Country): Int = ??? ``` ```scala LVIC(getDialCode) = (|Country| - 2) * log_2 |Int| = (50 - 2) * log_2 2 ^ 32 = 48 * 32 = 1536 ``` -- ```scala def parseCountry(x: String): Option[Country] = ??? ``` -- ```scala LVIC(parseCountry) = (|String| - n) * log_2 |Option[Country]| = (|String| - n) * log_2 (|Country| + 1) = (|String| - n) * log_2 51 =~ |String| * 6 ``` -- ### Using Country reduced LVIC by a factor of 5! --- background-image: url(img/types-vs-tests/getDialCode-1.svg) --- background-image: url(img/types-vs-tests/getDialCode-2.svg) --- background-image: url(img/types-vs-tests/json-json.svg) --- # Conclusions
## 1. VIC combines both types and unit tests in a single metric -- ## 2. It can be automatically calculated -- ## 3. Reach unimaginable numbers except for toy examples -- ## 4. Use small enum instead of Int or String when possible -- ## 5. If we only have basic types and unit tests, we cannot hope to write correct programs --- class: center, middle # Advanced tests --- # Property Based Tests
```scala def isEven(x: Int): Boolean = x % 2 == 0 ``` -- ```scala class IsEvenTest extends AnyFunSuite with ScalaCheckDrivenPropertyChecks { test("2 * x")( forAll((x: Int) => assert(isEven(2 * x) == true) )) } ``` -- ### VIC with this property ```scala VIC(isEven) = |Boolean| ^ (|Int| / 2) = 2 ^ (2 ^ 32 / 2) = 2 ^ (2 ^ 31) // instead of 2 ^ (2 ^ 32) ``` --- # Property Based Tests
```scala def increment(x: Int): Int = x + 1 ```
```scala class IncrementTest extends AnyFunSuite with ScalaCheckDrivenPropertyChecks { test("increment property") ( forAll{ (x: Int) => ??? } ) } ``` --- background-image: url(img/increment-mapping-2.jpg) # Increment Property ? --- background-image: url(img/increment-mapping-3.jpg) # Injective --- class: center, middle ## `increment(x) == increment(y)` ## implies ## `x == y` --- # Property Based Tests
```scala class IncrementTest extends AnyFunSuite with ScalaCheckDrivenPropertyChecks { test("increment is injective (one-to-one)") { forAll((x: Int, y: Int) => assert(increment(x) != increment(y) || x == y) ) } } ``` -- ### VIC with injective property ```scala VIC(increment) = (2 ^ 32) * (2 ^ 32 - 1) * (2 ^ 32 - 2) * ... * 1 = (2 ^ 32)! // instead of (2 ^ 32) ^ (2 ^ 32) ``` -- ### It is equivalent to 125 million unit tests --- background-image: url(img/increment-mapping-2.jpg) # Increment Property ? --- # Property Based Tests
```scala class IncrementTest extends AnyFunSuite with ScalaCheckDrivenPropertyChecks { test("∀ x - MaxInt, f(x) > x") { forAll((x: Int) => assert(increment(x) > x || x == Int.MaxValue) ) } } ``` -- ### VIC with greater than property ```scala VIC(increment) = (2 ^ 32 - 1) * (2 ^ 32 - 2) * ... * 1 * 2 ^ 32 = (2 ^ 32)! ``` --- # Combine Properties ```scala class IncrementTest extends AnyFunSuite with ScalaCheckDrivenPropertyChecks { test("increment is injective (one-to-one)") { forAll((x: Int, y: Int) => assert(increment(x) != increment(y) || x == y) ) } test("∀ x - MaxInt, f(x) > x") { forAll((x: Int) => assert(increment(x) > x || x == Int.MaxValue) ) } } ``` --- background-image: url(img/combine-properties-1.jpg) # Combine Properties --- background-image: url(img/combine-properties-2.jpg) # Combine Properties --- background-image: url(img/combine-properties-3.jpg) # Combine Properties --- background-image: url(img/combine-properties-4.jpg) # Combine Properties --- background-image: url(img/combine-properties-5.jpg) # Combine Properties --- background-image: url(img/combine-properties-6.jpg) # Combine Properties --- background-image: url(img/combine-properties-7.jpg) # Combine Properties --- background-image: url(img/combine-properties-8.jpg) # Combine Properties --- background-image: url(img/combine-properties-9.jpg) # Combine Properties --- class: center, middle # VIC(increment) = 1 ### with injective and f(x) > x properties --- class: center, middle # Property based testing is very effective but it is difficult to measure impact on VIC --- class: center, middle # Advanced types --- # Parametric Polymorphism
```scala def identityInt(x: Int): Int = x ``` ```scala |identityInt| = (2 ^ 32) ^ (2 ^ 32) ``` --
```scala def identity[A](x: A): A = x ``` ```scala |identity| = 1 ``` --- # Parametric Polymorphism
```scala def mapInt(fa: Option[Int], f: Int => Int): Option[Int] = ??? ``` -- ```scala |mapInt| = |Option[Int]| ^ |(Option[Int], Int => Int)| =~ |Int| ^ |Int => Int| =~ 2 ^ 32 ^ ((2 ^ 32) ^ (2 ^ 32)) ``` --
```scala def map[A, B](fa: Option[A], f: A => B): Option[B] = ??? ``` --- # Parametric Polymorphism
```scala def map[A, B](fa: Option[A], f: A => B): Option[B] = fa match { case None => None case Some(x) => Some(f(x)) } ``` ```scala def map[A, B](fa: Option[A], f: A => B): Option[B] = fa match { case None => None case Some(x) => None } ``` -- .forty-two-left[ ```scala |map| = 2 ``` ] .fifty-two-right[ ### Yoneda lemma can calculate `|map|` automatically ] --- # Adhoc Polymorphism
```scala def voidList(fa: List[Int]): List[Unit] = ??? ```
```scala |voidList| = |List[Unit]| ^ |List[Int]| = ∞ ``` --- # Adhoc Polymorphism
```scala def voidList2[A](fa: List[A]): List[Unit] = ??? ``` -- ```scala def voidList2[A](fa: List[A]): List[Unit] = Nil def voidList2[A](fa: List[A]): List[Unit] = List(()) def voidList2[A](fa: List[A]): List[Unit] = List((), ()) def voidList2[A](fa: List[A]): List[Unit] = List((), (), ()) def voidList2[A](fa: List[A]): List[Unit] = fa.map(_ => ()).take(5) ``` --
```scala |voidList2| > |List[Unit]| = ∞ ``` --- # Adhoc Polymorphism
```scala import cats.Functor import cats.syntax.functor._ def void[F[_]: Functor, A](fa: F[A]): F[Unit] = fa.map(_ => ()) ``` --
```scala |void| = 1 ``` --- class: center, middle # The more polymoprhic a function is, the more precise it is --- # More Advanced Types
.middle[ ## GADTS / Type equalities ## Dependent Types ## Linear Types ] --- class: middle, center background-image: url(img/fp-tower/website-background.svg) # .white[Thanks!] .white[Slides available on GitHub at `fp-tower/types-vs-tests`] --- background-image: url(img/types-4.jpg) --- # Basic Types > Unit Tests .center.middle[
# `VIC(Any => Any) = |Any| ^ (|Any| - n)` ### where `n` is the number of unit tests ]