Quali alternative di gestione automatica delle risorse esistono per Scala?


102

Ho visto molti esempi di ARM (gestione automatica delle risorse) sul web per Scala. Scriverne uno sembra essere un rito di passaggio, anche se la maggior parte si assomiglia molto l'una all'altra. Ho fatto vedere un esempio piuttosto fresco utilizzando continuazioni, però.

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


Questa domanda genererebbe più risposte se non fosse un wiki della comunità? Nota bene se votate le risposte nella reputazione del premio wiki della comunità ...
huynhjl

2
i riferimenti univoci possono aggiungere un altro livello di sicurezza a ARM per garantire che i riferimenti alle risorse vengano restituiti al gestore prima che venga chiamato close (). thread.gmane.org/gmane.comp.lang.scala/19160/focus=19168
retronym

@retronym Penso che il plugin di unicità sarà una vera rivoluzione, più che continuazioni. E, in effetti, penso che questa sia una cosa in Scala che molto probabilmente si ritroverà trasferita ad altre lingue in un futuro non troppo lontano. Quando viene fuori, assicuriamoci di modificare le risposte di conseguenza. :-)
Daniel C. Sobral

1
Poiché devo essere in grado di annidare più istanze java.lang.AutoCloseable, ognuna delle quali dipende dalla precedente istanziazione con successo, ho finalmente trovato uno schema che è stato molto utile per me. L'ho scritto come una risposta su simile domanda StackOverflow: stackoverflow.com/a/34277491/501113
chaotic3quilibrium

Risposte:


10

Per ora Scala 2.13 ha finalmente supportato: try with resourcesutilizzando Using :), Esempio:

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

o usando Using.resourceevitareTry

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

Puoi trovare altri esempi in Using doc.

Un'utilità per eseguire la gestione automatica delle risorse. Può essere utilizzato per eseguire un'operazione utilizzando le risorse, dopodiché rilascia le risorse in ordine inverso rispetto alla loro creazione.


Potresti aggiungere anche la Using.resourcevariante?
Daniel C. Sobral,

@ DanielC.Sobral, certo, l'ho appena aggiunto.
chengpohi

Come scriveresti questo per Scala 2.12? Ecco un usingmetodo simile :def using[A <: AutoCloseable, B](resource: A) (block: A => B): B = try block(resource) finally resource.close()
Mike Slinn

75

La voce del blog di Chris Hansen "ARM Blocks in Scala: Revisited" del 26/03/09 parla della diapositiva 21 della presentazione FOSDEM di Martin Odersky . Il prossimo blocco è preso direttamente dalla diapositiva 21 (con autorizzazione):

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

--end citazione--

Quindi possiamo chiamare in questo modo:

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? Quel modello sembrerebbe affrontare il 95% di dove avrei bisogno della gestione automatica delle risorse ...

Modifica: aggiunto snippet di codice


Edit2: estensione del modello di progettazione - prendendo ispirazione dall'istruzione di Python withe indirizzando:

  • istruzioni da eseguire prima del blocco
  • rilancio dell'eccezione a seconda della risorsa gestita
  • gestire due risorse con una sola istruzione using
  • gestione specifica delle risorse fornendo una conversione implicita e una Managedclasse

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

2
Ci sono alternative, ma non intendo implicare che ci sia qualcosa di sbagliato in questo. Voglio solo tutte quelle risposte qui, su Stack Overflow. :-)
Daniel C. Sobral

5
Sai se c'è qualcosa di simile nell'API standard? Mi sembra un compito ingrato dover scrivere tutto il tempo per me stesso.
Daniel Darabos

È passato un po 'di tempo da quando questo è stato pubblicato ma la prima soluzione non chiude il flusso interno se il costruttore out lancia il che probabilmente non accadrà qui ma ci sono altri casi in cui questo può essere un male. La chiusura può anche lanciare. Nessuna distinzione nemmeno tra eccezioni fatali. Il secondo ha odori di codice ovunque e ha zero vantaggi rispetto al primo. Perdi anche i tipi effettivi, quindi sarebbe inutile per qualcosa come ZipInputStream.
steinybot

Come mi consigliate di farlo se il blocco restituisce un iteratore?
Jorge Machado

62

Daniel,

Di recente ho distribuito la libreria scala-arm per la gestione automatica delle risorse. Puoi trovare la documentazione qui: https://github.com/jsuereth/scala-arm/wiki

Questa libreria supporta tre stili di utilizzo (attualmente):

1) Imperativo / per-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 Continuazioni delimitate

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 utilizza 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. Consulta la documentazione e fammi sapere se pensi a qualche utile funzionalità da aggiungere.


1
Sì, l'ho visto. Voglio esaminare il codice, per vedere come si realizzano alcune cose, ma al momento sono troppo occupato. Ad ogni modo, poiché l'obiettivo della domanda è fornire un riferimento a un codice ARM affidabile, sto facendo di questa la risposta accettata.
Daniel C. Sobral

18

Ecco la soluzione di James Iry che utilizza 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 seguito 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
}

L'uso di (new BufferedWriter (new FileWriter ("test_copy.txt"))) non presenta problemi quando il costruttore BufferedWriter fallisce? ogni risorsa dovrebbe essere racchiusa in un blocco using ...
Jaap

@Jaap Questo è lo stile suggerito da Oracle . BufferedWriternon genera eccezioni controllate, quindi se viene generata un'eccezione, il programma non dovrebbe ripristinarsi da essa.
Daniel C. Sobral

7

Vedo un'evoluzione graduale in 4 passaggi per fare ARM in Scala:

  1. No ARM: Dirt
  2. Solo chiusure: migliori, ma più blocchi annidati
  3. Monade di continuazione: utilizzare For per appiattire la nidificazione, ma separazione innaturale in 2 blocchi
  4. Continuazioni di stile dirette: Nirava, aha! Questa è anche l'alternativa più sicura dai tipi: una risorsa al di fuori del blocco delle risorse sarà di tipo errore.

1
Intendiamoci, i CPS in Scala sono implementati tramite monadi. :-)
Daniel C. Sobral

1
Mushtaq, 3) È possibile eseguire la gestione delle risorse in una monade che non è la monade della continuazione 4) La gestione delle risorse utilizzando il mio codice withResources / resource delimited continuations non è più (né meno) sicuro di tipo "using". È ancora possibile dimenticare di gestire una risorsa che ne ha bisogno. confronta usando (new Resource ()) {first => val second = new Resource () // oops! // usa le risorse} // solo il primo viene chiuso con le risorse {val first = resource (new Resource ()) val second = new Resource () // oops! // usa le risorse ...} // viene chiuso solo per primo
James Iry

2
Daniel, CPS in Scala è come CPS in qualsiasi linguaggio funzionale. Sono continuazioni delimitate che utilizzano una monade.
James Iry

James, grazie per averlo spiegato bene. Seduto in India, potevo solo desiderare di essere lì per il tuo discorso BASE. Aspetto di vedere quando metti quelle diapositive online :)
Mushtaq Ahmed

6

C'è un ARM leggero (10 righe di codice) incluso con i 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()
      }
    }
  }

Questo è molto carino. Ho preso qualcosa di simile a questo approccio, ma ho definito un metodo mape flatMapper CloseableOps invece di foreach in modo che per le comprensione non si producesse un attraversabile.
EdgeCaseBerg

1

Che ne dici di usare le classi Type

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

1

Un'altra alternativa è la monade Lazy TryClose di Choppy. È 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


0

Ecco la risposta di @ chengpohi, modificata in modo che funzioni con Scala 2.8+, invece che solo con Scala 2.13 (sì, funziona anche 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
  }
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.