Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add mapK to transformers #1987

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion core/src/main/scala/cats/data/EitherK.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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))))
Expand Down Expand Up @@ -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
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is by accident?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's Emacs doing some extra housekeeping from having the file open. I'll revert it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also-- you're fast! This PR was up for just a few minutes and you were already on top of it 😄.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haha, yeah, sorry for nitpicking so quickly 😄

5 changes: 5 additions & 0 deletions core/src/main/scala/cats/data/EitherT.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)))

Expand Down
6 changes: 6 additions & 0 deletions core/src/main/scala/cats/data/Func.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
6 changes: 6 additions & 0 deletions core/src/main/scala/cats/data/IdT.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)))

Expand Down
7 changes: 7 additions & 0 deletions core/src/main/scala/cats/data/IndexedReaderWriterStateT.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
*/
Expand Down
7 changes: 7 additions & 0 deletions core/src/main/scala/cats/data/IndexedStateT.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 =>
Expand Down
9 changes: 8 additions & 1 deletion core/src/main/scala/cats/data/Kleisli.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)))

Expand Down Expand Up @@ -48,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)))
Expand Down
10 changes: 9 additions & 1 deletion core/src/main/scala/cats/data/Nested.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 6 additions & 0 deletions core/src/main/scala/cats/data/OneAnd.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
5 changes: 5 additions & 0 deletions core/src/main/scala/cats/data/OptionT.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)))

Expand Down
10 changes: 9 additions & 1 deletion core/src/main/scala/cats/data/Tuple2K.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 6 additions & 0 deletions core/src/main/scala/cats/data/WriterT.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)) }
Expand Down
9 changes: 8 additions & 1 deletion free/src/main/scala/cats/free/Coyoneda.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
34 changes: 27 additions & 7 deletions free/src/main/scala/cats/free/Free.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure if we want to deprecate this one. compile seems a meaningful alias.

final def compile[T[_]](f: FunctionK[S, T]): Free[T, A] = mapK(f)

/**
* Lift into `G` (typically a `EitherK`) given `InjectK`. Analogous
Expand All @@ -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(...)"
Expand Down Expand Up @@ -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, ?]] =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why FunctionK[F, G] here but F ~> G in most other places?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method is a copy-paste-rename from the original Free.compile. I'll go through and switch FunctionK to ~> in the files modified by this PR, for consistency.

λ[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
Expand Down
22 changes: 14 additions & 8 deletions free/src/main/scala/cats/free/FreeT.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,28 @@ 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)

/**
* 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)
Expand Down Expand Up @@ -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)
}

9 changes: 8 additions & 1 deletion free/src/main/scala/cats/free/Yoneda.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -48,4 +56,3 @@ object Yoneda {
def apply[B](f: A => B): F[B] = F.map(fa)(f)
}
}

4 changes: 2 additions & 2 deletions free/src/test/scala/cats/free/CoyonedaSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down
8 changes: 4 additions & 4 deletions free/src/test/scala/cats/free/FreeTSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down
7 changes: 7 additions & 0 deletions tests/src/test/scala/cats/tests/EitherTSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
7 changes: 7 additions & 0 deletions tests/src/test/scala/cats/tests/IdTSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
}
}

}
Loading