Saturday, February 23, 2008

ARM Blocks in Scala, Part 3: The Concession

Update: Here's a better approach to Automatic Resource Management in Scala.

After a couple of attempts (part 1, part 2) at implementing Automatic Resource Management in Scala, I've decided not to "reinvent the wheel" here. I will defer instead to the implementation found in Scalax. I did not know about their ManagedResource class before my first post (many thanks to the commenters who pointed me there), and if I had known about it I may not have made the attempt. I'm glad I did, however, because it gave me a chance to improve my Scala skills. I'm not "there" yet, though, as I still find myself writing Java-like code in Scala. When I catch myself doing so, I am usually able to refactor it into the Scala style, which ends up being more compact and elegant. Come to think of it, that is why I recommend ManagedResource over my approach. I would say mine is the Java-like approach, while ManagedResource is more elegant and more consistent with Scala style.

Let's look at the same example from my first two posts using ManagedResource:


def createReader = ManagedResource(new BufferedReader(new FileReader("test.txt")))
def createWriter = ManagedResource(new BufferedWriter(new FileWriter("test_copy.txt")))

//copy a file, line by line
for(reader <- createReader; writer <- createWriter) {
var line = reader.readLine
while (line != null) {
writer.write(line)
writer.newLine
line = reader.readLine
}
}



Why is ManagedResource better?
It may be clear from the example, but let's discuss what it is that makes the usage of ManagedResource cleaner than the previous approach. ManagedResource uses for-comprehensions, and that alone solves many of the problems I encountered. For example, I had the problem of being able to define and initialize a resource and still being able to reference it inside of the block of code. For that reason, I had to define an initialization function (in part 2), but "for" takes care of this nicely: for(a <- ManagedResource(new SomeResource())) .... It also takes care of the many-resources problem elegantly, without using varargs: for(a <- createA; b <- createB; ...). In short, "for" seems like the right tool for the job.

That being said, I think ManagedResource does have some room for improvement. For example, consider the following code segment:


def createReader = ManagedResource(new BufferedReader(new FileReader("test.txt")))
def createWriter = ManagedResource(new BufferedWriter(new FileWriter("test_copy.txt")))

try {
//copy a file, line by line
for(reader <- createReader; writer <- createWriter) {
try {
var line = reader.readLine
while (line != null) {
writer.write(line)
writer.newLine
line = reader.readLine
}
} catch {
case e: IOException => println("Exception thrown while copying: " + e.getMessage)
}
}
} catch {
case e: IOException => println("Exception thrown upon open or close: " + e.getMessage)
}


As you can see, we have total control inside of the block of code, but if an exception occurs while initializing or disposing we have no way of knowing which of the two steps was the culprit. This is probably not an issue most of the time, but I could see a possible need for handling exceptions in initialization differently from exceptions in disposal. Maybe this can be improved (it would have to be non-intrusive for the more common, general case), or maybe some level of control must be sacrificed.

Also, at the time of writing, ManagedResource exposes methods for opening (initializing) and closing (disposing) a resource: "unsafeOpen" and "unsafeClose". These cannot be called from within the block of a for-comprehension, but I see no need to make them public, "protected[control]" should be the maximum visibility - if that. Making them public is a mistake because it allows for the same type of resource leaks we set out to quash. In fact, anyone who is calling these methods externally requires more control over where and when resources are initialized and disposed, and should not be using ManagedResource to begin with. If there is a legitimate reason for exposing these methods, I would like to see it.

Overall, however, I have to congratulate Scalax (Jamie Webb in particular) for getting it right. Scalax is still in a very early stage, so maybe the concerns addressed here will be addressed by the time it is ready for release. These concerns are relatively minor anyway, so I recommend using ManagedResource as is.

2 comments:

  1. I don't think using the for comprehension should be the Scala way of doing this. It's like using interfaces to get static imports in Java < 5.
    Coding Exercises: 2 or more, use a for

    ReplyDelete
  2. I agree that it feels unnatural to the Java/C programmer (like me), but in this case I think it makes sense to leverage the language features of for-comprehensions.

    As an alternative, it is pretty easy to duplicate the syntax of C#'s "using" (it's very similar to my first post), but then multiple resources have to be nested. For example:

    def reader = new BufferedReader(...
    def writer = new BufferedWriter(...

    using (reader) {
    using (writer) {
    ...
    }
    }


    This is the best alternative I've seen to for-comprehensions, however.

    Thanks for linking me in your post.

    ReplyDelete