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

102

He visto muchos ejemplos de ARM (gestión automática de recursos) en la web para Scala. Parece ser un rito de iniciación escribir uno, aunque la mayoría se parecen mucho entre sí. Sin embargo, vi un ejemplo bastante bueno usando continuaciones.

En cualquier caso, gran parte de ese código tiene defectos 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.

Daniel C. Sobral
fuente
¿Esta pregunta generaría más respuestas si no fuera una wiki comunitaria? Tenga en cuenta que si votaron las respuestas en la reputación del premio de la comunidad wiki ...
huynhjl
2
Las referencias únicas pueden agregar otro nivel de seguridad a ARM para garantizar que las referencias a los recursos se devuelvan al administrador antes de que se llame a close (). thread.gmane.org/gmane.comp.lang.scala/19160/focus=19168
retrónimo
@retronym Creo que el complemento de singularidad será toda una revolución, más que las continuaciones. Y, de hecho, creo que esto es algo en Scala que es muy probable que se adapte a otros idiomas en un futuro no muy lejano. Cuando esto salga, asegurémonos de editar las respuestas en consecuencia. :-)
Daniel C. Sobral
1
Debido a que necesito poder anidar múltiples instancias java.lang.AutoCloseable, cada una de las cuales depende de la instancia previa que se haya creado con éxito, finalmente encontré un patrón que ha sido muy útil para mí. Lo escribí como respuesta a una pregunta similar de StackOverflow: stackoverflow.com/a/34277491/501113
chaotic3quilibrium

Respuestas:

10

Por ahora, Scala 2.13 finalmente es compatible: try with resourcesmediante el uso de :), Ejemplo:

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

o usando Using.resourceevitarTry

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

Puede encontrar más ejemplos en Using doc.

Una utilidad para realizar la gestión automática de recursos. Se puede utilizar para realizar una operación utilizando recursos, después de lo cual libera los recursos en orden inverso al de su creación.

chengpohi
fuente
¿Podrías agregar la Using.resourcevariante también?
Daniel C. Sobral
@ DanielC.Sobral, seguro, lo acabo de agregar.
Chengpohi
¿Cómo escribirías esto para Scala 2.12? Aquí hay un usingmétodo similar :def using[A <: AutoCloseable, B](resource: A) (block: A => B): B = try block(resource) finally resource.close()
Mike Slinn
75

La entrada del blog de Chris Hansen 'Bloques ARM en Scala: Revisited' del 26/03/09 habla sobre la diapositiva 21 de la presentación FOSDEM de Martin Odersky . El siguiente bloque se tomó 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()
  }
}

- fin de cotización--

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 parecería abordar el 95% de los lugares donde necesitaría la administración automática de recursos ...

Editar: fragmento de código agregado


Edit2: extendiendo el patrón de diseño, inspirándose en la withdeclaración de Python y abordando:

  • declaraciones para ejecutar antes del bloque
  • volver a lanzar la excepción según el recurso administrado
  • Manejo de dos recursos con una sola declaración de uso
  • manejo específico de recursos al proporcionar una conversión implícita y una Managedclase

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(_) 
    }
  }
}
huynhjl
fuente
2
Hay alternativas, pero no quiero dar a entender que hay algo malo en eso. Solo quiero todas esas respuestas aquí, en Stack Overflow. :-)
Daniel C. Sobral
5
¿Sabes si hay algo como esto en la API estándar? Parece una tarea tener que escribir esto para mí todo el tiempo.
Daniel Darabos
Ha pasado un tiempo desde que se publicó, pero la primera solución no cierra el flujo interno si el constructor de salida arroja, lo que probablemente no sucederá aquí, pero hay otros casos en los que esto puede ser malo. El cierre también puede tirar. Tampoco hay distinción entre excepciones fatales. El segundo tiene código que huele en todas partes y no tiene ventajas sobre el primero. Incluso pierde los tipos reales, por lo que sería inútil para algo como ZipInputStream.
steinybot
¿Cómo recomiendas hacer esto si el bloque devuelve un iterador?
Jorge Machado
62

Daniel

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

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 utiliza un rasgo de tipo de recurso, por lo que puede adaptarse a la mayoría de los tipos de recursos. Tiene una alternativa para usar tipificación estructural contra clases con un método close o dispose. Consulte la documentación y avíseme si piensa en alguna función útil para agregar.

jsuereth
fuente
1
Sí, vi esto. Quiero revisar el código para ver cómo logras algunas cosas, pero estoy demasiado ocupado en este momento. De todos modos, dado que el objetivo de la pregunta es proporcionar una referencia a un código ARM confiable, hago que esta sea la respuesta aceptada.
Daniel C. Sobral
18

Aquí está la solución de James Iry usando continuaciones:

// 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 continuaciones 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 C. Sobral
fuente
¿No sufre problemas el uso de (new BufferedWriter (new FileWriter ("test_copy.txt"))) cuando falla el constructor BufferedWriter? cada recurso debe estar envuelto en un bloque de uso ...
Jaap
@Jaap Este es el estilo sugerido por Oracle . BufferedWriterno lanza excepciones comprobadas, por lo que si se lanza alguna excepción, no se espera que el programa se recupere de ella.
Daniel C. Sobral
7

Veo una evolución gradual de 4 pasos para hacer ARM en Scala:

  1. Sin BRAZO: suciedad
  2. Solo cierres: mejor, pero con varios bloques anidados
  3. Mónada de continuación: use For para aplanar el anidamiento, pero la separación antinatural en 2 bloques
  4. Continuaciones de estilo directo: Nirava, ¡ajá! Esta es también la alternativa más segura para los tipos: un recurso fuera del bloque de recursos será un error de tipo.
Mushtaq Ahmed
fuente
1
Eso sí, CPS en Scala se implementan a través de mónadas. :-)
Daniel C. Sobral
1
Mushtaq, 3) Puede hacer la administración de recursos en una mónada que no es la mónada de continuación 4) La administración de recursos usando mi código de continuaciones delimitadas con recursos / recursos no es más (ni menos) seguro que "usar". Todavía es posible olvidarse de administrar un recurso que lo necesita. compare usando (new Resource ()) {first => val second = new Resource () // ¡Ups! // usar recursos} // solo primero se cierra con Recursos {val primero = recurso (nuevo recurso ()) val segundo = nuevo recurso () // ¡Ups! // usa recursos ...} // solo se cierra primero
James Iry
2
Daniel, CPS en Scala es como CPS en cualquier lenguaje funcional. Son continuaciones delimitadas que usan una mónada.
James Iry
James, gracias por explicarlo bien. Sentado en la India, solo podía desear estar allí para su charla BASE. Esperando a ver cuando pongas esas diapositivas en línea :)
Mushtaq Ahmed
6

Hay ARM de peso ligero (10 líneas de código) incluido con better-files. 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

Así es como se implementa si no desea toda la biblioteca:

  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()
      }
    }
  }
pathikrit
fuente
Esto está muy bien. Tomé algo similar a este enfoque, pero definí un método mapy flatMappara CloseableOps en lugar de foreach para que las comprensiones no produjeran un transitable.
EdgeCaseBerg
1

¿Qué hay de usar 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) } 
}
Santhosh Sath
fuente
1

Otra alternativa es la mónada Lazy TryClose de Choppy. Es bastante bueno con conexiones a 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 arroyos:

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

ChoppyTheLumberjack
fuente
0

Aquí está la respuesta de @ chengpohi, modificada para que funcione con Scala 2.8+, en lugar de solo Scala 2.13 (sí, también funciona con Scala 2.13):

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
  }
Mike Slinn
fuente