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();
}
}
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()
}
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()
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:
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())
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))
Together, these operations provide support for using AutoClose
in for-comprehensions.
object AutoClose:
def apply[R <: AutoCloseable](resource: => R): AutoClose[R] =
new AutoClose(() => resource)
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`
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
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)