4
\$\begingroup\$

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 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) = {
    val res = new FileInputStream(infilename)
    try {
      f(res)
    } finally {
      res.close()
    }
  }
}

// Opens a FileOutputStream and loans it to the callback. After that, closes the stream.
def withOutputStream(outfilename: String) = new ForComprehensible[FileOutputStream] {
  override def apply[B](f: FileOutputStream => B) = {
    val res = new FileOutputStream(outfilename)
    try {
      f(res)
    } finally {
      res.close()
    }
  }
}

// Pipes all the content of the input stream into the output stream.
@tailrec def pipe(is: InputStream, os: OutputStream) : Unit = {
  val bytes = new Array[Byte](1024)
  val n = is.read(bytes)
  if (n >= 0) {
    os.write(bytes, 0, n)
    pipe(is, os)
  }
}

// The following are equivalent. Both automatically close streams.
// Without for-comprehension:
withInputStream("input.txt"){ is =>
  withOutputStream("output.txt"){ os =>
    pipe(is, os)
  }
}

// With for-comprehension:
for {
  is <- withInputStream("input.txt")
  os <- withOutputStream("output.txt")
} {
  pipe(is, os)
}

It works and looks nice. Before writing this, I googled for "scala loan pattern for-comprehension" and found nothing looking relevant. I think this trait is quite straight-forward and I doubt no one ever come up with something like this.

So I've got skeptical; is there any problem with this?

\$\endgroup\$
1
\$\begingroup\$

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 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 <- withInputStream("input.txt") if is.available() > 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 <- withInputStream("input.txt") if is.available() > 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.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.