embracing-nondeterminism-code/src/test/scala/green/thisfieldwas/embracingnondeterminism/control/MonadLaws.scala

81 lines
3.3 KiB
Scala

package green.thisfieldwas.embracingnondeterminism.control
import green.thisfieldwas.embracingnondeterminism.util._
import org.scalacheck.Arbitrary
import org.scalacheck.Arbitrary.arbitrary
import scala.reflect.runtime.universe.TypeTag
/** Include this trait to check that your context `F[_]` conforms to the Monad
* laws. Provide an implicit instance of `LiftedGen` for your context `F[_]`
* and call the `checkMonadLaws[F]()` function within your laws spec to
* register the associated properties.
*/
trait MonadLaws { this: Laws with ApplicativeLaws =>
import green.thisfieldwas.embracingnondeterminism.syntax.applicative._
import green.thisfieldwas.embracingnondeterminism.syntax.monad._
implicit def arbitraryFA[F[_]: LiftedGen, A: Arbitrary]: Arbitrary[F[A]] = Arbitrary(arbitrary[A].lift)
/** Defined per Monad laws taken from the Haskell wiki:
* [[https://wiki.haskell.org/Monad_laws#The_three_laws]]
*
* These laws extend the Applicative laws, so `checkApplicativeLaws[F]()`
* should be executed alongside this function.
*
* The laws as defined here leverage Kleisli composition, which is defined
* using the operator `>=>` in terms of `flatMap()`, to better highlight the
* left and right identities and associativity that should be exhibited by
* the composition of monadic operations.
*
* @param TT
* The type tag of the context
* @tparam F
* The context type being tested
*/
def checkMonadLaws[F[_]: Monad: LiftedGen]()(implicit TT: TypeTag[F[Any]]): Unit = {
property(s"${TT.name} Monad composition preserves left identity") {
// The identity of Kleisli composition simply lifts a value into the
// context. Kleisli composition of a function after `pure()` when applied
// to a value will produce the same result as applying the function
// directly to the value.
forAll(for {
a <- arbitrary[Int]
h <- arbitrary[Int => F[String]]
} yield (a, h)) { case (a, h) =>
val leftIdentity = ((_: Int).pure[F]) >=> h
leftIdentity(a) mustBe h(a)
}
}
property(s"${TT.name} Monad composition preserves right identity") {
// The identity of Kleisli composition simply lifts a value into the
// context. Kleisli composition of `pure()` after a function when applied
// to a value will produce the same result as applying the function
// directly to the value.
forAll(for {
a <- arbitrary[Int]
h <- arbitrary[Int => F[String]]
} yield (a, h)) { case (a, h) =>
val rightIdentity = h >=> ((_: String).pure[F])
rightIdentity(a) mustBe h(a)
}
}
property(s"${TT.name} Monad composition is associative") {
// `flatMap()` and thus Kleisli composition are both associative.
// This means that your program may be factored with these operations
// in any arbitrary grouping and the output will be the same.
forAll(for {
a <- arbitrary[Double]
f <- arbitrary[Double => F[String]]
g <- arbitrary[String => F[Int]]
h <- arbitrary[Int => F[Boolean]]
} yield (a, f, g, h)) { case (a, f, g, h) =>
val assocLeft = (f >=> g) >=> h
val assocRight = f >=> (g >=> h)
assocLeft(a) mustBe assocRight(a)
}
}
}
}