patternMinor
A general-use For-comprehensible Loan Pattern
Viewed 0 times
generalforcomprehensibleloanusepattern
Problem
I use Scala's loan pattern in order to ensure and automate the resource aquisition and release after its use. When multiple resources have to be acquired at once, however, the function calls must be nested, impairing readability.
Scala ARM supports for-comprehension to avoid the nesting. However, in order to have Scala ARM manage resources, the managed class must extend scala.resource.Resource or simply AutoCloseable, which requires a definiton of
I'd like to avoid defining
My idea is to restrict resource access to only via certain loaner-functions, while supporting for-comprehension to keep readability.
So, I wrote a trait
Since
```
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.InputStream
import java.io.OutputStream
import scala.annotation.tailrec
// Opens a FileInputStream and loans it to the callback. After that, closes the stream.
def withInputStream(infilename: String) = new ForComprehensible[FileInputStream] {
override def apply[B](f: FileInputStream => B
Scala ARM supports for-comprehension to avoid the nesting. However, in order to have Scala ARM manage resources, the managed class must extend scala.resource.Resource or simply AutoCloseable, which requires a definiton of
close() method.I'd like to avoid defining
close() methods because it separates aquisition and release into different methods but I believe such operations are so tightly coupled that keeping them close will enhance the maintainability.My idea is to restrict resource access to only via certain loaner-functions, while supporting for-comprehension to keep readability.
So, I wrote a trait
ForComprehensible:/**
* @tparam A The type of the loaned object.
*/
trait ForComprehensible[+A] {
/**
* The loaner-function. Subclasses must define this.
*
* @tparam B The result type of the callback
* @param f The callback
* @return The result value of the callback
*/
def apply[B](f: A => B) : B
def map[B](f: A => B) : B = apply(f)
def flatMap[B](f: A => B) : B = apply(f)
def foreach[B](f: A => B) : Unit = { apply(f); () }
// The methods filter and withFilter are omitted on purpose because they don't make sense.
}Since
ForComprehensible defines the apply method, client code can use one much like an ordinary loaner-function. Here is a simple example program which copies a content of a file into another:```
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.InputStream
import java.io.OutputStream
import scala.annotation.tailrec
// Opens a FileInputStream and loans it to the callback. After that, closes the stream.
def withInputStream(infilename: String) = new ForComprehensible[FileInputStream] {
override def apply[B](f: FileInputStream => B
Solution
As long as you restrict yourself to simple uses, I think it's ok. When you try complex tricks, however, the lack of definition of the
Binding to a tuple
This doesn't work because assigning to a tuple (or more generally, a pattern) involves a call to
Guards
This doesn't work because an "if" guard is translated to a call to
The trait is still "safe" because both of the above are catched at compilation time. However, experienced Scala programmers may find it cumbersome when they write up a complex generator and then get errors.
Can we get them work? A naïve approach is to define
this enables binding to a tuple (for some reason I don't fully understand), but causes guards to be silently ignored (i.e. assumes true). This makes
If we are to implement
EDIT: after some more experiments, I found out that a guard on
Consider:
note the
It depends on the body of the for-expression. For example, if it loads the lines of the file into a
Guards, or filters, only make sense when they operate on a collection (
filter (or withFilter) method leads to certain inconveniences. Here are examples:Binding to a tuple
for {
// opens two files at once and returns a tuple of Input/OutputStreams
(is, os) <- withIOStream("input.txt", "output.txt") // (doesn't work)
} { ... }This doesn't work because assigning to a tuple (or more generally, a pattern) involves a call to
filter. If you want to use pattern matching, bind it to a variable first, and then match it against a tuple in the for expression.Guards
for {
// handle only if the file is not empty
is 0 // (doesn't work)
} { ... }This doesn't work because an "if" guard is translated to a call to
filter. Since the guard (usually) depends on the loaned object (in this example, is), such conditionals must be put into the body of the for expression.The trait is still "safe" because both of the above are catched at compilation time. However, experienced Scala programmers may find it cumbersome when they write up a complex generator and then get errors.
Can we get them work? A naïve approach is to define
filter and withFilter like:// unsafe, incorrect for certain uses
trait ForComprehensible[+A] {
...
// ignore the filtering and just return itself
def filter(f: A => Boolean): ForComprehensible[A] = this
// not essential here, just to suppress compiler warnings
def withFilter(f: A => Boolean) = filter(f)
}this enables binding to a tuple (for some reason I don't fully understand), but causes guards to be silently ignored (i.e. assumes true). This makes
ForComprehensible an unsafe construct.If we are to implement
filter (and withFilter) properly, we have to open a FileInputStream (in our example) in the filter method. It means the stream must be opened twice (in filter and apply), or we open it once in filter and then keep it open using some magic until the callback of apply returns. The former is simply unacceptable, and the latter would make the implementation of the trait too complicated.EDIT: after some more experiments, I found out that a guard on
ForComprehensible is not an implementation problem but rather a semantic problem.Consider:
for {
is 0
} yield { ... }note the
yield. What this whole expression has to return if the guard is.available() > 0 doesn't hold?It depends on the body of the for-expression. For example, if it loads the lines of the file into a
Seq[String], it would return Seq(). If it counts the number of lines in the file, it would return 0 (if the body returns Int) or None (Option[Int]).Guards, or filters, only make sense when they operate on a collection (
Option can be considered a kind of collection whose maximum size is 1). Since ForComprehensible is just a syntactic trick to enable a loaner-function to be in for-generators, I conclude for now that we should stick to for-body if we want to check some conditions for acquiring resources.Code Snippets
for {
// opens two files at once and returns a tuple of Input/OutputStreams
(is, os) <- withIOStream("input.txt", "output.txt") // (doesn't work)
} { ... }for {
// handle only if the file is not empty
is <- withInputStream("input.txt") if is.available() > 0 // (doesn't work)
} { ... }// unsafe, incorrect for certain uses
trait ForComprehensible[+A] {
...
// ignore the filtering and just return itself
def filter(f: A => Boolean): ForComprehensible[A] = this
// not essential here, just to suppress compiler warnings
def withFilter(f: A => Boolean) = filter(f)
}for {
is <- withInputStream("input.txt") if is.available() > 0
} yield { ... }Context
StackExchange Code Review Q#146640, answer score: 2
Revisions (0)
No revisions yet.