0

Say I have 2 Functors f0 and f1 and I have some code that looks like —

f0.map(v0 => f1.map(v1 => f0f1(v0, v1)))

Is there a way simplify this such that I can use a for expression and make the code cleaner —

for { 
  v0 <- f0
  v1 <- f1
} yield f0f1(v0, v1)

The map function is available thru a syntactic sugar that looks like —

  implicit class FunctorOps[F[_], A](fa: F[A]) {
    def F = implicitly[Functor[F]]
    def map[B](ab: A => B): F[B] = F.map(fa)(ab)
  }
tusharmath
  • 10,622
  • 12
  • 56
  • 83
  • In my case its actually something like — `IO[Option[X]]` – tusharmath Sep 29 '18 at 13:53
  • I don't want to actually care about what the container of my value is, I want to apply transformations to what's inside – tusharmath Sep 29 '18 at 13:54
  • @LuisMiguelMejíaSuárez what are you all talking about? Where does `Future` come from, all of a sudden? Which implementation of `Functor` has a `map` which takes a single argument? They all take [two arguments](https://github.com/typelevel/cats/blob/master/core/src/main/scala/cats/Functor.scala), and the function usually comes second. Unclear what you're asking, please provide a [mcve]. – Andrey Tyukin Sep 29 '18 at 13:54
  • @AndreyTyukin I am using Cats because of which `map` is added as a syntactic sugar. – tusharmath Sep 29 '18 at 13:56
  • @Tushar then it's unclear what the syntactic sugar is added to. What exactly are `f0` and `f1`. They aren't instances of `Functor[F]`, as it seems. – Andrey Tyukin Sep 29 '18 at 13:57
  • @AndreyTyukin I have added the syntactic sugar that's automatically inserted by Cats – tusharmath Sep 29 '18 at 14:10
  • How are snippets from Cats supposed to clarify the types of `f0` and `f1` in your code? We know what the code in Cats looks like. We don't know what `f0` and `f1` in your code are. – Andrey Tyukin Sep 29 '18 at 14:14
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/180990/discussion-between-tushar-and-andrey-tyukin). – tusharmath Sep 29 '18 at 14:19

2 Answers2

2

Assuming that f0: F0[X] and f1: F1[Y], where both F0 and F1 have Functor instances, and f: (X, Y) => Z, the for-comprehension equivalent to

f0.map(x => f1.map(y => f(x, y)))

would be

for (x <- f0) yield for (y <- f1) yield f(x, y)

Example:

val f0 = Option(42)
val f1 = List(1, 2, 3)
for (x <- f0) yield for (y <- f1) yield x * y

produces:

res1: Option[List[Int]] = Some(List(42, 84, 126))

whereas

for (y <- f1) yield for (x <- f0) yield x * y

produces

res2: List[Option[Int]] = List(Some(42), Some(84), Some(126))

I'm not sure whether the for comprehension is much cleaner than the nested maps in this case.

Andrey Tyukin
  • 43,673
  • 4
  • 57
  • 93
0

I don't think what you are after is possible in principle.

The nice for syntax you are after translates to something like

f0.flatMap(v0 => (f1.map(v1 => f0f1(v0, v1))))

The problem here is the flatMap part. For there to be a flatMap you need a Monad, not only a Functor, or at least something with the FlatMap instance. But Monads, as opposed to Functors, don't compose. So there is no way to get a nested Monad automatically from F[_] and G[_], even if both of them are Monads. [1]

You might get the nice syntax using some of the monad transformers, but they don't (and can't) exist for all monads. [2]

For monads where there are transformers, something like what you want is possible:

val l: List[Int]      = List(1,2,3)
val o: Option[String] = Some("abc")

val ol: OptionT[List, String] = for {
  a <- OptionT.liftF(l)
  b <- OptionT.fromOption[List](o)
} yield a + b.toString

There are scala docs ([3]) if you are interested in OptionT.

Whether that's nicer or not lies in the eyes of the beholder.

That being sad, if you need this more often, you might want to write your own helper functions along the lines of

def combine2[F[_]: Functor, G[_]: Functor, A, B, C](
    fa: F[A],
    gb: G[B])(
    f: (A, B) => C
  ): F[G[C]] =
    fa.map(a => gb.map(b => (f(a, b))))

If you don't want to do this, one thing you can do is what @andrey-tyukin already mentioned. But I agree that it's probably best to just nest the calls to map.

Another thing you might want to look at is Nested [4]. It doesn't help you in this particular case, but it might help you reduce some of the nested maps down the road.

--

[1] http://blog.tmorris.net/posts/monads-do-not-compose/

[2] Why is there no IO transformer in Haskell?

[3] https://typelevel.org/cats/api/cats/data/OptionT.html

[4] https://typelevel.org/cats/api/cats/data/Nested.html

felher
  • 81
  • 3