Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Difference between flatMap, flatTap, evalMap and evalTap

In Scala fs2 library for functional streams:

I am trying to understand the difference between flatMap, flatTap, evalMap and evalTap. They all seem to perform the same thing, which is transformation of the stream values.

What is the difference and when each of them should be used?

like image 342
Lev Denisov Avatar asked Nov 12 '19 14:11

Lev Denisov


Video Answer


1 Answers

Traditionally, tap like functions allow you to observe (or peek into) the elements in the stream, but discard the result of the observing effect. For example, in fs2 you can see the signature for evalTap is:

def evalTap[F2[x] >: F[x]](f: (O) ⇒ F2[_])(implicit arg0: Functor[F2]): Stream[F2, O]

Notice how f is a function from O => F2[_], meaning "you take an O value and return an effect type F2 for which a Functor exists", but it doesn't affect the return type of the stream, which is still O.

For example, in case we want to emit elements of the stream to the console, we can do:

import cats.effect.{ExitCode, IO, IOApp}
import cats.implicits._

object Test extends IOApp {
  override def run(args: List[String]): IO[ExitCode] = {
    fs2
      .Stream(1, 2, 3)
      .covary[IO]
      .evalTap(i => IO(println(i)))
      .map(_ + 1)
      .compile
      .drain
      .as(ExitCode.Success)
  }
}

This will yield 1 2 3.

You can see that we emit each element of the stream to the console using evalTap, where we have an effect of type IO[Unit], yet we can immediately map each such element in the next step of the pipeline as it didn't effect to result type of the stream.

I couldn't find flatTap but I think they're generally the same in fs2 (https://github.com/functional-streams-for-scala/fs2/issues/1177)

On the other hand, a function like flatMap does cause the return type of the stream to change. We can see the signature:

def flatMap[F2[x] >: F[x], O2](f: O => Stream[F2, O2]): Stream[F2, O2] =

Notice how unlike evalTap, the result of executing f is O2, which is also encoded in the return type. If we take the same example as above:

fs2
  .Stream(1, 2, 3)
  .covary[IO]
  .flatMap(i => fs2.Stream(IO(println(i))))
  .map(_ + 1)
  .compile
  .drain
  .as(ExitCode.Success)

This will no longer compile, as flatMap returns an Stream[IO, Unit], meaning that the execution of println and the fact that it returns Unit directly affects downstream combinators.

evalMap is an alias for a flatMap which allows you to omit the wrapping of the Stream type and is generally implemented in terms of flatMap:

def evalMap[F2[x] >: F[x], O2](f: O => F2[O2]): Stream[F2, O2] =
  flatMap(o => Stream.eval(f(o)))

Which is a bit more convenient to use.

like image 117
Yuval Itzchakov Avatar answered Oct 06 '22 00:10

Yuval Itzchakov