A Simple Try-With-Resources Construct in Scala
Check out how to implement try-with-resources in Scala, then take a lesson in generic programming using the Shapeless library.
Join the DZone community and get the full member experience.
Join For FreeYou may have noticed that Scala is lacking the try-with-resources
construct that Java has. This construct allows us to automatically invoke the close
method of an AutoCloseable
resource at the end of a code block that uses it. The interface is very broadly used in lots of classes of the standard Java library and many third-party libs as well.
This structure is really handy because it allows us to avoid human errors caused by inattentiveness. Calling the close
method after the resource has been used can be easily missed or forgotten if done manually every time, but the consequences of missing it can sometimes be severe.
Luckily, it is easy to implement an analogous — and even more powerful — construct in Scala.
Here is how it looks:
def loan[A <: AutoCloseable, B](resource: A)(block: A => B): B = {
Try(block(resource)) match {
case Success(result) =>
resource.close()
result
case Failure(e) =>
resource.close()
throw e
}
}
This is a simple curried function with two arguments. The first argument is the resource of type A that has to implement Autocloseable
(A <: AutoCloseable
). The second argument, a function from A to B, is the actual code that we want to execute that uses the resource.
Here is how the usage looks:
// Lets define our on AutoCloseable implementation for testing
case class ResourceA(it: String) extends AutoCloseable {
override def close(): Unit = println("closing a")
}
val result:
String = loan(ResourceA("123")) {
resource =>
print(s "${resource.it}")
resource.it
}
result // "123"
When being run, it outputs 123
and then closing a
, affirming that it has closed the resource as we expected.
Notably, loan
is also an expression that means we can assign a value it returns to a variable.
If we want to get hold of several resources, we can nest such calls in the following fashion:
// another AutoCloseable implementation for testing
case class ResourceA(it: String) extends AutoCloseable {
override def close(): Unit = println("closing a")
}
val result = loan(ResourceA("123")) {
r1 =>
loan(ResourceB("456")) {
r2 =>
// both ResourceA and ResourceB in this scope
r1.it + r2.it // returns "123456"
}
}
It looks fine, though nesting three or more such resources can be tedious and won’t look nice (although admittedly, the possibility of requiring it is relatively rare).
So here comes the next logical step…
Arbitrary Number of Arguments Using Shapeless
Let's go one step further. Suppose you want to avoid nesting and be able to call it this way:
val r1 = ResourceA("123")
val r2 = ResourceB("456")
val r3 = ResourceC("456")
loan(r1, r2, r3) { case (a, b, c) =>
// piece of code that uses all 3 arguments
}
We can, of course, overload our function to accept two arguments, and another one for three and so on, but there is no fun in that!
Let’s have an exercise in generic programming with Shapeless instead (but don’t confuse it with Java generics).
Shapeless is a popular library for generic programming that used in many Scala libraries. Sometimes, it can almost do miracles from a library user’s perspective.
A very common pattern it uses is to state the dependencies we need and dependencies between them via implicit arguments. When compiling, the compiler searches for a suitable combination of these parameters, or infers them. It compiles fine when the search is successful and fails otherwise.
We need to add Shapeless to your build.sbt
file (assuming that we use SBT as a build tool).
resolvers++ = Seq(
Resolver.sonatypeRepo("releases"),
Resolver.sonatypeRepo("snapshots")
)
libraryDependencies += "com.chuusai" % "shapeless" % "2.3.2"
And here is how the code looks:
import shapeless._, ops.hlist._, ops.nat._
import scala.util. {
Failure,
Success,
Try
}
object Loan {
def apply[A <: AutoCloseable, B](resource: A)(block: A => B): B = {
Try(block(resource)) match {
case Success(result) =>
resource.close()
result
case Failure(e) =>
resource.close()
throw e
}
}
def apply[Tup, L <: HList, Len <: Nat, B](resources: Tup)(block: Tup => B)(
implicit gen: Generic.Aux[Tup, L],
con: ToList[L, AutoCloseable],
length: Length.Aux[L, Len],
gt: GT[Len, nat._1]
): B = {
Try(block(resources)) match {
case Success(result) =>
gen.to(resources).toList.foreach {
_.close()
}
result
case Failure(e) =>
gen.to(resources).toList.foreach {
_.close()
}
throw e
}
}
}
For this case, with only one element, the code remains the same. So, let's skip to the interesting part.
For first implicit argument, gen: Generic.Aux[Tup, L]
, we enforce a requirement for it to have an HList (a heterogeneous list that stores the types of elements it has in compile time) representation to be available somewhere in the scope. Think of L
as the output type of the HList for such conditions.
But let's elaborate on this a bit more. Aux is a well-established pattern in Shapeless. For Generic
, the definition looks like this: type Aux[T, Repr0] = Generic[T] { type Repr = Repr0 }
. It is designed to avoid the following problem. To express a dependency between implicit arguments, we may want to write something like this:
implicit gen: Generic[Tup], con: ToList[gen.Repr, AutoCloseable] ...
And use the output type of the generic with a gen.Repr
reference. But unfortunately, it won’t compile because we can’t refer to a type member of one parameter from another parameter in the same block.
The next three conditions we enforce on the HList L
are that all its elements should be implementing AutoCloseable
, and it should contain more than 1 element (because the one element method def apply<a href="resource: A">A <: AutoCloseable, B</a>(block: A => B)
handles a single element case):
con: ToList[L, AutoCloseable],
length: Length.Aux[L, Len],
gt: GT[Len, nat._1]
object Example extends App {
case class ResourceA(it: String) extends AutoCloseable {
override def close(): Unit = println("closing a")
}
case class ResourceB(it: String) extends AutoCloseable {
override def close(): Unit = println("closing b")
}
Loan(ResourceA("123"), ResourceB("456")) {
case (a, b) =>
println(a.it)
println(b.it)
}
Loan(ResourceA("123"), ResourceB("456"))
looks like a regular function call with two arguments, but we are actually passing a tuple here — basically a syntactic sugar for Loan((ResourceA("123"), ResourceB("456")))
. Another convenient thing about tuples in Scala is that they are case classes and can be easily represented as Shapeless HLists via the Generic to
method without much effort.
Recap
So we’ve shown how easy it is to implement try-with-resources in Scala — and an even more powerful solution with some Shapeless magic.
I also suggest you read this great guide about Shapeless and generic programming.
Opinions expressed by DZone contributors are their own.
Comments