Skip to content

Commit

Permalink
Added more implementations of map2Eval (#1819)
Browse files Browse the repository at this point in the history
* Added more implementations of map2Eval

I hit a case where I wanted laziness with map2Eval and realized in many
cases where we could have it, we do not. There were all the cases I saw
that we missed, which I hope our existing tests cover.

* Add tests for map2 and map2Eval

* remove incorrect instances
  • Loading branch information
johnynek authored Aug 17, 2017
1 parent 80a9853 commit 68a8666
Show file tree
Hide file tree
Showing 10 changed files with 53 additions and 3 deletions.
1 change: 1 addition & 0 deletions core/src/main/scala/cats/data/EitherT.scala
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,7 @@ private[data] trait EitherTFunctor[F[_], L] extends Functor[EitherT[F, L, ?]] {
private[data] trait EitherTMonad[F[_], L] extends Monad[EitherT[F, L, ?]] with EitherTFunctor[F, L] {
implicit val F: Monad[F]
def pure[A](a: A): EitherT[F, L, A] = EitherT.pure(a)

def flatMap[A, B](fa: EitherT[F, L, A])(f: A => EitherT[F, L, B]): EitherT[F, L, B] = fa flatMap f
def tailRecM[A, B](a: A)(f: A => EitherT[F, L, Either[A, B]]): EitherT[F, L, B] =
EitherT(F.tailRecM(a)(a0 => F.map(f(a0).value) {
Expand Down
4 changes: 4 additions & 0 deletions core/src/main/scala/cats/data/IdT.scala
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ private[data] sealed trait IdTApply[F[_]] extends Apply[IdT[F, ?]] with IdTFunct
implicit val F0: Apply[F]

override def ap[A, B](ff: IdT[F, A => B])(fa: IdT[F, A]): IdT[F, B] = fa.ap(ff)

override def map2Eval[A, B, Z](fa: IdT[F, A], fb: Eval[IdT[F, B]])(f: (A, B) => Z): Eval[IdT[F, Z]] =
F0.map2Eval(fa.value, fb.map(_.value))(f) // if F0 has a lazy map2Eval, leverage it
.map(IdT(_))
}

private[data] sealed trait IdTApplicative[F[_]] extends Applicative[IdT[F, ?]] with IdTApply[F] {
Expand Down
6 changes: 6 additions & 0 deletions core/src/main/scala/cats/data/Ior.scala
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,12 @@ private[data] sealed abstract class IorInstances extends IorInstances0 {

def flatMap[B, C](fa: Ior[A, B])(f: B => Ior[A, C]): Ior[A, C] = fa.flatMap(f)

override def map2Eval[B, C, Z](fa: Ior[A, B], fb: Eval[Ior[A, C]])(f: (B, C) => Z): Eval[Ior[A, Z]] =
fa match {
case l @ Ior.Left(_) => Eval.now(l) // no need to evaluate fb
case notLeft => fb.map(fb => map2(notLeft, fb)(f))
}

def tailRecM[B, C](b: B)(fn: B => Ior[A, Either[B, C]]): A Ior C = {
@tailrec
def loop(v: Ior[A, Either[B, C]]): A Ior C = v match {
Expand Down
7 changes: 7 additions & 0 deletions core/src/main/scala/cats/data/Tuple2K.scala
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,13 @@ private[data] sealed trait Tuple2KApply[F[_], G[_]] extends Apply[λ[α => Tuple
Tuple2K(F.ap(f.first)(fa.first), G.ap(f.second)(fa.second))
override def product[A, B](fa: Tuple2K[F, G, A], fb: Tuple2K[F, G, B]): Tuple2K[F, G, (A, B)] =
Tuple2K(F.product(fa.first, fb.first), G.product(fa.second, fb.second))
override def map2Eval[A, B, Z](fa: Tuple2K[F, G, A], fb: Eval[Tuple2K[F, G, B]])(f: (A, B) => Z): Eval[Tuple2K[F, G, Z]] = {
val fbmemo = fb.memoize // don't recompute this twice internally
for {
fz <- F.map2Eval(fa.first, fbmemo.map(_.first))(f)
gz <- G.map2Eval(fa.second, fbmemo.map(_.second))(f)
} yield Tuple2K(fz, gz)
}
}

private[data] sealed trait Tuple2KApplicative[F[_], G[_]] extends Applicative[λ[α => Tuple2K[F, G, α]]] with Tuple2KApply[F, G] {
Expand Down
5 changes: 5 additions & 0 deletions core/src/main/scala/cats/data/WriterT.scala
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,11 @@ private[data] sealed trait WriterTApply[F[_], L] extends WriterTFunctor[F, L] wi

def ap[A, B](f: WriterT[F, L, A => B])(fa: WriterT[F, L, A]): WriterT[F, L, B] =
fa ap f

override def map2Eval[A, B, Z](fa: WriterT[F, L, A], fb: Eval[WriterT[F, L, B]])(f: (A, B) => Z): Eval[WriterT[F, L, Z]] =
F0.map2Eval(fa.run, fb.map(_.run)) { case ((la, a), (lb, b)) => (L0.combine(la, lb), f(a, b)) }
.map(WriterT(_)) // F0 may have a lazy map2Eval

override def product[A, B](fa: WriterT[F, L, A], fb: WriterT[F, L, B]): WriterT[F, L, (A, B)] =
WriterT(F0.map(F0.product(fa.run, fb.run)) { case ((l1, a), (l2, b)) => (L0.combine(l1, l2), (a, b)) })
}
Expand Down
7 changes: 6 additions & 1 deletion core/src/main/scala/cats/instances/list.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@ trait ListInstances extends cats.kernel.instances.ListInstances {
fa.flatMap(f)

override def map2[A, B, Z](fa: List[A], fb: List[B])(f: (A, B) => Z): List[Z] =
fa.flatMap(a => fb.map(b => f(a, b)))
if (fb.isEmpty) Nil // do O(1) work if fb is empty
else fa.flatMap(a => fb.map(b => f(a, b))) // already O(1) if fa is empty

override def map2Eval[A, B, Z](fa: List[A], fb: Eval[List[B]])(f: (A, B) => Z): Eval[List[Z]] =
if (fa.isEmpty) Eval.now(Nil) // no need to evaluate fb
else fb.map(fb => map2(fa, fb)(f))

def tailRecM[A, B](a: A)(f: A => List[Either[A, B]]): List[B] = {
val buf = List.newBuilder[B]
Expand Down
7 changes: 6 additions & 1 deletion core/src/main/scala/cats/instances/map.scala
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,12 @@ trait MapInstances extends cats.kernel.instances.MapInstances {
fa.map { case (k, a) => (k, f(a)) }

override def map2[A, B, Z](fa: Map[K, A], fb: Map[K, B])(f: (A, B) => Z): Map[K, Z] =
fa.flatMap { case (k, a) => fb.get(k).map(b => (k, f(a, b))) }
if (fb.isEmpty) Map.empty // do O(1) work if fb is empty
else fa.flatMap { case (k, a) => fb.get(k).map(b => (k, f(a, b))) }

override def map2Eval[A, B, Z](fa: Map[K, A], fb: Eval[Map[K, B]])(f: (A, B) => Z): Eval[Map[K, Z]] =
if (fa.isEmpty) Eval.now(Map.empty) // no need to evaluate fb
else fb.map(fb => map2(fa, fb)(f))

override def ap[A, B](ff: Map[K, A => B])(fa: Map[K, A]): Map[K, B] =
fa.flatMap { case (k, a) => ff.get(k).map(f => (k, f(a))) }
Expand Down
8 changes: 8 additions & 0 deletions core/src/main/scala/cats/instances/stream.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ trait StreamInstances extends cats.kernel.instances.StreamInstances {
def flatMap[A, B](fa: Stream[A])(f: A => Stream[B]): Stream[B] =
fa.flatMap(f)

override def map2[A, B, Z](fa: Stream[A], fb: Stream[B])(f: (A, B) => Z): Stream[Z] =
if (fb.isEmpty) Stream.empty // do O(1) work if fb is empty
else fa.flatMap(a => fb.map(b => f(a, b))) // already O(1) if fa is empty

override def map2Eval[A, B, Z](fa: Stream[A], fb: Eval[Stream[B]])(f: (A, B) => Z): Eval[Stream[Z]] =
if (fa.isEmpty) Eval.now(Stream.empty) // no need to evaluate fb
else fb.map(fb => map2(fa, fb)(f))

def coflatMap[A, B](fa: Stream[A])(f: Stream[A] => B): Stream[B] =
fa.tails.toStream.init.map(f)

Expand Down
6 changes: 6 additions & 0 deletions laws/src/main/scala/cats/laws/ApplyLaws.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ trait ApplyLaws[F[_]] extends FunctorLaws[F] with CartesianLaws[F] {
val compose: (B => C) => (A => B) => (A => C) = _.compose
fbc.ap(fab.ap(fa)) <-> fbc.map(compose).ap(fab).ap(fa)
}

def map2ProductConsistency[A, B, C](fa: F[A], fb: F[B], f: (A, B) => C): IsEq[F[C]] =
F.map(F.product(fa, fb)) { case (a, b) => f(a, b) } <-> F.map2(fa, fb)(f)

def map2EvalConsistency[A, B, C](fa: F[A], fb: F[B], f: (A, B) => C): IsEq[F[C]] =
F.map2(fa, fb)(f) <-> (F.map2Eval(fa, Eval.now(fb))(f).value)
}

object ApplyLaws {
Expand Down
5 changes: 4 additions & 1 deletion laws/src/main/scala/cats/laws/discipline/ApplyTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ trait ApplyTests[F[_]] extends FunctorTests[F] with CartesianTests[F] {
val name = "apply"
val parents = Seq(functor[A, B, C], cartesian[A, B, C])
val bases = Seq.empty
val props = Seq("apply composition" -> forAll(laws.applyComposition[A, B, C] _))
val props = Seq(
"apply composition" -> forAll(laws.applyComposition[A, B, C] _),
"map2/product-map consistency" -> forAll(laws.map2ProductConsistency[A, B, C] _),
"map2/map2Eval consistency" -> forAll(laws.map2EvalConsistency[A, B, C] _))
}
}

Expand Down

0 comments on commit 68a8666

Please sign in to comment.