Scala 有哪些自动资源管理的替代方案?

我在 Scala 的网站上看到过许多 ARM (自动资源管理)的例子。虽然大多数看起来都很相似,但写这样一本书似乎是一个必经的过程。但是,我在 是的中看到了一个使用延续的非常酷的示例。

无论如何,很多代码都有这样或那样的缺陷,所以我认为在 Stack Overflow 这里有一个参考是个好主意,在这里我们可以投票选出最正确和最合适的版本。

21843 次浏览

I see a gradual 4 step evolution for doing ARM in Scala:

  1. No ARM: Dirt
  2. Only closures: Better, but multiple nested blocks
  3. Continuation Monad: Use For to flatten the nesting, but unnatural separation in 2 blocks
  4. Direct style continuations: Nirava, aha! This is also the most type-safe alternative: a resource outside withResource block will be type error.

Chris Hansen's blog entry 'ARM Blocks in Scala: Revisited' from 3/26/09 talks about about slide 21 of Martin Odersky's FOSDEM presentation. This next block is taken straight from slide 21 (with permission):

def using[T <: { def close() }]
(resource: T)
(block: T => Unit)
{
try {
block(resource)
} finally {
if (resource != null) resource.close()
}
}

--end quote--

Then we can call like this:

using(new BufferedReader(new FileReader("file"))) { r =>
var count = 0
while (r.readLine != null) count += 1
println(count)
}

What are the drawbacks of this approach? That pattern would seem to address 95% of where I would need automatic resource management...

Edit: added code snippet


Edit2: extending the design pattern - taking inspiration from python with statement and addressing:

  • statements to run before the block
  • re-throwing exception depending on the managed resource
  • handling two resources with one single using statement
  • resource-specific handling by providing an implicit conversion and a Managed class

This is with Scala 2.8.

trait Managed[T] {
def onEnter(): T
def onExit(t:Throwable = null): Unit
def attempt(block: => Unit): Unit = {
try { block } finally {}
}
}


def using[T <: Any](managed: Managed[T])(block: T => Unit) {
val resource = managed.onEnter()
var exception = false
try { block(resource) } catch  {
case t:Throwable => exception = true; managed.onExit(t)
} finally {
if (!exception) managed.onExit()
}
}


def using[T <: Any, U <: Any]
(managed1: Managed[T], managed2: Managed[U])
(block: T => U => Unit) {
using[T](managed1) { r =>
using[U](managed2) { s => block(r)(s) }
}
}


class ManagedOS(out:OutputStream) extends Managed[OutputStream] {
def onEnter(): OutputStream = out
def onExit(t:Throwable = null): Unit = {
attempt(out.close())
if (t != null) throw t
}
}
class ManagedIS(in:InputStream) extends Managed[InputStream] {
def onEnter(): InputStream = in
def onExit(t:Throwable = null): Unit = {
attempt(in.close())
if (t != null) throw t
}
}


implicit def os2managed(out:OutputStream): Managed[OutputStream] = {
return new ManagedOS(out)
}
implicit def is2managed(in:InputStream): Managed[InputStream] = {
return new ManagedIS(in)
}


def main(args:Array[String]): Unit = {
using(new FileInputStream("foo.txt"), new FileOutputStream("bar.txt")) {
in => out =>
Iterator continually { in.read() } takeWhile( _ != -1) foreach {
out.write(_)
}
}
}

Here's James Iry solution using continuations:

// standard using block definition
def using[X <: {def close()}, A](resource : X)(f : X => A) = {
try {
f(resource)
} finally {
resource.close()
}
}


// A DC version of 'using'
def resource[X <: {def close()}, B](res : X) = shift(using[X, B](res))


// some sugar for reset
def withResources[A, C](x : => A @cps[A, C]) = reset{x}

Here are the solutions with and without continuations for comparison:

def copyFileCPS = using(new BufferedReader(new FileReader("test.txt"))) {
reader => {
using(new BufferedWriter(new FileWriter("test_copy.txt"))) {
writer => {
var line = reader.readLine
var count = 0
while (line != null) {
count += 1
writer.write(line)
writer.newLine
line = reader.readLine
}
count
}
}
}
}


def copyFileDC = withResources {
val reader = resource[BufferedReader,Int](new BufferedReader(new FileReader("test.txt")))
val writer = resource[BufferedWriter,Int](new BufferedWriter(new FileWriter("test_copy.txt")))
var line = reader.readLine
var count = 0
while(line != null) {
count += 1
writer write line
writer.newLine
line = reader.readLine
}
count
}

And here's Tiark Rompf's suggestion of improvement:

trait ContextType[B]
def forceContextType[B]: ContextType[B] = null


// A DC version of 'using'
def resource[X <: {def close()}, B: ContextType](res : X): X @cps[B,B] = shift(using[X, B](res))


// some sugar for reset
def withResources[A](x : => A @cps[A, A]) = reset{x}


// and now use our new lib
def copyFileDC = withResources {
implicit val _ = forceContextType[Int]
val reader = resource(new BufferedReader(new FileReader("test.txt")))
val writer = resource(new BufferedWriter(new FileWriter("test_copy.txt")))
var line = reader.readLine
var count = 0
while(line != null) {
count += 1
writer write line
writer.newLine
line = reader.readLine
}
count
}

Daniel,

I've just recently deployed the scala-arm library for automatic resource management. You can find the documentation here: https://github.com/jsuereth/scala-arm/wiki

This library supports three styles of usage (currently):

1) Imperative/for-expression:

import resource._
for(input <- managed(new FileInputStream("test.txt")) {
// Code that uses the input as a FileInputStream
}

2) Monadic-style

import resource._
import java.io._
val lines = for { input <- managed(new FileInputStream("test.txt"))
val bufferedReader = new BufferedReader(new InputStreamReader(input))
line <- makeBufferedReaderLineIterator(bufferedReader)
} yield line.trim()
lines foreach println

3) Delimited Continuations-style

Here's an "echo" tcp server:

import java.io._
import util.continuations._
import resource._
def each_line_from(r : BufferedReader) : String @suspendable =
shift { k =>
var line = r.readLine
while(line != null) {
k(line)
line = r.readLine
}
}
reset {
val server = managed(new ServerSocket(8007)) !
while(true) {
// This reset is not needed, however the  below denotes a "flow" of execution that can be deferred.
// One can envision an asynchronous execuction model that would support the exact same semantics as below.
reset {
val connection = managed(server.accept) !
val output = managed(connection.getOutputStream) !
val input = managed(connection.getInputStream) !
val writer = new PrintWriter(new BufferedWriter(new OutputStreamWriter(output)))
val reader = new BufferedReader(new InputStreamReader(input))
writer.println(each_line_from(reader))
writer.flush()
}
}
}

The code makes uses of a Resource type-trait, so it's able to adapt to most resource types. It has a fallback to use structural typing against classes with either a close or dispose method. Please check out the documentation and let me know if you think of any handy features to add.

There is light-weight (10 lines of code) ARM included with better-files. See: https://github.com/pathikrit/better-files#lightweight-arm

import better.files._
for {
in <- inputStream.autoClosed
out <- outputStream.autoClosed
} in.pipeTo(out)
// The input and output streams are auto-closed once out of scope

Here is how it is implemented if you don't want the whole library:

  type Closeable = {
def close(): Unit
}


type ManagedResource[A <: Closeable] = Traversable[A]


implicit class CloseableOps[A <: Closeable](resource: A) {
def autoClosed: ManagedResource[A] = new Traversable[A] {
override def foreach[U](f: A => U) = try {
f(resource)
} finally {
resource.close()
}
}
}

How about using Type classes

trait GenericDisposable[-T] {
def dispose(v:T):Unit
}
...


def using[T,U](r:T)(block:T => U)(implicit disp:GenericDisposable[T]):U = try {
block(r)
} finally {
Option(r).foreach { r => disp.dispose(r) }
}

Another alternative is Choppy's Lazy TryClose monad. It's pretty good with database connections:

val ds = new JdbcDataSource()
val output = for {
conn  <- TryClose(ds.getConnection())
ps    <- TryClose(conn.prepareStatement("select * from MyTable"))
rs    <- TryClose.wrap(ps.executeQuery())
} yield wrap(extractResult(rs))


// Note that Nothing will actually be done until 'resolve' is called
output.resolve match {
case Success(result) => // Do something
case Failure(e) =>      // Handle Stuff
}

And with streams:

val output = for {
outputStream      <- TryClose(new ByteArrayOutputStream())
gzipOutputStream  <- TryClose(new GZIPOutputStream(outputStream))
_                 <- TryClose.wrap(gzipOutputStream.write(content))
} yield wrap({gzipOutputStream.flush(); outputStream.toByteArray})


output.resolve.unwrap match {
case Success(bytes) => // process result
case Failure(e) => // handle exception
}

More info here: https://github.com/choppythelumberjack/tryclose

For now Scala 2.13 has finally supported: try with resources by using Using :), Example:

val lines: Try[Seq[String]] =
Using(new BufferedReader(new FileReader("file.txt"))) { reader =>
Iterator.unfold(())(_ => Option(reader.readLine()).map(_ -> ())).toList
}

or using Using.resource avoid Try

val lines: Seq[String] =
Using.resource(new BufferedReader(new FileReader("file.txt"))) { reader =>
Iterator.unfold(())(_ => Option(reader.readLine()).map(_ -> ())).toList
}

You can find more examples from Using doc.

A utility for performing automatic resource management. It can be used to perform an operation using resources, after which it releases the resources in reverse order of their creation.

Here is @chengpohi's answer, modified so it works with Scala 2.8+, instead of just Scala 2.13 (yes, it works with Scala 2.13 also):

def unfold[A, S](start: S)(op: S => Option[(A, S)]): List[A] =
Iterator
.iterate(op(start))(_.flatMap{ case (_, s) => op(s) })
.map(_.map(_._1))
.takeWhile(_.isDefined)
.flatten
.toList


def using[A <: AutoCloseable, B](resource: A)
(block: A => B): B =
try block(resource) finally resource.close()


val lines: Seq[String] =
using(new BufferedReader(new FileReader("file.txt"))) { reader =>
unfold(())(_ => Option(reader.readLine()).map(_ -> ())).toList
}

While Using is OK, I prefer the monadic style of resource composition. Twitter Util's Managed is pretty nice, except for its dependencies and its not-very-polished API.

To that end, I've published https://github.com/dvgica/managerial for Scala 2.12, 2.13, and 3.0.0. Largely based on the Twitter Util Managed code, no dependencies, with some API improvements inspired by cats-effect Resource.

The simple example:

import ca.dvgi.managerial._
val fileContents = Managed.from(scala.io.Source.fromFile("file.txt")).use(_.mkString)

But the real strength of the library is composing resources via for comprehensions.

Let me know what you think!