1. Introduction

In programming, a resource refers to something that has limited availability such as files, database connections, sockets, and so on. After we use a resource, we need to properly release it to avoid resource leakage. In every programming language, much emphasis is given to handling resources properly. However, developers often do not properly handle the resources, leading to memory leakage or performance degradation. In this tutorial, we’ll look at how Cats Effect ensures that the resources are handled properly and avoids the many issues we may run into with standard Java and Scala resource handling.

2. Standard Way of Resource Handling

In Java and Scala, the normal way to handle a resource is either by using try-catch-finally or try-with-resources. The Using pattern in Scala 2.13 reduces the possibilities of mistakes related to resource handling. However, it still isn’t convenient to use in a fully functional style. Moreover, it’s not easy to use composability with this approach. In general, there are three main steps involved in handling a resource:

  • Acquiring the resource – for example, opening a file or a database connection
  • Using the resource – for example, reading the content of the file, or executing database queries
  • Closing the resource – for example, closing the file or returning the connection to the pool

Cats Effect provides a very safe and composable way to handle the resources. Let’s look at different approaches to resource handling using Cats Effect 3.

3. Setup

To start, let’s add the library dependency of Cats Effect 3 to our build.sbt:

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

4. Resource Handling Using Bracket Pattern

Cats Effect provides a nice way to handle resources using the bracket method. This method follows the same three main steps we outlined above. Let’s say we want to read the content from a text file. First, we’ll create three different methods for each step. All three methods will be wrapping the response in Cats Effect IO Monad. We can implement the method to open a file as:

def sourceIO: IO[Source] = IO(Source.fromResource("sample.txt"))

Once we have the source, we can use it to read the file content:

def readLines(source: Source): IO[String] = IO(source.getLines().mkString("\n"))

Now that we’ve read the file content, we can implement the third step to close the resource:

def closeFile(source: Source): IO[Unit] = IO(source.close())

Finally, we can combine the above-created methods together using the bracket pattern. We invoke the bracket method on resource acquisition and pass the methods to read and close the resource as parameters to it. We can define the bracket method using pseudo-code as:

acquireIO.bracket(usageIO)(releaseIO)

This way, Cats Effect forces us to provide the implementation to release the resources along with the usage. This will ensure that the resources are not leaked accidentally. Let’s compose our sample methods together:

val bracketRead: IO[String] =
  sourceIO.bracket(src => readLines(src))(src => closeFile(src))

5. Disadvantages of Bracket Pattern

The bracket pattern is good when we only have one level of resources. If we need to handle nested levels of resources, then this will get more complex and unreadable. Let’s assume that we need to write the file contents into another file using the same bracket pattern. Let’s create the required methods in the same way we did before:

def writeLines(writer: FileWriter, content: String): IO[Unit] =
    IO.println("Writing the contents to file") >> IO(writer.write(content))
def closeWriteFile(writer: FileWriter): IO[Unit] =
    IO.println("Closing the file writer") >> IO(writer.close())
val bracketRead: IO[String] =
    sourceIO.bracket(src => readLines(src))(src => closeFile(src))

Now, we can combine both the brackets together to read from one file and write to another file:

val bracketReadWrite = sourceIO.bracket { src =>
    val contentsIO = readLines(src)
    writerIO.bracket(fw =>
      contentsIO.flatMap(contents => writeLines(fw, contents))
    )(fw => closeWriteFile(fw))
} { src =>
    closeFile(src)
}

Cats Effect runtime will release the resources in the reverse order of acquisition. However, when we need to use nested brackets, the code becomes less readable and tightly coupled between each layer.

6. Resource Handling Using Resource

Cats Effect provides another way to handle resources that avoids the caveats of the bracket pattern, using Cats Effect’s Resource. The resource pattern also follows the same three-step approach of acquire-use-release. However, this pattern clearly separates the acquire-release part from the usage part. Let’s rewrite the first scenario of reading a file using the resource pattern:

val makeResourceForRead: Resource[IO,Source] = Resource.make(sourceIO)(src => closeFile(src))
val readWithResource: IO[String] = makeResourceForRead.use(src => readLines(src))

Here, the acquire and release parts are tied together using the make() method, which will return a Resource. We can then use this resource using use(). This way, the handling and usage of the resource are separated cleanly. Now, let’s rewrite the complex bracket pattern using the resource pattern:

val makeResourceForWrite: Resource[IO, FileWriter] = Resource.make(writerIO)(fw => closeWriteFile(fw))
val readWriteWithResource: IO[Unit] = for {
    content <- readWithResource
    _ <- makeResourceForWrite.use(fw => writeLines(fw, content))
} yield ()

Here, we can easily combine the read and write resources using a for-comprehension. This makes it much easier to read and understand. Whenever we invoke the use method, the Cats Effect will automatically acquire resources and release them on completion. However, this means that if the use method is invoked multiple times, the acquire and release operations also will be done multiple times.

7. Conclusion

In this tutorial, we looked at different ways to handle resources in Cats Effect. As always, the sample code used in this article is available over on GitHub.

Comments are closed on this article!