From 5fca9427a7bbe4a2c45e328f3f37c058d467598b Mon Sep 17 00:00:00 2001 From: Andy Scott Date: Mon, 16 Oct 2017 12:19:01 -0400 Subject: [PATCH 1/5] Add mapK to most (hopefully all) transformers --- core/src/main/scala/cats/data/EitherT.scala | 5 +++++ core/src/main/scala/cats/data/IdT.scala | 6 ++++++ .../scala/cats/data/IndexedReaderWriterStateT.scala | 7 +++++++ core/src/main/scala/cats/data/IndexedStateT.scala | 7 +++++++ core/src/main/scala/cats/data/Kleisli.scala | 6 ++++++ core/src/main/scala/cats/data/OptionT.scala | 5 +++++ core/src/main/scala/cats/data/WriterT.scala | 6 ++++++ tests/src/test/scala/cats/tests/EitherTSuite.scala | 7 +++++++ tests/src/test/scala/cats/tests/IdTSuite.scala | 7 +++++++ .../cats/tests/IndexedReaderWriterStateTTests.scala | 7 +++++++ .../test/scala/cats/tests/IndexedStateTSuite.scala | 7 +++++++ tests/src/test/scala/cats/tests/KleisliSuite.scala | 7 +++++++ tests/src/test/scala/cats/tests/OptionTSuite.scala | 7 +++++++ tests/src/test/scala/cats/tests/WriterTSuite.scala | 11 +++++++++-- 14 files changed, 93 insertions(+), 2 deletions(-) diff --git a/core/src/main/scala/cats/data/EitherT.scala b/core/src/main/scala/cats/data/EitherT.scala index b0f8094eb0..85ac89d315 100644 --- a/core/src/main/scala/cats/data/EitherT.scala +++ b/core/src/main/scala/cats/data/EitherT.scala @@ -91,6 +91,11 @@ final case class EitherT[F[_], A, B](value: F[Either[A, B]]) { def map[D](f: B => D)(implicit F: Functor[F]): EitherT[F, A, D] = bimap(identity, f) + /** + * Modify the context `F` using transformation `f`. + */ + def mapK[G[_]](f: F ~> G): EitherT[G, A, B] = EitherT[G, A, B](f(value)) + def semiflatMap[D](f: B => F[D])(implicit F: Monad[F]): EitherT[F, A, D] = flatMap(b => EitherT.right(f(b))) diff --git a/core/src/main/scala/cats/data/IdT.scala b/core/src/main/scala/cats/data/IdT.scala index d8b3f09c35..07b07e79c0 100644 --- a/core/src/main/scala/cats/data/IdT.scala +++ b/core/src/main/scala/cats/data/IdT.scala @@ -9,6 +9,12 @@ final case class IdT[F[_], A](value: F[A]) { def map[B](f: A => B)(implicit F: Functor[F]): IdT[F, B] = IdT(F.map(value)(f)) + /** + * Modify the context `F` using transformation `f`. + */ + def mapK[G[_]](f: F ~> G): IdT[G, A] = + IdT[G, A](f(value)) + def flatMap[B](f: A => IdT[F, B])(implicit F: FlatMap[F]): IdT[F, B] = IdT(F.flatMap(value)(f.andThen(_.value))) diff --git a/core/src/main/scala/cats/data/IndexedReaderWriterStateT.scala b/core/src/main/scala/cats/data/IndexedReaderWriterStateT.scala index 72b25d37c0..fc3ef56bd8 100644 --- a/core/src/main/scala/cats/data/IndexedReaderWriterStateT.scala +++ b/core/src/main/scala/cats/data/IndexedReaderWriterStateT.scala @@ -52,6 +52,13 @@ final class IndexedReaderWriterStateT[F[_], E, L, SA, SB, A](val runF: F[(E, SA) def map[B](f: A => B)(implicit F: Functor[F]): IndexedReaderWriterStateT[F, E, L, SA, SB, B] = transform { (l, s, a) => (l, s, f(a)) } + /** + * Modify the context `F` using transformation `f`. + */ + def mapK[G[_]](f: F ~> G)(implicit F: Functor[F]): IndexedReaderWriterStateT[G, E, L, SA, SB, A] = + IndexedReaderWriterStateT.applyF( + f(F.map(runF)(rwsa => (e, sa) => f(rwsa(e, sa))))) + /** * Modify the resulting state using `f` and the resulting value using `g`. */ diff --git a/core/src/main/scala/cats/data/IndexedStateT.scala b/core/src/main/scala/cats/data/IndexedStateT.scala index bd170310b7..c599eacf24 100644 --- a/core/src/main/scala/cats/data/IndexedStateT.scala +++ b/core/src/main/scala/cats/data/IndexedStateT.scala @@ -39,6 +39,13 @@ final class IndexedStateT[F[_], SA, SB, A](val runF: F[SA => F[(SB, A)]]) extend def map[B](f: A => B)(implicit F: Functor[F]): IndexedStateT[F, SA, SB, B] = transform { case (s, a) => (s, f(a)) } + /** + * Modify the context `F` using transformation `f`. + */ + def mapK[G[_]](f: F ~> G)(implicit F: Functor[F]): IndexedStateT[G, SA, SB, A] = + IndexedStateT.applyF( + f(F.map(runF)(_.andThen(fsa => f(fsa))))) + def contramap[S0](f: S0 => SA)(implicit F: Functor[F]): IndexedStateT[F, S0, SB, A] = IndexedStateT.applyF { F.map(runF) { safsba => diff --git a/core/src/main/scala/cats/data/Kleisli.scala b/core/src/main/scala/cats/data/Kleisli.scala index 280561ec2b..e90e19885c 100644 --- a/core/src/main/scala/cats/data/Kleisli.scala +++ b/core/src/main/scala/cats/data/Kleisli.scala @@ -21,6 +21,12 @@ final case class Kleisli[F[_], A, B](run: A => F[B]) { self => def mapF[N[_], C](f: F[B] => N[C]): Kleisli[N, A, C] = Kleisli(run andThen f) + /** + * Modify the context `F` using transformation `f`. + */ + def mapK[G[_]](f: F ~> G): Kleisli[G, A, B] = + Kleisli[G, A, B](run andThen f.apply) + def flatMap[C](f: B => Kleisli[F, A, C])(implicit F: FlatMap[F]): Kleisli[F, A, C] = Kleisli((r: A) => F.flatMap[B, C](run(r))((b: B) => f(b).run(r))) diff --git a/core/src/main/scala/cats/data/OptionT.scala b/core/src/main/scala/cats/data/OptionT.scala index 6dfab405da..cf59218c8f 100644 --- a/core/src/main/scala/cats/data/OptionT.scala +++ b/core/src/main/scala/cats/data/OptionT.scala @@ -28,6 +28,11 @@ final case class OptionT[F[_], A](value: F[Option[A]]) { def map[B](f: A => B)(implicit F: Functor[F]): OptionT[F, B] = OptionT(F.map(value)(_.map(f))) + /** + * Modify the context `F` using transformation `f`. + */ + def mapK[G[_]](f: F ~> G): OptionT[G, A] = OptionT[G, A](f(value)) + def semiflatMap[B](f: A => F[B])(implicit F: Monad[F]): OptionT[F, B] = flatMap(a => OptionT.liftF(f(a))) diff --git a/core/src/main/scala/cats/data/WriterT.scala b/core/src/main/scala/cats/data/WriterT.scala index 22390eaec8..582f5c85a4 100644 --- a/core/src/main/scala/cats/data/WriterT.scala +++ b/core/src/main/scala/cats/data/WriterT.scala @@ -27,6 +27,12 @@ final case class WriterT[F[_], L, V](run: F[(L, V)]) { functorF.map(run) { z => (z._1, fn(z._2)) } } + /** + * Modify the context `F` using transformation `f`. + */ + def mapK[G[_]](f: F ~> G): WriterT[G, L, V] = + WriterT[G, L, V](f(run)) + def contramap[Z](fn: Z => V)(implicit F: Contravariant[F]): WriterT[F, L, Z] = WriterT { F.contramap(run) { z => (z._1, fn(z._2)) } diff --git a/tests/src/test/scala/cats/tests/EitherTSuite.scala b/tests/src/test/scala/cats/tests/EitherTSuite.scala index 87cb2cc14a..c81ff1ad8e 100644 --- a/tests/src/test/scala/cats/tests/EitherTSuite.scala +++ b/tests/src/test/scala/cats/tests/EitherTSuite.scala @@ -242,6 +242,13 @@ class EitherTSuite extends CatsSuite { } } + test("mapK consistent with f(value)+pure") { + val f: List ~> Option = λ[List ~> Option](_.headOption) + forAll { (eithert: EitherT[List, String, Int]) => + eithert.mapK(f) should === (EitherT(f(eithert.value))) + } + } + test("semiflatMap consistent with value.flatMap+f+pure") { forAll { (eithert: EitherT[List, String, Int], f: Int => List[String]) => eithert.semiflatMap(f) should === (EitherT(eithert.value.flatMap { diff --git a/tests/src/test/scala/cats/tests/IdTSuite.scala b/tests/src/test/scala/cats/tests/IdTSuite.scala index e3f1f65f89..7cc192890c 100644 --- a/tests/src/test/scala/cats/tests/IdTSuite.scala +++ b/tests/src/test/scala/cats/tests/IdTSuite.scala @@ -87,4 +87,11 @@ class IdTSuite extends CatsSuite { } } + test("mapK consistent with f(value)+pure") { + val f: List ~> Option = λ[List ~> Option](_.headOption) + forAll { (idT: IdT[List, Int]) => + idT.mapK(f) should === (IdT(f(idT.value))) + } + } + } diff --git a/tests/src/test/scala/cats/tests/IndexedReaderWriterStateTTests.scala b/tests/src/test/scala/cats/tests/IndexedReaderWriterStateTTests.scala index 45748955ea..235e1145fc 100644 --- a/tests/src/test/scala/cats/tests/IndexedReaderWriterStateTTests.scala +++ b/tests/src/test/scala/cats/tests/IndexedReaderWriterStateTTests.scala @@ -288,6 +288,13 @@ class ReaderWriterStateTSuite extends CatsSuite { } } + test("ReaderWriterStateT.mapK transforms effect") { + val f: Eval ~> Id = λ[Eval ~> Id](_.value) + forAll { (state: ReaderWriterStateT[Eval, Long, String, String, Int], env: Long, initial: String) => + state.mapK(f).runA(env, initial) should === (state.runA(env, initial).value) + } + } + test(".get and then .run produces the same state as value") { forAll { (c: String, initial: Long, rws: ReaderWriterState[String, String, Long, Long]) => val (_, state, value) = rws.get.run(c, initial).value diff --git a/tests/src/test/scala/cats/tests/IndexedStateTSuite.scala b/tests/src/test/scala/cats/tests/IndexedStateTSuite.scala index a708a0b64f..1f5fe0091f 100644 --- a/tests/src/test/scala/cats/tests/IndexedStateTSuite.scala +++ b/tests/src/test/scala/cats/tests/IndexedStateTSuite.scala @@ -233,6 +233,13 @@ class IndexedStateTSuite extends CatsSuite { } } + test("StateT#mapK transforms effect") { + val f: Eval ~> Id = λ[Eval ~> Id](_.value) + forAll { (state: StateT[Eval, Long, Int], initial: Long) => + state.mapK(f).runA(initial) should === (state.runA(initial).value) + } + } + test("StateT#transformS modifies state") { final case class Env(int: Int, str: String) val x = StateT((x: Int) => Option((x + 1, x))) diff --git a/tests/src/test/scala/cats/tests/KleisliSuite.scala b/tests/src/test/scala/cats/tests/KleisliSuite.scala index f6e9029454..d1877e293b 100644 --- a/tests/src/test/scala/cats/tests/KleisliSuite.scala +++ b/tests/src/test/scala/cats/tests/KleisliSuite.scala @@ -164,6 +164,13 @@ class KleisliSuite extends CatsSuite { } } + test("mapK") { + val t: List ~> Option = λ[List ~> Option](_.headOption) + forAll { (f: Kleisli[List, Int, Int], i: Int) => + t(f.run(i)) should === (f.mapK(t).run(i)) + } + } + test("flatMapF") { forAll { (f: Kleisli[List, Int, Int], t: Int => List[Int], i: Int) => f.run(i).flatMap(t) should === (f.flatMapF(t).run(i)) diff --git a/tests/src/test/scala/cats/tests/OptionTSuite.scala b/tests/src/test/scala/cats/tests/OptionTSuite.scala index 8bc3751d85..047240a8cc 100644 --- a/tests/src/test/scala/cats/tests/OptionTSuite.scala +++ b/tests/src/test/scala/cats/tests/OptionTSuite.scala @@ -264,6 +264,13 @@ class OptionTSuite extends CatsSuite { } } + test("mapK consistent with f(value)+pure") { + val f: List ~> Option = λ[List ~> Option](_.headOption) + forAll { (optiont: OptionT[List, Int]) => + optiont.mapK(f) should === (OptionT(f(optiont.value))) + } + } + test("semiflatMap consistent with value.flatMap+f+pure") { forAll { (o: OptionT[List, Int], f: Int => List[String]) => o.semiflatMap(f) should === (OptionT(o.value.flatMap { diff --git a/tests/src/test/scala/cats/tests/WriterTSuite.scala b/tests/src/test/scala/cats/tests/WriterTSuite.scala index 6cbcbd5dc2..06ca448ed7 100644 --- a/tests/src/test/scala/cats/tests/WriterTSuite.scala +++ b/tests/src/test/scala/cats/tests/WriterTSuite.scala @@ -64,7 +64,7 @@ class WriterTSuite extends CatsSuite { WriterT.valueT[Id, Int, Int](i).value should === (i) } } - + test("Writer.pure and WriterT.lift are consistent") { forAll { (i: Int) => val writer: Writer[String, Int] = Writer.value(i) @@ -72,7 +72,7 @@ class WriterTSuite extends CatsSuite { writer.run.some should === (writerT.run) } } - + test("show") { val writerT: WriterT[Id, List[String], String] = WriterT.put("foo")(List("Some log message")) writerT.show should === ("(List(Some log message),foo)") @@ -89,6 +89,13 @@ class WriterTSuite extends CatsSuite { Writer.tell("foo").written should === ("foo") } + test("mapK consistent with f(value)+pure") { + val f: List ~> Option = λ[List ~> Option](_.headOption) + forAll { (writert: WriterT[List, String, Int]) => + writert.mapK(f) should === (WriterT(f(writert.run))) + } + } + { // F has a SemigroupK implicit val F: SemigroupK[ListWrapper] = ListWrapper.semigroupK From d0d061fc6ae9a3115cb438bdd5c32a459fddc749 Mon Sep 17 00:00:00 2001 From: Andy Scott Date: Sat, 21 Oct 2017 16:09:59 -0700 Subject: [PATCH 2/5] Add mapK to free related constructs --- free/src/main/scala/cats/free/Coyoneda.scala | 9 +++++++- free/src/main/scala/cats/free/FreeT.scala | 22 ++++++++++++------- free/src/main/scala/cats/free/Yoneda.scala | 9 +++++++- .../test/scala/cats/free/CoyonedaSuite.scala | 4 ++-- .../src/test/scala/cats/free/FreeTSuite.scala | 8 +++---- 5 files changed, 36 insertions(+), 16 deletions(-) diff --git a/free/src/main/scala/cats/free/Coyoneda.scala b/free/src/main/scala/cats/free/Coyoneda.scala index 58e3b722f6..d1d9de4019 100644 --- a/free/src/main/scala/cats/free/Coyoneda.scala +++ b/free/src/main/scala/cats/free/Coyoneda.scala @@ -46,9 +46,16 @@ sealed abstract class Coyoneda[F[_], A] extends Serializable { self => final def map[B](f: A => B): Aux[F, B, Pivot] = unsafeApply(fi)(f.asInstanceOf[Any => Any] :: ks) - final def transform[G[_]](f: FunctionK[F, G]): Aux[G, A, Pivot] = + /** + * Modify the context `F` using transformation `f`. + */ + final def mapK[G[_]](f: F ~> G): Aux[G, A, Pivot] = unsafeApply(f(fi))(ks) + @deprecated("Use mapK", "1.0.0") + final def transform[G[_]](f: FunctionK[F, G]): Aux[G, A, Pivot] = + mapK(f) + } object Coyoneda { diff --git a/free/src/main/scala/cats/free/FreeT.scala b/free/src/main/scala/cats/free/FreeT.scala index 630d7f0a33..27efc39f7b 100644 --- a/free/src/main/scala/cats/free/FreeT.scala +++ b/free/src/main/scala/cats/free/FreeT.scala @@ -21,6 +21,17 @@ sealed abstract class FreeT[S[_], M[_], A] extends Product with Serializable { final def map[B](f: A => B)(implicit M: Applicative[M]): FreeT[S, M, B] = flatMap(a => pure(f(a))) + /** + * Modify the context `M` using transformation `mn`. + */ + def mapK[N[_]](mn: M ~> N): FreeT[S, N, A] = + step match { + case e @ FlatMapped(_, _) => + FlatMapped(e.a.mapK(mn), e.f.andThen(_.mapK(mn))) + case Suspend(m) => + Suspend(mn(m)) + } + /** 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) @@ -28,14 +39,10 @@ sealed abstract class FreeT[S[_], M[_], A] extends Product with Serializable { /** * Changes the underlying `Monad` for this `FreeT`, ie. * turning this `FreeT[S, M, A]` into a `FreeT[S, N, A]`. - */ + */ + @deprecated("Use mapK", "1.0.0") def hoist[N[_]](mn: FunctionK[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)) - } + mapK(mn) @deprecated("Use compile", "0.8.0") def interpret[T[_]](st: FunctionK[S, T])(implicit M: Functor[M]): FreeT[T, M, A] = compile(st) @@ -251,4 +258,3 @@ private[free] sealed trait FreeTSemigroupK[S[_], M[_]] extends SemigroupK[FreeT[ 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/main/scala/cats/free/Yoneda.scala b/free/src/main/scala/cats/free/Yoneda.scala index 2e9e468081..841435b7f4 100644 --- a/free/src/main/scala/cats/free/Yoneda.scala +++ b/free/src/main/scala/cats/free/Yoneda.scala @@ -28,6 +28,14 @@ abstract class Yoneda[F[_], A] extends Serializable { self => new Yoneda[F, B] { def apply[C](g: B => C): F[C] = self(f andThen g) } + + /** + * Modify the context `F` using transformation `f`. + */ + def mapK[G[_]](f: F ~> G): Yoneda[G, A] = + new Yoneda[G, A] { + def apply[B](g: A => B): G[B] = f(self(g)) + } } object Yoneda { @@ -48,4 +56,3 @@ object Yoneda { def apply[B](f: A => B): F[B] = F.map(fa)(f) } } - diff --git a/free/src/test/scala/cats/free/CoyonedaSuite.scala b/free/src/test/scala/cats/free/CoyonedaSuite.scala index 17e61ea4ce..89dcd9fc6d 100644 --- a/free/src/test/scala/cats/free/CoyonedaSuite.scala +++ b/free/src/test/scala/cats/free/CoyonedaSuite.scala @@ -25,11 +25,11 @@ class CoyonedaSuite extends CatsSuite { } } - test("transform and run is same as applying natural trans") { + test("mapK and run is same as applying natural trans") { val nt = λ[FunctionK[Option, List]](_.toList) val o = Option("hello") val c = Coyoneda.lift(o) - c.transform(nt).run should === (nt(o)) + c.mapK(nt).run should === (nt(o)) } test("map order") { diff --git a/free/src/test/scala/cats/free/FreeTSuite.scala b/free/src/test/scala/cats/free/FreeTSuite.scala index d604021fe8..36e5e07df1 100644 --- a/free/src/test/scala/cats/free/FreeTSuite.scala +++ b/free/src/test/scala/cats/free/FreeTSuite.scala @@ -76,18 +76,18 @@ class FreeTSuite extends CatsSuite { Eq[FreeTOption[Unit]].eqv(expected, result) should ===(true) } - test("hoist to universal id equivalent to original instance") { + test("mapK to universal id equivalent to original instance") { forAll { a: FreeTOption[Int] => - val b = a.hoist(FunctionK.id) + val b = a.mapK(FunctionK.id) Eq[FreeTOption[Int]].eqv(a, b) should ===(true) } } - test("hoist stack-safety") { + test("mapK 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) + val b = a.mapK(FunctionK.id) } test("compile to universal id equivalent to original instance") { From 2c4e75316b4646629c82be8dfd2366c949f22be7 Mon Sep 17 00:00:00 2001 From: Andy Scott Date: Sat, 21 Oct 2017 16:16:21 -0700 Subject: [PATCH 3/5] Deprecate Kleisli#transform in favor of mapK --- core/src/main/scala/cats/data/Kleisli.scala | 3 ++- tests/src/test/scala/cats/tests/KleisliSuite.scala | 9 --------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/core/src/main/scala/cats/data/Kleisli.scala b/core/src/main/scala/cats/data/Kleisli.scala index e90e19885c..f2f4cad0b0 100644 --- a/core/src/main/scala/cats/data/Kleisli.scala +++ b/core/src/main/scala/cats/data/Kleisli.scala @@ -54,8 +54,9 @@ final case class Kleisli[F[_], A, B](run: A => F[B]) { self => def local[AA](f: AA => A): Kleisli[F, AA, B] = Kleisli(f.andThen(run)) + @deprecated("Use mapK", "1.0.0") def transform[G[_]](f: FunctionK[F, G]): Kleisli[G, A, B] = - Kleisli(a => f(run(a))) + mapK(f) def lower(implicit F: Applicative[F]): Kleisli[F, A, F[B]] = Kleisli(a => F.pure(run(a))) diff --git a/tests/src/test/scala/cats/tests/KleisliSuite.scala b/tests/src/test/scala/cats/tests/KleisliSuite.scala index d1877e293b..4f7257288f 100644 --- a/tests/src/test/scala/cats/tests/KleisliSuite.scala +++ b/tests/src/test/scala/cats/tests/KleisliSuite.scala @@ -219,15 +219,6 @@ class KleisliSuite extends CatsSuite { (List(1, 2, 3) >>= l.run) should === (List(Some(2), Some(3), Some(4))) } - test("transform") { - val opt = Kleisli { (x: Int) => Option(x.toDouble) } - val optToList = λ[FunctionK[Option,List]](_.toList) - val list = opt.transform(optToList) - - val is = 0.to(10).toList - is.map(list.run) should === (is.map(Kleisli { (x: Int) => List(x.toDouble) }.run)) - } - test("local") { case class Config(i: Int, s: String) From 99a15102fec6d37d7d53c30f465df56a3b223721 Mon Sep 17 00:00:00 2001 From: Andy Scott Date: Sun, 29 Oct 2017 11:27:28 -0700 Subject: [PATCH 4/5] Fix tests file name --- ...iterStateTTests.scala => IndexedReaderWriterStateTSuite.scala} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/src/test/scala/cats/tests/{IndexedReaderWriterStateTTests.scala => IndexedReaderWriterStateTSuite.scala} (100%) diff --git a/tests/src/test/scala/cats/tests/IndexedReaderWriterStateTTests.scala b/tests/src/test/scala/cats/tests/IndexedReaderWriterStateTSuite.scala similarity index 100% rename from tests/src/test/scala/cats/tests/IndexedReaderWriterStateTTests.scala rename to tests/src/test/scala/cats/tests/IndexedReaderWriterStateTSuite.scala From d623055b029a987b6f2f75f0dd5f34b976a5f26a Mon Sep 17 00:00:00 2001 From: Andy Scott Date: Sun, 29 Oct 2017 11:56:29 -0700 Subject: [PATCH 5/5] Add missing mapK methods --- core/src/main/scala/cats/data/EitherK.scala | 7 ++++- core/src/main/scala/cats/data/Func.scala | 6 ++++ core/src/main/scala/cats/data/Nested.scala | 10 +++++- core/src/main/scala/cats/data/OneAnd.scala | 6 ++++ core/src/main/scala/cats/data/Tuple2K.scala | 10 +++++- free/src/main/scala/cats/free/Free.scala | 34 ++++++++++++++++----- 6 files changed, 63 insertions(+), 10 deletions(-) diff --git a/core/src/main/scala/cats/data/EitherK.scala b/core/src/main/scala/cats/data/EitherK.scala index 4eb085efc6..e62fa7d509 100644 --- a/core/src/main/scala/cats/data/EitherK.scala +++ b/core/src/main/scala/cats/data/EitherK.scala @@ -16,6 +16,12 @@ final case class EitherK[F[_], G[_], A](run: Either[F[A], G[A]]) { def map[B](f: A => B)(implicit F: Functor[F], G: Functor[G]): EitherK[F, G, B] = EitherK(run.bimap(F.lift(f), G.lift(f))) + /** + * Modify the right side context `G` using transformation `f`. + */ + def mapK[H[_]](f: G ~> H): EitherK[F, H, A] = + EitherK(run.map(f.apply)) + def coflatMap[B](f: EitherK[F, G, A] => B)(implicit F: CoflatMap[F], G: CoflatMap[G]): EitherK[F, G, B] = EitherK( run.bimap(a => F.coflatMap(a)(x => f(leftc(x))), a => G.coflatMap(a)(x => f(rightc(x)))) @@ -234,4 +240,3 @@ private[data] trait EitherKComonad[F[_], G[_]] extends Comonad[EitherK[F, G, ?]] def extract[A](p: EitherK[F, G, A]): A = p.extract } - diff --git a/core/src/main/scala/cats/data/Func.scala b/core/src/main/scala/cats/data/Func.scala index db069f4362..17b480048e 100644 --- a/core/src/main/scala/cats/data/Func.scala +++ b/core/src/main/scala/cats/data/Func.scala @@ -12,6 +12,12 @@ sealed abstract class Func[F[_], A, B] { self => def run: A => F[B] def map[C](f: B => C)(implicit FF: Functor[F]): Func[F, A, C] = Func.func(a => FF.map(self.run(a))(f)) + + /** + * Modify the context `F` using transformation `f`. + */ + def mapK[G[_]](f: F ~> G): Func[G, A, B] = + Func.func(run andThen f.apply) } object Func extends FuncInstances { diff --git a/core/src/main/scala/cats/data/Nested.scala b/core/src/main/scala/cats/data/Nested.scala index 97e440ab9d..45c09962f9 100644 --- a/core/src/main/scala/cats/data/Nested.scala +++ b/core/src/main/scala/cats/data/Nested.scala @@ -23,7 +23,15 @@ package data * res1: List[Option[String]] = List(Some(2), None) * }}} */ -final case class Nested[F[_], G[_], A](value: F[G[A]]) +final case class Nested[F[_], G[_], A](value: F[G[A]]) { + + /** + * Modify the context `F` using transformation `f`. + */ + def mapK[H[_]](f: F ~> H): Nested[H, G, A] = + Nested(f(value)) + +} object Nested extends NestedInstances diff --git a/core/src/main/scala/cats/data/OneAnd.scala b/core/src/main/scala/cats/data/OneAnd.scala index 03782ea486..03f1a88de6 100644 --- a/core/src/main/scala/cats/data/OneAnd.scala +++ b/core/src/main/scala/cats/data/OneAnd.scala @@ -76,6 +76,12 @@ final case class OneAnd[F[_], A](head: A, tail: F[A]) { def map[B](f: A => B)(implicit F: Functor[F]): OneAnd[F, B] = OneAnd(f(head), F.map(tail)(f)) + /** + * Modify the context `F` using transformation `f`. + */ + def mapK[G[_]](f: F ~> G): OneAnd[G, A] = + OneAnd(head, f(tail)) + /** * Typesafe equality operator. * diff --git a/core/src/main/scala/cats/data/Tuple2K.scala b/core/src/main/scala/cats/data/Tuple2K.scala index 70c0c98c34..7bd5e2e417 100644 --- a/core/src/main/scala/cats/data/Tuple2K.scala +++ b/core/src/main/scala/cats/data/Tuple2K.scala @@ -8,7 +8,15 @@ import cats.Contravariant * * See: [[https://www.cs.ox.ac.uk/jeremy.gibbons/publications/iterator.pdf The Essence of the Iterator Pattern]] */ -final case class Tuple2K[F[_], G[_], A](first: F[A], second: G[A]) +final case class Tuple2K[F[_], G[_], A](first: F[A], second: G[A]) { + + /** + * Modify the context `G` of `second` using transformation `f`. + */ + def mapK[H[_]](f: G ~> H): Tuple2K[F, H, A] = + Tuple2K(first, f(second)) + +} object Tuple2K extends Tuple2KInstances diff --git a/free/src/main/scala/cats/free/Free.scala b/free/src/main/scala/cats/free/Free.scala index b07127b180..bd63c75168 100644 --- a/free/src/main/scala/cats/free/Free.scala +++ b/free/src/main/scala/cats/free/Free.scala @@ -17,6 +17,21 @@ sealed abstract class Free[S[_], A] extends Product with Serializable { final def map[B](f: A => B): Free[S, B] = flatMap(a => Pure(f(a))) + /** + * Modify the functor context `S` using transformation `f`. + * + * This is effectively compiling your free monad into another + * language by changing the suspension functor using the given + * natural transformation `f`. + * + * If your natural transformation is effectful, be careful. These + * effects will be applied by `mapK`. + */ + final def mapK[T[_]](f: S ~> T): Free[T, A] = + foldMap[Free[T, ?]] { // this is safe because Free is stack safe + λ[FunctionK[S, Free[T, ?]]](fa => Suspend(f(fa))) + }(Free.catsFreeMonadForFree) + /** * Bind the given continuation to the result of this computation. * All left-associated binds are reassociated to the right. @@ -147,11 +162,9 @@ sealed abstract class Free[S[_], A] extends Product with Serializable { * * If your natural transformation is effectful, be careful. These * effects will be applied by `compile`. - */ - final def compile[T[_]](f: FunctionK[S, T]): Free[T, A] = - foldMap[Free[T, ?]] { // this is safe because Free is stack safe - λ[FunctionK[S, Free[T, ?]]](fa => Suspend(f(fa))) - }(Free.catsFreeMonadForFree) + */ + @deprecated("Use mapK", "1.0.0") + final def compile[T[_]](f: FunctionK[S, T]): Free[T, A] = mapK(f) /** * Lift into `G` (typically a `EitherK`) given `InjectK`. Analogous @@ -169,7 +182,7 @@ sealed abstract class Free[S[_], A] extends Product with Serializable { *}}} */ final def inject[G[_]](implicit ev: InjectK[S, G]): Free[G, A] = - compile(λ[S ~> G](ev.inj(_))) + mapK(λ[S ~> G](ev.inj(_))) override def toString: String = "Free(...)" @@ -217,11 +230,18 @@ object Free extends FreeInstances { def defer[F[_], A](value: => Free[F, A]): Free[F, A] = pure(()).flatMap(_ => value) + /** + * a FunctionK, suitable for composition, which calls mapK + */ + def mapK[F[_], G[_]](fk: FunctionK[F, G]): FunctionK[Free[F, ?], Free[G, ?]] = + λ[FunctionK[Free[F, ?], Free[G, ?]]](f => f.mapK(fk)) + /** * a FunctionK, suitable for composition, which calls compile */ + @deprecated("Use mapK", "1.0.0") def compile[F[_], G[_]](fk: FunctionK[F, G]): FunctionK[Free[F, ?], Free[G, ?]] = - λ[FunctionK[Free[F, ?], Free[G, ?]]](f => f.compile(fk)) + mapK(fk) /** * a FunctionK, suitable for composition, which calls foldMap