82 lines
3.5 KiB
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))
|
|
}
|
|
}
|
|
}
|
|
}
|