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?
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With