diff --git a/docs/src/main/tut/freemonad.md b/docs/src/main/tut/freemonad.md index 16c15d8e3d..b0e1e6d4ff 100644 --- a/docs/src/main/tut/freemonad.md +++ b/docs/src/main/tut/freemonad.md @@ -493,6 +493,79 @@ As the sequence of operations becomes longer, the slower a `flatMap` "through" the structure will be. With `FlatMapped`, `Free` becomes a right-associated structure not subject to quadratic complexity. +## FreeT + +Often times we want to interleave the syntax tree when building a Free monad +with some other effect not declared as part of the ADT. +FreeT solves this problem by allowing us to mix building steps of the AST +with calling action in other base monad. + +In the following example a basic console application is shown. +When the user inputs some text we use a separate `State` monad to track what the user +typed. + +As we can observe in this case `FreeT` offers us a the alternative to delegate denotations to `State` +monad with stronger equational guarantees than if we were emulating the `State` ops in our own ADT. + +```tut:book +import cats.free._ +import cats._ +import cats.data._ + +/* A base ADT for the user interaction without state semantics */ +sealed abstract class Teletype[A] extends Product with Serializable +final case class WriteLine(line : String) extends Teletype[Unit] +final case class ReadLine(prompt : String) extends Teletype[String] + +type TeletypeT[M[_], A] = FreeT[Teletype, M, A] +type Log = List[String] + +/** Smart constructors, notice we are abstracting over any MonadState instance + * to potentially support other types beside State + */ +class TeletypeOps[M[_]](implicit MS : MonadState[M, Log]) { + def writeLine(line : String) : TeletypeT[M, Unit] = + FreeT.liftF[Teletype, M, Unit](WriteLine(line)) + def readLine(prompt : String) : TeletypeT[M, String] = + FreeT.liftF[Teletype, M, String](ReadLine(prompt)) + def log(s : String) : TeletypeT[M, Unit] = + FreeT.liftT[Teletype, M, Unit](MS.modify(s :: _)) +} + +object TeletypeOps { + implicit def teleTypeOpsInstance[M[_]](implicit MS : MonadState[M, Log]) : TeletypeOps[M] = new TeletypeOps +} + +type TeletypeState[A] = State[List[String], A] + +def program(implicit TO : TeletypeOps[TeletypeState]) : TeletypeT[TeletypeState, Unit] = { + for { + userSaid <- TO.readLine("what's up?!") + _ <- TO.log(s"user said : $userSaid") + _ <- TO.writeLine("thanks, see you soon!") + } yield () +} + +def interpreter = new (Teletype ~> TeletypeState) { + def apply[A](fa : Teletype[A]) : TeletypeState[A] = { + fa match { + case ReadLine(prompt) => + println(prompt) + val userInput = "hanging in here" //scala.io.StdIn.readLine() + StateT.pure[Eval, List[String], A](userInput) + case WriteLine(line) => + StateT.pure[Eval, List[String], A](println(line)) + } + } +} + +import TeletypeOps._ + +val state = program.foldMap(interpreter) +val initialState = Nil +val (stored, _) = state.run(initialState).value +``` + ## Future Work (TODO) There are many remarkable uses of `Free[_]`. In the future, we will diff --git a/free/src/main/scala/cats/free/FreeT.scala b/free/src/main/scala/cats/free/FreeT.scala new file mode 100644 index 0000000000..558b58e740 --- /dev/null +++ b/free/src/main/scala/cats/free/FreeT.scala @@ -0,0 +1,258 @@ +package cats +package free + +import cats.syntax.either._ +import scala.annotation.tailrec + +/** + * FreeT is a monad transformer for Free monads over a Functor S + * + * Stack safety for `Free` and `FreeT` is based on the paper + * [[http://functorial.com/stack-safety-for-free/index.pdf Stack Safety for Free]] by Phil Freeman + * + * This Scala implementation of `FreeT` and its usages are derived from + * [[https://github.com/scalaz/scalaz/blob/series/7.3.x/core/src/main/scala/scalaz/FreeT.scala Scalaz's FreeT]], + * originally written by Brian McKenna. + */ +sealed abstract class FreeT[S[_], M[_], A] extends Product with Serializable { + import FreeT._ + + final def map[B](f: A => B)(implicit M: Applicative[M]): FreeT[S, M, B] = + flatMap(a => pure(f(a))) + + /** Binds the given continuation to the result of this computation. */ + final def flatMap[B](f: A => FreeT[S, M, B]): FreeT[S, M, B] = + FlatMapped(this, f) + + /** + * Changes the underlying `Monad` for this `FreeT`, ie. + * turning this `FreeT[S, M, A]` into a `FreeT[S, N, A]`. + */ + def hoist[N[_]](mn: M ~> N): FreeT[S, N, A] = + step match { + case e @ FlatMapped(_, _) => + FlatMapped(e.a.hoist(mn), e.f.andThen(_.hoist(mn))) + case Suspend(m) => + Suspend(mn(m)) + } + + /** Change the base functor `S` for a `FreeT` action. */ + def interpret[T[_]](st: S ~> T)(implicit M: Functor[M]): FreeT[T, M, A] = + step match { + case e @ FlatMapped(_, _) => + FlatMapped(e.a.interpret(st), e.f.andThen(_.interpret(st))) + case Suspend(m) => + Suspend(M.map(m)(_.map(s => st(s)))) + } + + /** + * Runs to completion, mapping the suspension with the given transformation + * at each step and accumulating into the monad `M`. + */ + def foldMap(f: S ~> M)(implicit MR: Monad[M], RT: RecursiveTailRecM[M]): M[A] = { + def go(ft: FreeT[S, M, A]): M[Either[FreeT[S, M, A], A]] = + ft match { + case Suspend(ma) => MR.flatMap(ma) { + case Left(a) => MR.pure(Right(a)) + case Right(sa) => MR.map(f(sa))(Right(_)) + } + case g @ FlatMapped(_, _) => g.a match { + case Suspend(mx) => MR.flatMap(mx) { + case Left(x) => MR.pure(Left(g.f(x))) + case Right(sx) => MR.map(f(sx))(x => Left(g.f(x))) + } + case g0 @ FlatMapped(_, _) => MR.pure(Left(g0.a.flatMap(g0.f(_).flatMap(g.f)))) + } + } + + RT.sameType(MR).tailRecM(this)(go) + } + + /** Evaluates a single layer of the free monad */ + def resume(implicit S: Functor[S], MR: Monad[M], RT: RecursiveTailRecM[M]): M[Either[A, S[FreeT[S, M, A]]]] = { + def go(ft: FreeT[S, M, A]): M[Either[FreeT[S, M, A], Either[A, S[FreeT[S, M, A]]]]] = + ft match { + case Suspend(f) => MR.map(f)(as => Right(as.map(S.map(_)(pure(_))))) + case g1 @ FlatMapped(_, _) => g1.a match { + case Suspend(m1) => MR.map(m1) { + case Left(a) => Left(g1.f(a)) + case Right(fc) => Right(Right(S.map(fc)(g1.f(_)))) + } + case g2 @ FlatMapped(_, _) => MR.pure(Left(g2.a.flatMap(g2.f(_).flatMap(g1.f)))) + } + } + + RT.sameType(MR).tailRecM(this)(go) + } + + /** + * Runs to completion, using a function that maps the resumption from `S` to a monad `M`. + */ + def runM(interp: S[FreeT[S, M, A]] => M[FreeT[S, M, A]])(implicit S: Functor[S], MR: Monad[M], RT: RecursiveTailRecM[M]): M[A] = { + def runM2(ft: FreeT[S, M, A]): M[Either[FreeT[S, M, A], A]] = + MR.flatMap(ft.resume) { + case Left(a) => MR.pure(Right(a)) + case Right(fc) => MR.map(interp(fc))(Left(_)) + } + RT.sameType(MR).tailRecM(this)(runM2) + } + + /** + * Finds the first `M` instance, `m`, and maps it to contain the rest + * of the computation. Since only `map` is used on `m`, its structure + * is preserved. + */ + @tailrec + private[cats] final def toM(implicit M: Applicative[M]): M[FreeT[S, M, A]] = + this match { + case Suspend(m) => M.map(m) { + case Left(a) => pure(a) + case Right(s) => liftF(s) + } + case g1 @ FlatMapped(_, _) => g1.a match { + case Suspend(m) => M.map(m) { + case Left(a) => g1.f(a) + case Right(s) => liftF[S, M, g1.A](s).flatMap(g1.f) + } + case g0 @ FlatMapped(_, _) => g0.a.flatMap(g0.f(_).flatMap(g1.f)).toM + } + } + + @tailrec + private def step: FreeT[S, M, A] = + this match { + case g @ FlatMapped(_, _) => g.a match { + case g0 @ FlatMapped(_, _) => g0.a.flatMap(a => g0.f(a).flatMap(g.f)).step + case _ => g + } + case x => x + } + + override def toString(): String = "FreeT(...)" +} + +object FreeT extends FreeTInstances { + /** Suspend the computation with the given suspension. */ + private[free] case class Suspend[S[_], M[_], A](a: M[Either[A, S[A]]]) extends FreeT[S, M, A] + + /** Call a subroutine and continue with the given function. */ + private[free] case class FlatMapped[S[_], M[_], A0, B](a0: FreeT[S, M, A0], f0: A0 => FreeT[S, M, B]) extends FreeT[S, M, B] { + type A = A0 + def a: FreeT[S, M, A] = a0 + def f: A => FreeT[S, M, B] = f0 + } + + /** Return the given value in the free monad. */ + def pure[S[_], M[_], A](value: A)(implicit M: Applicative[M]): FreeT[S, M, A] = Suspend(M.pure(Left(value))) + + def suspend[S[_], M[_], A](a: M[Either[A, S[FreeT[S, M, A]]]])(implicit M: Applicative[M]): FreeT[S, M, A] = + liftT(a).flatMap({ + case Left(a) => pure(a) + case Right(s) => roll(s) + }) + + def tailRecM[S[_], M[_]: Applicative, A, B](a: A)(f: A => FreeT[S, M, Either[A, B]]): FreeT[S, M, B] = + f(a).flatMap { + case Left(a0) => tailRecM(a0)(f) + case Right(b) => pure[S, M, B](b) + } + + def liftT[S[_], M[_], A](value: M[A])(implicit M: Functor[M]): FreeT[S, M, A] = + Suspend(M.map(value)(Left(_))) + + /** A version of `liftT` that infers the nested type constructor. */ + def liftTU[S[_], MA](value: MA)(implicit M: Unapply[Functor, MA]): FreeT[S, M.M, M.A] = + liftT[S, M.M, M.A](M.subst(value))(M.TC) + + /** Suspends a value within a functor in a single step. Monadic unit for a higher-order monad. */ + def liftF[S[_], M[_], A](value: S[A])(implicit M: Applicative[M]): FreeT[S, M, A] = + Suspend(M.pure(Right(value))) + + def roll[S[_], M[_], A](value: S[FreeT[S, M, A]])(implicit M: Applicative[M]): FreeT[S, M, A] = + liftF[S, M, FreeT[S, M, A]](value).flatMap(identity) + +} + +private[free] sealed trait FreeTInstances3 { + implicit def catsFreeMonadStateForFreeT[S[_], M[_], E](implicit M1: MonadState[M, E]): MonadState[FreeT[S, M, ?], E] = + new MonadState[FreeT[S, M, ?], E] with FreeTMonad[S, M] { + override def M = implicitly + override def get = + FreeT.liftT(M1.get) + override def set(s: E) = + FreeT.liftT(M1.set(s)) + } +} + +private[free] sealed trait FreeTInstances2 extends FreeTInstances3 { + implicit def catsFreeMonadErrorForFreeT[S[_], M[_]: RecursiveTailRecM, E](implicit E: MonadError[M, E]): MonadError[FreeT[S, M, ?], E] = + new MonadError[FreeT[S, M, ?], E] with FreeTMonad[S, M] { + override def M = implicitly + override def handleErrorWith[A](fa: FreeT[S, M, A])(f: E => FreeT[S, M, A]) = + FreeT.liftT[S, M, FreeT[S, M, A]](E.handleErrorWith(fa.toM)(f.andThen(_.toM)))(M).flatMap(identity) + override def raiseError[A](e: E) = + FreeT.liftT(E.raiseError[A](e))(M) + } +} + +private[free] sealed trait FreeTInstances1 extends FreeTInstances2 { + implicit def catsFreeFlatMapForFreeT[S[_], M[_]](implicit M0: Applicative[M]): FlatMap[FreeT[S, M, ?]] = + new FreeTFlatMap[S, M] { + implicit def M: Applicative[M] = M0 + } + + implicit def catsFreeTransLiftForFreeT[S[_]]: TransLift.Aux[FreeT[S, ?[_], ?], Functor] = + new TransLift[FreeT[S, ?[_], ?]] { + + type TC[M[_]] = Functor[M] + + override def liftT[M[_]: Functor, A](ma: M[A]): FreeT[S, M, A] = + FreeT.liftT(ma) + } +} + +private[free] sealed trait FreeTInstances0 extends FreeTInstances1 { + implicit def catsFreeMonadForFreeT[S[_], M[_]](implicit M0: Applicative[M]): Monad[FreeT[S, M, ?]] with RecursiveTailRecM[FreeT[S, M, ?]] = + new FreeTMonad[S, M] { + def M = M0 + } + + implicit def catsFreeCombineForFreeT[S[_], M[_]: Applicative: SemigroupK]: SemigroupK[FreeT[S, M, ?]] = + new FreeTCombine[S, M] { + override def M = implicitly + override def M1 = implicitly + } +} + +private[free] sealed trait FreeTInstances extends FreeTInstances0 { + implicit def catsFreeMonadCombineForFreeT[S[_], M[_]: Alternative]: MonadCombine[FreeT[S, M, ?]] = + new MonadCombine[FreeT[S, M, ?]] with FreeTCombine[S, M] with FreeTMonad[S, M] { + override def M = implicitly + override def M1 = implicitly + + override def empty[A] = FreeT.liftT[S, M, A](MonoidK[M].empty[A])(M) + } +} + +private[free] sealed trait FreeTFlatMap[S[_], M[_]] extends FlatMap[FreeT[S, M, ?]] { + implicit def M: Applicative[M] + + override final def map[A, B](fa: FreeT[S, M, A])(f: A => B): FreeT[S, M, B] = fa.map(f) + def flatMap[A, B](fa: FreeT[S, M, A])(f: A => FreeT[S, M, B]): FreeT[S, M, B] = fa.flatMap(f) + override final def tailRecM[A, B](a: A)(f: A => FreeT[S, M, Either[A, B]]): FreeT[S, M, B] = + FreeT.tailRecM(a)(f) +} + +private[free] sealed trait FreeTMonad[S[_], M[_]] extends Monad[FreeT[S, M, ?]] with RecursiveTailRecM[FreeT[S, M, ?]] with FreeTFlatMap[S, M] { + implicit def M: Applicative[M] + + override final def pure[A](a: A): FreeT[S, M, A] = + FreeT.pure[S, M, A](a) +} + +private[free] sealed trait FreeTCombine[S[_], M[_]] extends SemigroupK[FreeT[S, M, ?]] { + implicit def M: Applicative[M] + def M1: SemigroupK[M] + override final def combineK[A](a: FreeT[S, M, A], b: FreeT[S, M, A]): FreeT[S, M, A] = + FreeT.liftT(M1.combineK(a.toM, b.toM))(M).flatMap(identity) +} diff --git a/free/src/test/scala/cats/free/FreeTTests.scala b/free/src/test/scala/cats/free/FreeTTests.scala new file mode 100644 index 0000000000..d88a101503 --- /dev/null +++ b/free/src/test/scala/cats/free/FreeTTests.scala @@ -0,0 +1,217 @@ +package cats +package free + +import cats._ +import cats.arrow.FunctionK +import cats.data._ +import cats.laws.discipline._ +import cats.tests.CatsSuite +import cats.instances.option._ + +import org.scalacheck.{Arbitrary, Gen} + +class FreeTTests extends CatsSuite { + + import FreeTTests._ + + { + implicit val freeTFlatMap: FlatMap[FreeTOption] = FreeT.catsFreeFlatMapForFreeT[Option, Option] + checkAll("FreeT[Option, Option, Int]", FlatMapTests[FreeTOption].flatMap[Int, Int, Int]) + checkAll("FlatMap[FreeT[Option, Option, ?]]", SerializableTests.serializable(FlatMap[FreeTOption])) + } + + { + implicit val freeTMonad: Monad[FreeTOption] = FreeT.catsFreeMonadForFreeT[Option, Option] + checkAll("FreeT[Option, Option, Int]", MonadTests[FreeTOption].monad[Int, Int, Int]) + checkAll("Monad[FreeT[Option, Option, ?]]", SerializableTests.serializable(Monad[FreeTOption])) + } + + { + implicit val freeTSemigroupK: SemigroupK[FreeTOption] = FreeT.catsFreeCombineForFreeT[Option, Option] + checkAll("FreeT[Option, Option, Int]", SemigroupKTests[FreeTOption].semigroupK[Int]) + checkAll("SemigroupK[FreeT[Option, Option, ?]]", SerializableTests.serializable(SemigroupK[FreeTOption])) + } + + { + implicit val freeTCombine: MonadCombine[FreeTOption] = FreeT.catsFreeMonadCombineForFreeT[Option, Option] + checkAll("FreeT[Option, Option, Int]", MonadCombineTests[FreeTOption].monadCombine[Int, Int, Int]) + checkAll("MonadCombine[FreeT[Option, Option, ?]]", SerializableTests.serializable(MonadCombine[FreeTOption])) + } + + { + implicit val eqEithertTFA: Eq[EitherT[FreeTOption, Unit, Int]] = EitherT.catsDataEqForEitherT[FreeTOption, Unit, Int] + checkAll("FreeT[Option, Option, Int]", MonadErrorTests[FreeTOption, Unit].monadError[Int, Int, Int]) + checkAll("MonadError[FreeT[Option, Option, ?], Unit]", SerializableTests.serializable(MonadError[FreeTOption, Unit])) + } + + { + import StateT._ + checkAll("FreeT[State[Int, ?], State[Int, ?], Int]", MonadStateTests[FreeTState, Int].monadState[Int, Int, Int]) + checkAll("MonadState[FreeT[State[Int, ?],State[Int, ?], ?], Int]", SerializableTests.serializable(MonadState[FreeTState, Int])) + } + + test("FlatMap stack safety tested with 50k flatMaps") { + val expected = Applicative[FreeTOption].pure(()) + val result = + Monad[FreeTOption].tailRecM(0)((i: Int) => + if (i < 50000) + Applicative[FreeTOption].pure(Either.left[Int, Unit](i + 1)) + else + Applicative[FreeTOption].pure(Either.right[Int, Unit](()))) + + Eq[FreeTOption[Unit]].eqv(expected, result) should ===(true) + } + + test("Stack safe with 50k left-associated flatMaps") { + val expected = Applicative[FreeTOption].pure(()) + val result = + (0 until 50000).foldLeft(Applicative[FreeTOption].pure(()))( + (fu, i) => fu.flatMap(u => Applicative[FreeTOption].pure(u)) + ) + + Eq[FreeTOption[Unit]].eqv(expected, result) should ===(true) + } + + test("Stack safe with flatMap followed by 50k maps") { + val expected = Applicative[FreeTOption].pure(()) + val result = + (0 until 50000).foldLeft(().pure[FreeTOption].flatMap(_.pure[FreeTOption]))( + (fu, i) => fu.map(identity) + ) + + Eq[FreeTOption[Unit]].eqv(expected, result) should ===(true) + } + + test("hoist to universal id equivalent to original instance") { + forAll { a: FreeTOption[Int] => + val b = a.hoist(FunctionK.id) + Eq[FreeTOption[Int]].eqv(a, b) should ===(true) + } + } + + test("hoist stack-safety") { + val a = (0 until 50000).foldLeft(Applicative[FreeTOption].pure(()))( + (fu, i) => fu.flatMap(u => Applicative[FreeTOption].pure(u)) + ) + val b = a.hoist(FunctionK.id) + } + + test("transLift for FreeT requires only Functor") { + implicit val transLiftInstance = FreeT.catsFreeTransLiftForFreeT[JustFunctor] + val d: FreeT[JustFunctor, JustFunctor, Int] = transLiftInstance.liftT[JustFunctor, Int](JustFunctor(1)) + } + + test("interpret to universal id equivalent to original instance") { + forAll { a: FreeTOption[Int] => + val b = a.interpret(FunctionK.id) + Eq[FreeTOption[Int]].eqv(a, b) should ===(true) + } + } + + test("interpret stack-safety") { + val a = (0 until 50000).foldLeft(Applicative[FreeTOption].pure(()))( + (fu, i) => fu.flatMap(u => Applicative[FreeTOption].pure(u)) + ) + val b = a.interpret(FunctionK.id) // used to overflow + } + + test("foldMap consistent with runM") { + forAll { a: FreeTOption[Int] => + val x = a.runM(identity) + val y = a.foldMap(FunctionK.id) + Eq[Option[Int]].eqv(x, y) should ===(true) + } + } + + test("== should not return true for unequal instances") { + val a = FreeT.pure[List, Option, Int](1).flatMap(x => FreeT.pure(2)) + val b = FreeT.pure[List, Option, Int](3).flatMap(x => FreeT.pure(4)) + a == b should be(false) + } + + test("toString is stack-safe") { + val result = + (0 until 50000).foldLeft(().pure[FreeTOption].flatMap(_.pure[FreeTOption]))( + (fu, i) => fu.map(identity) + ) + result.toString.length should be > 0 + } + + private[free] def liftTUCompilationTests() = { + val a: Either[String, Int]= Right(42) + val b: FreeT[Option, Either[String, ?], Int] = FreeT.liftTU(a) + } + +} + +object FreeTTests extends FreeTTestsInstances { + + import Arbitrary._ + import org.scalacheck.Arbitrary + + implicit def freeTIntStateArb[A: Arbitrary]: Arbitrary[FreeTState[A]] = freeTArb[IntState, IntState, A] + + implicit def freeTArb[F[_], G[_]: Applicative, A](implicit F: Arbitrary[F[A]], G: Arbitrary[G[A]], A: Arbitrary[A]): Arbitrary[FreeT[F, G, A]] = + Arbitrary(freeTGen[F, G, A](4)) + + private def freeTGen[F[_], G[_]: Applicative, A](maxDepth: Int)(implicit F: Arbitrary[F[A]], G: Arbitrary[G[A]], A: Arbitrary[A]): Gen[FreeT[F, G, A]] = { + val noFlatMapped = Gen.oneOf( + A.arbitrary.map(FreeT.pure[F, G, A]), + F.arbitrary.map(FreeT.liftF[F, G, A]) + ) + + val nextDepth = Gen.chooseNum(1, maxDepth - 1) + + def withFlatMapped = for { + fDepth <- nextDepth + freeDepth <- nextDepth + f <- arbFunction1[A, FreeT[F, G, A]](Arbitrary(freeTGen[F, G, A](fDepth))).arbitrary + freeFGA <- freeTGen[F, G, A](freeDepth) + } yield freeFGA.flatMap(f) + + if (maxDepth <= 1) noFlatMapped + else Gen.oneOf(noFlatMapped, withFlatMapped) + } + +} + +trait FreeTTestsInstances { + + import FreeT._ + import StateT._ + import cats.kernel.instances.option._ + import cats.tests.StateTTests._ + import CartesianTests._ + + type IntState[A] = State[Int, A] + type FreeTOption[A] = FreeT[Option, Option, A] + type FreeTState[A] = FreeT[IntState, IntState, A] + + case class JustFunctor[A](a: A) + + implicit val ftlWIso: Isomorphisms[FreeTOption] = CartesianTests.Isomorphisms.invariant[FreeTOption] + + implicit val ftlSIso: Isomorphisms[FreeTState] = CartesianTests.Isomorphisms.invariant[FreeTState] + + implicit val jfFunctor: Functor[JustFunctor] = new Functor[JustFunctor] { + override def map[A, B](fa: JustFunctor[A])(f: A => B): JustFunctor[B] = JustFunctor(f(fa.a)) + } + + implicit val intEq: Eq[Int] = new Eq[Int] { + def eqv(a: Int, b: Int) = a == b + } + + implicit def evalEq[A: Eq]: Eq[Eval[A]] = Eval.catsEqForEval[A] + + implicit def intStateEq[A: Eq]: Eq[IntState[A]] = stateEq[Int, A] + + implicit def intStateArb[A: Arbitrary]: Arbitrary[IntState[A]] = stateArbitrary[Int, A] + + implicit def freeTOptionEq[A](implicit A: Eq[A], OM: Monad[Option], RT: RecursiveTailRecM[Option]): Eq[FreeTOption[A]] = new Eq[FreeTOption[A]] { + def eqv(a: FreeTOption[A], b: FreeTOption[A]) = Eq[Option[A]].eqv(a.runM(identity), b.runM(identity)) + } + + implicit def freeTStateEq[A](implicit A: Eq[A], SM: Monad[IntState], RT: RecursiveTailRecM[IntState]): Eq[FreeTState[A]] = new Eq[FreeTState[A]] { + def eqv(a: FreeTState[A], b: FreeTState[A]) = Eq[IntState[A]].eqv(a.runM(identity)(SM, SM, RT), b.runM(identity)(SM, SM, RT)) + } +} diff --git a/tests/src/test/scala/cats/tests/KleisliTests.scala b/tests/src/test/scala/cats/tests/KleisliTests.scala index 6153e2bb0c..abdb093af4 100644 --- a/tests/src/test/scala/cats/tests/KleisliTests.scala +++ b/tests/src/test/scala/cats/tests/KleisliTests.scala @@ -19,8 +19,8 @@ class KleisliTests extends CatsSuite { implicit val iso = CartesianTests.Isomorphisms.invariant[Kleisli[Option, Int, ?]] - checkAll("Klesili[Option, Int, Int] with Unit", ApplicativeErrorTests[Kleisli[Option, Int, ?], Unit].applicativeError[Int, Int, Int]) - checkAll("ApplicativeError[Klesili[Option, Int, Int], Unit]", SerializableTests.serializable(ApplicativeError[Kleisli[Option, Int, ?], Unit])) + checkAll("Kleisli[Option, Int, Int] with Unit", ApplicativeErrorTests[Kleisli[Option, Int, ?], Unit].applicativeError[Int, Int, Int]) + checkAll("ApplicativeError[Kleisli[Option, Int, Int], Unit]", SerializableTests.serializable(ApplicativeError[Kleisli[Option, Int, ?], Unit])) checkAll("Kleisli[Option, Int, Int]", CartesianTests[Kleisli[Option, Int, ?]].cartesian[Int, Int, Int]) checkAll("Cartesian[Kleisli[Option, Int, ?]]", SerializableTests.serializable(Cartesian[Kleisli[Option, Int, ?]]))