DEV Community

Yawar Amin
Yawar Amin

Posted on • Edited on

Scala for-comprehensions for resource management

JAVA's 'try-with-resources' statement is its idiomatic answer to the question of safe resource management, as borrow checking is in Rust and the defer keyword is in Go. For example:

static String readFirstLineFromFile(String path) throws IOException {
  try (
    FileReader fr = new FileReader(path);
    BufferedReader br = new BufferedReader(fr)
  ) {
      return br.readLine();
  }
}
Enter fullscreen mode Exit fullscreen mode

Scala doesn't have language-level support for resource management, it just relies on its standard library, providing a Using utility object. Ported over, the previous example would look like:

import java.io.{BufferedReader,FileReader}
import scala.util.{Try,Using}

def readFirstLineFromFile(path: String): Try[String] = Using.Manager { use =>
  val fr = use(new FileReader(path))
  val br = use(new BufferedReader(fr))

  br.readLine()
}
Enter fullscreen mode Exit fullscreen mode

Unfortunately, this is pretty clunky. The main problem is that it doesn't provide any type-level support for controlling the resource access. We can easily forget to wrap it in the use resource manager and create a leak.

Fortunately, we can do better. We can write a helper that can be used in for-comprehensions that specifically manage AutoCloseable resources. Usage looks like:

def readFirstLineFromFile(path: String): String =
  for
    fr <- AutoClose(new FileReader(path))
    br <- AutoClose(new BufferedReader(fr))
  do
    br.readLine()
Enter fullscreen mode Exit fullscreen mode

Now, both the acquired resources are strongly typed and we cannot forget to use the AutoClose wrapper. This ensures that they must be closed properly at the end of the scope (in reverse order of opening, just like in Java).

Notice that this looks structurally similar to the Java try-with-resources example. There's a good reason for that–for-comprehensions generalize the idea of binding variables in scope while doing certain things automatically behind the scenes. Which is exactly what 'try-with-resources' is doing but with a special syntax just for this use-case.

Here's the implementation:

// AutoClose.scala

package util

class AutoClose[R <: AutoCloseable] private (val acquire: () => R) extends AnyVal:
Enter fullscreen mode Exit fullscreen mode

The key concept is that the resource being auto-closed must be a subtype of the standard Java AutoCloseable interface. This will include everything that will work with Java's 'try-with-resources'.

  import AutoClose.apply

  def foreach[U](f: R => U): U =
    val resource = acquire()
    try f(resource) finally Option(resource).foreach(_.close())
Enter fullscreen mode Exit fullscreen mode

We implement foreach in a slightly different way than the normal which is to give it a return type of Unit. With a generic return type U it becomes possible to use it in a much more general way, e.g. above where we return the first line of the read file, and also to implement the rest of the operations.

Of course, the key here is to use Scala's 'try-finally' to ensure that the resource is safely closed despite any exceptions that may arise. Note the use of Option(...).foreach(_.close()) to handle the case of the acquired resource being null. We sometimes get null from Java I/O APIs!

  def flatMap[R2 <: AutoCloseable](f: R => AutoClose[R2]): AutoClose[R2] =
    foreach(f)

  def map[R2 <: AutoCloseable](f: R => R2): AutoClose[R2] = apply(foreach(f))
Enter fullscreen mode Exit fullscreen mode

Together, these operations provide support for using AutoClose in for-comprehensions.

object AutoClose:
  def apply[R <: AutoCloseable](resource: => R): AutoClose[R] =
    new AutoClose(() => resource)
Enter fullscreen mode Exit fullscreen mode

And finally, the helper smart constructor in the companion object.

This is a powerful utility to safely handle JVM resources. And the nice thing about it is how easy it is to adopt into the code. Say you're using Apache HttpClient to do web requests:

val response = httpClient.execute(request)
// do stuff with `response`

// maybe remember to close the `response`
Enter fullscreen mode Exit fullscreen mode

We should really ensure there's no resource leak by auto-closing the response after we finish using it:

import util.AutoClose

for
  response <- AutoClose(httpClient.execute(request))
do
  // stuff with `response`

// after the for-comprehension scope, the `response` is automatically closed
Enter fullscreen mode Exit fullscreen mode

On a side note, there are not many tools out there that can statically analyze a Scala codebase and find potential resource leaks. Rust's borrow checking combined with its automatic resource management still has the edge here. In theory there's nothing stopping people from creating resource-safe I/O APIs that enforce the resources are disposed properly. But for now, some manual work will be required.

EDIT: amazingly, I came up with almost the exact same code four years ago: https://www.reddit.com/r/scala/comments/a7047a/a_short_story_about_resource_handling/ebzxsij/

Top comments (0)