1. Introduction

Cats Effect is one of the most popular libraries in the Scala ecosystem. It provides a lot of powerful functions to transform the values within the Cats Effect types.

In this tutorial, we’ll look at the differences between four such functions – flatMap(), flatTap(), evalMap(), and evalTap().

2. Setup

Firstly, let’s add the Cats Effect dependency to the build.sbt to get started:

libraryDependencies += "org.typelevel" %% "cats-effect" % "3.3.13"

3. flatMap()

The function flatMap() is one of the most popular functions in Scala. flatMap() allows us to operate on the values within a type (such as IO, Resource, and so on) and apply a function to transform that value. The flatMap operation obeys the law of associativity.

Let’s understand it using an example with IO datatype:

val strIO = IO("This is a string IO")  
def countLetters(str: String): IO[Int] = IO(str.length())
val countIO: IO[Int] = strIO.flatMap(s => countLetters(s))

We invoke the flatMap() function on an IO[String] by passing a method that counts the number of letters in the input. As a result, the data type of the final result becomes IO[Int].

The flatMap() method returns the result of the final operation in the chain, in this case, the number of letters in the text.

4. flatTap()

The flatTap() method is very similar to flatMap(), except that it returns the same result on which the method is applied after executing the logic within the flatTap() method.

Let’s look at it with a simple example:

def countLettersWithPrint(str: String): IO[Unit] = IO {
    println("String length is "+str.length())
}
val flatTapIO: IO[String] = strIO.flatTap(countLettersWithPrint)

Here, the method countLettersWithPrint() returns IO[Unit] after printing the result. However, the flatTap() chain still returns the type as IO[String] from strIO.

Let’s change flatTap() to flatMap() in the same example. As a result, the expression’s return type will become IO[Unit] since flatMap() returns the result from the right side of the expression.

The flatTap() method is beneficial in situations like logging the result of an IO, without affecting the return type. Otherwise, we have to write boilerplate code to retain the expected result:

val flatTapIO_V2: IO[String] = strIO.map{r => 
  countLettersWithPrint(r)
  r
}

5. evalMap()

Resource handling is one of the most useful features of the Cats Effect library. The Resource data structure enforces the logic to open and close the resources after their usage.

The method evalMap() on a resource allows us to apply effectful transformation on the acquired resource while keeping all the resource contracts unchanged.

This means the acquire and release logic will always execute on the original resource.

Let’s look at an example. First, we write the logic to acquire and release a resource:

case class SimpleConnection(url: String)
def acquireResource(): IO[SimpleConnection] = {
    IO.println("Opening Simple Connection") >> IO(SimpleConnection("localhost:8000")) 
}
def releaseResource(con: SimpleConnection): IO[Unit] = {
    IO.println("Closing Simple Connection: "+con.url)
}

Next, we create and use the resource:

val resource: Resource[IO,SimpleConnection] = Resource.make(acquireResource)(releaseResource)
val simpleResourceData: IO[Unit] = resource.use(simpleConn => IO.println("Result using Simple Resource"))

This first acquires the resource, then uses it and closes it after the usage.

We can use evalMap() on the resource to transform the acquired resource into something else, but without breaking acquire and release rules:

case class ComplexConnection(url: String)
def transformConnection(con: SimpleConnection) = {
    IO {
      println("Transforming connection to complex")
      ComplexConnection(con.url)
    }
}
val modifiedResource: Resource[IO,ComplexConnection] = resource.evalMap(con => transformConnection(con))

Finally, we can use the modifiedResource:

val evalMapResult: IO[Unit] = modifiedResource.use(complex => IO.println("Using complex connection from evalMap to execute.."))

When we execute this IO in an IDE, we can see the results printed:

Opening Connection
Transforming connection to complex
Using complex connection from evalMap to execute..
Closing Simple Connection: localhost:8000

6. evalTap()

The method evalTap() applies the effectful transformation just like evalMap(). However, unlike evalMap(), evalTap() keeps the return type as the left side of the expression.
We can change the evalMap() example to evalTap() to understand the difference:
val tappedResource: Resource[IO,SimpleConnection] = resource.evalTap(con => transformConnection(con))

Since we applied evalTap(), the type of resource continues to be SimpleConnection.

We can execute the tappedResource:

val result: IO[Unit] = tappedResource.use(c => IO.println("Using complex connection to execute.."))

Let’s look at the results printed:

Opening Simple Connection
Transforming connection to complex
Using simple connection from evalTap to execute..
Closing Simple Connection: localhost:8000

7. Function Summary

Let’s look at a summary of all the methods. The table below shows the expression and return type for each method:

Expression Return Type
IO[A] flatMap IO[B] IO[B]
IO[A] flatTap IO[B] IO[A]
Resource[IO,A] evalMap Resource[IO, B] Resource[IO, B]
Resource[IO,A] evalTap Resource[IO, B] Resource[IO, A]

8. Conclusion

In this article, we looked at the differences between four commonly used functions in Cats Effect.

As always, the sample code used here is available over on GitHub.

 

Comments are closed on this article!