81 lines
3.3 KiB
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)
|
|
}
|
|
}
|
|
}
|
|
}
|