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

Testing for Lawful Good Adventurers

Testing for Lawful Good Adventurers

LambdaConf 2018, Boulder CO USA.
Laws and law testing.

Chris Phelps

June 04, 2018
Tweet

More Decks by Chris Phelps

Other Decks in Technology

Transcript

  1. Copyright 2018 Tendril, Inc. All rights reserved. Monads Are... INTRO

    • Burritos • Elephants • Lannisters • Pipes • Monoids in the category of Endofunctors 2
  2. Copyright 2018 Tendril, Inc. All rights reserved. Monads are classes

    with well-behaved apply and flatMap methods
  3. Copyright 2018 Tendril, Inc. All rights reserved. “Well-behaved” Monad Methods

    INTRO • Signatures • Behaviors o Left identity o Build with apply then flatMap a function is the same as calling the function o Right identity o FlatMap the apply doesn’t change the calling instance o Associativity o Nesting of a chain of functions does not matter
  4. Copyright 2018 Tendril, Inc. All rights reserved. Overview INTRO •

    Benefits of Laws • Typeclass Pattern in Scala • Property Testing • Law Testing o Scalaz o Cats 6
  5. Copyright 2018 Tendril, Inc. All rights reserved. What are laws

    BENEFITS • Specify behavior • Interaction of methods • Equivalence of operations • Consistency with algorithms • Constraints and assumptions 8
  6. Copyright 2018 Tendril, Inc. All rights reserved. Laws as constraints

    BENEFITS • Instance implementers o Constrain possible implementations o Disallow incoherent approaches o Confidence that algorithms can use • Algorithm implementers o Provide safe assumptions o Allow expectations about behavior o Instance will behave 9
  7. Copyright 2018 Tendril, Inc. All rights reserved. Typeclass Law Assumption

    Monoid Left and Right Identity zero does not change the value Functor Identity map(apply) does not change value Functor Composition safe to rewrite chains as composition Monad Left Identity flatMap uses the function result Monad Right Identity apply creates consistent context Monad Coherence map and flatMap are consistent Assumptions BENEFITS 10
  8. Copyright 2018 Tendril, Inc. All rights reserved. What are Typeclasses

    Anyway? TYPECLASSES IN SCALA • External implementation of an interface o No need to extend directly o Possible without access to class source • Introduce via a context bound or implicit parameter • Implement in terms of of other instances o Adder[Pair[Int]] defined in terms of Adder[Int] 12
  9. Copyright 2018 Tendril, Inc. All rights reserved. How To Build

    a Typeclass TYPECLASSES IN SCALA • Define an interface • Implement for your class • Introduce to implicit scope • Pass to function as implicit parameter OR as type constraint • Call methods from the interface 13
  10. Copyright 2018 Tendril, Inc. All rights reserved. Typeclass Example TYPECLASSES

    IN SCALA trait Adder[A] { def add(x: A, y: A): A } implicit val intAdder = new Adder[Int] { override def add(x: Int, y: Int): Int = x + y } def addThings[T: Adder](x: T, y: T): T = { implicitly[Adder[T]].add(x, y) } addThings(5, 7) 14
  11. Copyright 2018 Tendril, Inc. All rights reserved. Inductive Typeclass Definition

    TYPECLASSES IN SCALA case class Pair[T](fst: T, snd: T) implicit def pairAdder[T: Adder] = new Adder[Pair[T] { def add(x: Pair[T], y: Pair[T]): Pair[T] = { val ta = implicitly[Adder[T]] Pair(ta.add(x.fst, y.fst), ta.add(x.snd, y.snd)) } } 15
  12. Copyright 2018 Tendril, Inc. All rights reserved. Property testing PROPERTY

    TESTING • Properties hold for a variety of inputs • Random* data generated for inputs o “Biased” random • Multiple checks • Test case minimization 17
  13. Copyright 2018 Tendril, Inc. All rights reserved. Generators PROPERTY TESTING

    val vowel = Gen.oneOf('A', 'E', 'I', 'O', 'U', 'Y') val smallEvenInteger = Gen.choose(0,200) suchThat (_ % 2 == 0) val myGen = for { n <- Gen.choose(10,20) m <- Gen.choose(2*n, 500) } yield (n,m) 18
  14. Copyright 2018 Tendril, Inc. All rights reserved. Arbitrary PROPERTY TESTING

    implicit lazy val arbBool: Arbitrary[Boolean] = Arbitrary(oneOf(true, false)) implicit def arbMyOption[T](implicit a: Arbitrary[T]): Arbitrary[MyOption[T]] = Arbitrary( Gen.oneOf( arbitrary[T].map(v => MySome(v)), Gen.const(MyNone) ) ) 19
  15. Copyright 2018 Tendril, Inc. All rights reserved. Scalaz Law Testing

    LAW TESTING • Scalacheck • Laws specified with the abstraction o Monoid.MonoidLaw trait • Binding with scalacheck o scalaz-scalacheck-binding 22 trait MonoidLaw extends SemigroupLaw { def leftIdentity(a: F)(implicit F: Equal[F]): Boolean = F.equal(a, append(zero, a)) def rightIdentity(a: F)(implicit F: Equal[F]): Boolean = F.equal(a, append(a, zero)) }
  16. Copyright 2018 Tendril, Inc. All rights reserved. Scalaz Example LAW

    TESTING case class Pair[T](fst: T, snd: T) implicit val pairMonoid = new Monoid[Pair[Int]] { def zero: Pair[Int] = { val amonoid = Monoid[Int] Pair(amonoid.zero, amonoid.zero) } def append(f1: Pair[Int], f2: => Pair[Int]): Pair[Int] = { val amonoid = Monoid[Int] val newfst = amonoid.append(f1.fst, f2.fst) val newsnd = amonoid.append(f1.snd, f2.snd) Pair(newfst, newsnd) } } 23
  17. Copyright 2018 Tendril, Inc. All rights reserved. Equal and Arbitrary

    LAW TESTING implicit val pairIntEq = new Equal[Pair[Int]] { def equal(a1: Pair[Int], a2: Pair[Int]): Boolean = { a1.fst == a2.fst && a1.snd == a2.snd } } implicit def arbPairInt: Arbitrary[Pair[Int]] = Arbitrary{ for { n <- Gen.choose(10,20) m <- Gen.choose(2*n, 500) } yield Pair(n,m) } 24
  18. Copyright 2018 Tendril, Inc. All rights reserved. Running the test

    LAW TESTING class PairMonoidSpec extends FlatSpec with Matchers // From scalaz.SpecLite def checkAll(props: Properties): Unit = ??? it should "test laws" in { checkAll(monoid.laws[Pair[Int]]) } 25
  19. Copyright 2018 Tendril, Inc. All rights reserved. Running the test

    LAW TESTING class PairMonoidSpec extends FlatSpec with Matchers // From scalaz.SpecLite def checkAll(props: Properties): Unit = ??? it should "test laws" in { checkAll(monoid.laws[Pair[Int]]) } 26
  20. Copyright 2018 Tendril, Inc. All rights reserved. Running the test

    LAW TESTING class PairMonoidSpec extends FlatSpec with Matchers // From scalaz.SpecLite def checkAll(props: Properties): Unit = ??? it should "test laws" in { checkAll(monoid.laws[Pair[Int]]) } 27
  21. Copyright 2018 Tendril, Inc. All rights reserved. Discipline LAW TESTING

    • Typelevel project • Builds on Scalacheck • Integrates with Scalatest or Specs2 • Base trait for laws: RuleSet • Compute unique properties to check 29
  22. Copyright 2018 Tendril, Inc. All rights reserved. Cats Law Suites

    LAW TESTING package cats.laws trait MonoidKLaws[F[_]] extends SemigroupKLaws[F] { override implicit def F: MonoidK[F] def monoidKLeftIdentity[A](a: F[A]): IsEq[F[A]] = F.combineK(F.empty, a) <-> a def monoidKRightIdentity[A](a: F[A]): IsEq[F[A]] = F.combineK(a, F.empty) <-> a } 30
  23. Copyright 2018 Tendril, Inc. All rights reserved. Cats Discipline Machinery

    LAW TESTING package cats.laws.discipline trait MonadTests[F[_]] extends ApplicativeTests[F] with FlatMapTests[F] def monad[A: Arbitrary: Eq, B: Arbitrary: Eq, C: Arbitrary: Eq] (implicit ...): RuleSet 31
  24. Copyright 2018 Tendril, Inc. All rights reserved. Your Own Implementation

    LAW TESTING sealed trait MyOption[+A] case object MyNone extends MyOption[Nothing] final case class MySome[A](value: A) extends MyOption[A] implicit val myOptionMonad = new Monad[MyOption] { def pure[A](value: A): MyOption[A] = ??? def flatMap[A, B](opt: MyOption[A]) (fn: A => MyOption[B]): MyOption[B] = ??? def tailRecM[A, B](a: A) (f: A => MyOption[Either[A, B]]): MyOption[B] = ??? } 32
  25. Copyright 2018 Tendril, Inc. All rights reserved. Equal and Arbitrary

    LAW TESTING implicit def arbMyOption[T](implicit a: Arbitrary[T]): Arbitrary[MyOption[T]] = Arbitrary( Gen.oneOf( arbitrary[T].map(v => MySome(v)), Gen.const(MyNone) ) ) implicit def eqMyOption[T](implicit t: Eq[T]): Eq[MyOption[T]] = new Eq[MyOption[T]] { def eqv(x: MyOption[T], y: MyOption[T]): Boolean = ??? } 33
  26. Copyright 2018 Tendril, Inc. All rights reserved. Running the Test

    LAW TESTING class MyOptionSpec extends FunSuite with Matchers with Discipline checkAll("MyOption[Int]", MonadTests[MyOption].monad[Int, Int, Int]) 34
  27. Copyright 2018 Tendril, Inc. All rights reserved. Running the Test

    LAW TESTING class MyOptionSpec extends FunSuite with Matchers with Discipline checkAll("MyOption[Int]", MonadTests[MyOption].monad[Int, Int, Int]) 35
  28. Copyright 2018 Tendril, Inc. All rights reserved. Running the Test

    LAW TESTING class MyOptionSpec extends FunSuite with Matchers with Discipline checkAll("MyOption[Int]", MonadTests[MyOption].monad[Int, Int, Int]) 36
  29. Copyright 2018 Tendril, Inc. All rights reserved. Takeaways TAKEAWAYS •

    Laws separate good behavior from domain logic • Testing lawfulness ensures your implementations will behave properly • Property testing provides a mechanism for testing lawfulness • Cats and scalaz include a suite of laws for their typeclasses 38