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

82 lines
3.5 KiB
Scala

package green.thisfieldwas.embracingnondeterminism.control
import green.thisfieldwas.embracingnondeterminism.data.FunctorLaws
import green.thisfieldwas.embracingnondeterminism.util._
import org.scalacheck.Arbitrary.arbitrary
import scala.reflect.runtime.universe.TypeTag
/** Include this trait to check that your context `F[_]` conforms to the
* Applicative laws. Provide an implicit instance of `LiftedGen` for your
* context `F[_]` and call the `checkApplicativeLaws[F]()` function within your
* laws spec to register the associated properties.
*/
trait ApplicativeLaws { this: Laws with FunctorLaws =>
import green.thisfieldwas.embracingnondeterminism.syntax.applicative._
/** Defined per Applicative laws taken from the Haskell wiki:
* [[https://en.wikibooks.org/wiki/Haskell/Applicative_functors#Applicative_functor_laws]]
*
* These laws extend the Functor laws, so `checkFunctorLaws[F]()` should be
* executed alongside this function.
*
* @param TT
* The type tag of the context
* @tparam F
* The context type being tested
*/
def checkApplicativeLaws[F[_]: Applicative: LiftedGen]()(implicit TT: TypeTag[F[Any]]): Unit = {
property(s"${TT.name} Applicative preserves identity functions") {
// A lifted identity function applied to a lifted argument is the same as
// the identity function applied directly to the lifted argument.
forAll(arbitrary[Double].lift) { v =>
// Haskell: pure id <*> v = v
(identity[Double] _).pure.ap(v) mustBe identity(v)
}
}
property(s"${TT.name} Applicative preserves function homomorphism") {
// Lifting a function and an argument then applying them produces the
// same result as applying the unlifted function and unlifted argument
// then lifting the result.
forAll(for {
f <- arbitrary[Double => String]
x <- arbitrary[Double]
} yield (f, x)) { case (f, x) =>
// Haskell: pure f <*> pure x = pure (f x)
f.pure.ap(x.pure) mustBe f(x).pure
}
}
property(s"${TT.name} Applicative preserves function interchange") {
// Given a lifted function and an unlifted argument, applying the lifted
// function after lifting the argument should give the same result as
// when reversing the order of the function and argument. This is
// difficult to express in words, and the code is hard to follow, but
// roughly this translates to
// `ap(ff: F[A => B])(pure(a)) == ap(pure(f => f(a)))(ff)`.
forAll(for {
u <- arbitrary[Double => String].lift
y <- arbitrary[Double]
} yield (u, y)) { case (u, y) =>
// Haskell: u <*> pure y = pure ($ y) <*> u
u.ap(y.pure) mustBe ((f: Double => String) => f(y)).pure.ap(u)
}
}
property(s"${TT.name} Applicative preserves function composition") {
// Given lifted functions `ff: F[A => B]` and `fg: F[B => C]` and
// argument `fa: F[A]`: lifting `compose()` and applying `fg`, `ff`, and
// `fa` produces the same result as applying `fg` after applying `ff` to
// `fa`.
val compose: (String => Int) => (Double => String) => Double => Int = g => f => g compose f
forAll(for {
u <- arbitrary[String => Int].lift
v <- arbitrary[Double => String].lift
w <- arbitrary[Double].lift
} yield (u, v, w)) { case (u, v, w) =>
// Haskell: pure (.) <*> u <*> v <*> w = u <*> (v <*> w)
compose.pure.ap(u).ap(v).ap(w) mustBe u.ap(v.ap(w))
}
}
}
}