Given a monad for Fun
type
type FUN[A] = Map[String, String] => (List[String], A)
val funMonad: Monad[FUN] = new Monad[FUN] {
override def flatMap[A, B](fa: FUN[A])(f: (A) => FUN[B]): FUN[B] = m => {
val (list1, a1) = fa(m)
val (list2, a2) = f(a1)(m)
(list1 ++ list2, a2)
}
override def pure[A](x: A): FUN[A] = m => (Nil, x)
}
The question is: How man can use discipline library to test that this instance of Monad obeys Monad Laws?
Below is partial result, which fails because compiler could not find implicit for CartesianTests.Isomorphisms[FUN]
.
import cats.Monad
import cats.kernel.Eq
import org.scalacheck.rng.Seed
import org.scalacheck.{Arbitrary, Gen}
class MyMonadSpec extends FunSuite with scalatest.Discipline {
...
implicit def funEq[T: Eq]: Eq[FUN[T]] = {
val sampleInput: Map[String, String] = {
def genMap: Gen[Map[String, String]] = for {
size <- Gen.size
keys <- Gen.containerOfN[List, String](size, Arbitrary.arbitrary[String])
values <- Gen.containerOfN[List, String](size, Arbitrary.arbitrary[String])
} yield keys.zip(values).toMap
genMap(Gen.Parameters.default.withSize(10), Seed.apply(123L)).get
}
Eq.instance[FUN[T]] ((f1, f2) => f1(sampleInput) == f2(sampleInput))
}
import cats.kernel.instances.int._
import cats.kernel.instances.tuple._
import cats.laws.discipline.MonadTests
checkAll("Int", MonadTests[FUN](funMonad).monad[Int, Int, Int])
//Error: could not find implicit value for parameter iso: cats.laws.discipline.CartesianTests.Isomorphisms[[A]scala.collection.immutable.Map[String,String] => (List[String], A)]
Normally you could just put your instance into scope and do this (you could clean this up a bit by importing cats.instances.all._
, but I'm being explicit for clarity):
import cats.instances.int._
import cats.instances.list._
import cats.instances.map._
import cats.instances.string._
import cats.instances.tuple._
import cats.laws.discipline.MonadTests
import cats.laws.discipline.eq._
MonadTests[FUN].monad[Int, Int, Int].all.check
You'd get the Isomorphisms
instance for free, since it only requires an Invariant
instance, which is implied by the Monad
. Also note that you shouldn't need to define your own Eq
instances—the eq
package provides a Function1
instance that's appropriate for testing for you.
In this case the compiler won't actually find the Invariant
instance, though (this could be an SI-2712 issue or it could have something to do with the alias—off the top of my head I'm not sure), and it seems like you want to test the monad instance without putting it into implicit scope. One easy way to do this is to provide your own Isomorphisms
:
import cats.laws.discipline.CartesianTests.Isomorphisms
implicit val funIsomorphisms: Isomorphisms[FUN] = Isomorphisms.invariant(funMonad)
Or as a complete working example (on Cats 0.7.2):
import cats.Monad
import cats.instances.int._
import cats.instances.list._
import cats.instances.map._
import cats.instances.string._
import cats.instances.tuple._
import cats.instances.map._
import cats.laws.discipline.CartesianTests.Isomorphisms
import cats.laws.discipline.MonadTests
import cats.laws.discipline.eq._
type FUN[A] = Map[String, String] => (List[String], A)
val funMonad: Monad[FUN] = new Monad[FUN] {
def flatMap[A, B](fa: FUN[A])(f: (A) => FUN[B]): FUN[B] = m => {
val (list1, a1) = fa(m)
val (list2, a2) = f(a1)(m)
(list1 ++ list2, a2)
}
def pure[A](x: A): FUN[A] = m => (Nil, x)
def tailRecM[A, B](a: A)(f: A => FUN[Either[A, B]]): FUN[B] = defaultTailRecM(a)(f)
}
implicit val funIsomorphisms: Isomorphisms[FUN] = Isomorphisms.invariant(funMonad)
And then:
scala> MonadTests[FUN](funMonad).monad[Int, Int, Int].all.check
+ monad.ap consistent with product + map: OK, passed 100 tests.
+ monad.applicative homomorphism: OK, passed 100 tests.
+ monad.applicative identity: OK, passed 100 tests.
+ monad.applicative interchange: OK, passed 100 tests.
+ monad.applicative map: OK, passed 100 tests.
+ monad.apply composition: OK, passed 100 tests.
+ monad.cartesian associativity: OK, passed 100 tests.
+ monad.covariant composition: OK, passed 100 tests.
+ monad.covariant identity: OK, passed 100 tests.
+ monad.flatMap associativity: OK, passed 100 tests.
+ monad.flatMap consistent apply: OK, passed 100 tests.
+ monad.followedBy consistent flatMap: OK, passed 100 tests.
+ monad.invariant composition: OK, passed 100 tests.
+ monad.invariant identity: OK, passed 100 tests.
+ monad.map flatMap coherence: OK, passed 100 tests.
+ monad.monad left identity: OK, passed 100 tests.
+ monad.monad right identity: OK, passed 100 tests.
+ monad.monoidal left identity: OK, passed 100 tests.
+ monad.monoidal right identity: OK, passed 100 tests.
+ monad.mproduct consistent flatMap: OK, passed 100 tests.
+ monad.tailRecM consistent flatMap: OK, passed 100 tests.
(You could also use checkAll
—I'm just doing .all.check
because it doesn't require you to have ScalaTest around or instantiate a FunSuite
.)