scala resource-management

¿Qué alternativas de gestión automática de recursos existen para Scala?



resource-management (7)

He visto muchos ejemplos de ARM (gestión automática de recursos) en la web para Scala. Parece ser un rito de pasaje para escribir uno, aunque la mayoría se parece mucho el uno al otro. Sin embargo, vi un buen ejemplo usando las continuaciones.

En cualquier caso, gran parte de ese código tiene fallas de un tipo u otro, así que pensé que sería una buena idea tener una referencia aquí en Stack Overflow, donde podemos votar las versiones más correctas y apropiadas.


¿Qué tal el uso de clases de tipo

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) } }


Aquí está la solución de James Iry con las siguientes versiones:

// 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}

Aquí están las soluciones con y sin continuación para comparar:

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 }

Y aquí está la sugerencia de mejora de Tiark Rompf:

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,

Recientemente implementé la biblioteca scala-arm para la administración automática de recursos. Puede encontrar la documentación aquí: http://wiki.github.com/jsuereth/scala-arm/

Esta biblioteca admite tres estilos de uso (actualmente):

1) Imperativo / para-expresión:

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

2) estilo monádico

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) Estilo de Continuaciones Delimitadas

Aquí hay un servidor tcp "echo":

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() } } }

El código hace uso de un rasgo de tipo Recurso, por lo que puede adaptarse a la mayoría de los tipos de recursos. Tiene una alternativa para usar el tipado estructural en contra de las clases con un método de cierre o disposición. Por favor revisa la documentación y avísame si piensas en alguna característica útil para agregar.


Daniel, bueno, preguntaste esto. Estoy intrigado después de ver el código de James Iry. Veo una evolución gradual de 4 pasos para hacer ARM en Scala:

  1. Sin ARM: Suciedad
  2. Solo cierres: mejores, pero múltiples bloques anidados
  3. Continuación de la mónada: Usa para aplanar la anidación, pero la separación no natural en 2 bloques
  4. Continuidades de estilo directo: Nirava, aha! Esta es también la alternativa más segura para el tipo de letra: un recurso externo al bloque Recurso será de tipo error.

Lo que realmente me gustaría ver es una presentación que describa esto. Será muy educativo y debería convencer a los mendigos de que hay un mundo más allá de las Mónadas :)


Hay ARM liviano (10 líneas de código) incluido con mejores archivos. Ver: 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

Aquí está cómo se implementa si no desea la biblioteca completa:

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() } } }


La entrada de Chris Hansen en el blog ''ARM Blocks in Scala: Revisited'' del 26/03/09 habla sobre la diapositiva 21 de la presentación FOSDEM de Martin Odersky. El siguiente bloque se toma directamente de la diapositiva 21 (con permiso):

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

--poner cita--

Entonces podemos llamar así:

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

¿Cuáles son los inconvenientes de este enfoque? Ese patrón parece abordar el 95% de los casos en los que necesitaría administración automática de recursos ...

Editar: fragmento de código agregado

Edit2: ampliando el patrón de diseño, inspirándose en python with instrucción y direccionamiento:

  • declaraciones para ejecutar antes del bloque
  • volviendo a lanzar la excepción dependiendo del recurso administrado
  • manejo de dos recursos con una sola declaración de uso
  • manejo específico del recurso al proporcionar una conversión implícita y una clase Managed

Esto es con 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(_) } } }


Otra alternativa es la mónada Lazy TryClose de Choppy. Es bastante bueno con las conexiones de bases de datos:

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 }

Y con transmisiones:

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 }

Más información aquí: https://github.com/choppythelumberjack/tryclose