Skip to content

Commit

Permalink
Finish adding mapK (#2000)
Browse files Browse the repository at this point in the history
* Add mapK to most (hopefully all) transformers

* Add mapK to free related constructs

* Deprecate Kleisli#transform in favor of mapK

* Fix tests file name

* Add missing mapK methods

* Don't deprecate Free methods and use mapK for parallel kleisli instance
  • Loading branch information
LukaJCB authored Oct 31, 2017
1 parent f3981dd commit 94f4928
Show file tree
Hide file tree
Showing 25 changed files with 193 additions and 40 deletions.
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
}

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
13 changes: 10 additions & 3 deletions 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 Expand Up @@ -148,10 +155,10 @@ private[data] sealed abstract class KleisliInstances1 extends KleisliInstances2
def monad: Monad[Kleisli[M, A, ?]] = catsDataMonadForKleisli

def sequential: Kleisli[F, A, ?] ~> Kleisli[M, A, ?] =
λ[Kleisli[F, A, ?] ~> Kleisli[M, A, ?]](_.transform(P.sequential))
λ[Kleisli[F, A, ?] ~> Kleisli[M, A, ?]](_.mapK(P.sequential))

def parallel: Kleisli[M, A, ?] ~> Kleisli[F, A, ?] =
λ[Kleisli[M, A, ?] ~> Kleisli[F, A, ?]](_.transform(P.parallel))
λ[Kleisli[M, A, ?] ~> Kleisli[F, A, ?]](_.mapK(P.parallel))
}
}

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
32 changes: 25 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,8 @@ 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)
*/
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 +181,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 +229,17 @@ 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
*/
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
21 changes: 13 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,27 @@ 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]`.
*/
*/
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 +257,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

0 comments on commit 94f4928

Please sign in to comment.