pawel.panasewicz pawel.panasewicz - 2 months ago 15
Scala Question

How to test Monad instance using discipline

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)]

Answer

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.)