Quali alternative di gestione automatica delle risorse esistono per Scala?

Ho visto molti esempi di ARM (gestione automatica delle risorse) sul web per Scala. Sembra essere un rito di passaggio per scriverne uno, anche se la maggior parte sembrano quasi l’uno con l’altro. Ho visto un esempio piuttosto interessante usando le continuazioni, però.

In ogni caso, un sacco di quel codice ha difetti di un tipo o di un altro, quindi ho pensato che sarebbe una buona idea avere un riferimento qui su Stack Overflow, dove possiamo votare le versioni più corrette e appropriate.

Daniel,

Recentemente ho implementato la libreria scala-arm per la gestione automatica delle risorse. Puoi trovare la documentazione qui: http://wiki.github.com/jsuereth/scala-arm/

Questa libreria supporta tre stili di utilizzo (attualmente):

1) Imperativo / espressione:

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

2) Stile monadico

 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) Stile di continuazione delimitato

Ecco un server 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() } } } 

Il codice fa uso di un tratto di tipo risorsa, quindi è in grado di adattarsi alla maggior parte dei tipi di risorse. Ha un fallback per usare la tipizzazione strutturale contro le classi con un metodo close o dispose. Si prega di consultare la documentazione e fammi sapere se pensi a qualche funzione utile da aggiungere.

Il blog di Chris Hansen “ARM Blocks in Scala: Revisited” del 26/03/09 parla della diapositiva 21 della presentazione FOSDEM di Martin Odersky. Questo blocco successivo viene preso direttamente dalla diapositiva 21 (con authorization):

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

–prima citazione–

Quindi possiamo chiamare così:

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

Quali sono gli svantaggi di questo approccio? Sembrerebbe che questo schema riguardi il 95% di dove avrei bisogno della gestione automatica delle risorse …

Modifica: aggiunto snippet di codice


Edit2: estendere il modello di design – prendendo ispirazione da python with statement e indirizzamento:

  • dichiarazioni da eseguire prima del blocco
  • rilanciare l’eccezione in base alla risorsa gestita
  • gestire due risorse con una sola istruzione using
  • gestione specifica delle risorse fornendo una conversione implicita e una class Managed

Questo è 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(_) } } } 

Ecco la soluzione di James Iry usando le continuazioni:

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

Ecco le soluzioni con e senza continuazioni per il confronto:

 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 } 

Ed ecco il suggerimento di miglioramento di 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, bene hai chiesto questo. Sono io stesso incuriosito dopo aver visto il codice di James Iry. Vedo un’evoluzione graduale in 4 fasi per fare ARM in Scala:

  1. No ARM: Dirt
  2. Solo chiusure: meglio, ma più blocchi nidificati
  3. Continuazione Monad: Utilizzare Per per appiattire il nidificazione, ma separazione innaturale in 2 blocchi
  4. Continuazioni dirette di stile: Nirava, aha! Questa è anche l’alternativa più sicura dal tipo: una risorsa esterna con Blocco origine sarà un errore di tipo.

Quello che mi piacerebbe davvero vedere è una presentazione che li descrive. Sarà molto educativo e dovrebbe convincere i principianti che esiste un mondo oltre Monads 🙂

C’è un BRACCIO leggero (10 linee di codice) incluso con file migliori. Vedi: 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 

Ecco come viene implementato se non vuoi l'intera libreria:

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

Che ne dici di usare le classi di 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) } } 

Un’altra alternativa è Choppy’s Lazy TryClose monad. È abbastanza buono con le connessioni al database:

 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 } 

E con i flussi:

 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 } 

Maggiori informazioni qui: https://github.com/choppythelumberjack/tryclose