Welche Alternativen für das automatische Ressourcenmanagement gibt es für Scala?

102

Ich habe im Web viele Beispiele für ARM (Automatic Resource Management) für Scala gesehen. Es scheint ein Übergangsritus zu sein, einen zu schreiben, obwohl die meisten einander ziemlich ähnlich sehen. Ich habe allerdings ein ziemlich cooles Beispiel mit Fortsetzungen gesehen.

Auf jeden Fall weist ein Großteil dieses Codes Fehler des einen oder anderen Typs auf, daher hielt ich es für eine gute Idee, hier auf Stack Overflow eine Referenz zu haben, in der wir über die korrektesten und am besten geeigneten Versionen abstimmen können.

Daniel C. Sobral
quelle
Würde diese Frage mehr Antworten generieren, wenn es kein Community-Wiki wäre? Beachten Sie sicher, wenn abgestimmte Antworten in der Community Wiki Award Reputation ...
Huynhjl
2
Eindeutige Referenzen können ARM eine weitere Sicherheitsstufe hinzufügen, um sicherzustellen, dass Referenzen auf Ressourcen an den Manager zurückgegeben werden, bevor close () aufgerufen wird. thread.gmane.org/gmane.comp.lang.scala/19160/focus=19168
Retronym
@retronym Ich denke, das Plugin für die Einzigartigkeit wird eine ziemliche Revolution sein, mehr als Fortsetzungen. Tatsächlich denke ich, dass dies eine Sache in Scala ist, die in nicht allzu ferner Zukunft wahrscheinlich auf andere Sprachen portiert wird. Wenn dies herauskommt, müssen wir die Antworten entsprechend bearbeiten. :-)
Daniel C. Sobral
1
Da ich in der Lage sein muss, mehrere java.lang.AutoCloseable-Instanzen zu verschachteln, von denen jede von der vorherigen Instanz abhängt, die erfolgreich instanziiert wurde, bin ich schließlich auf ein Muster gestoßen, das für mich sehr nützlich war. Ich schrieb es als Antwort auf eine ähnliche StackOverflow-Frage: stackoverflow.com/a/34277491/501113
chaotic3quilibrium

Antworten:

10

Im Moment hat Scala 2.13 endlich unterstützt: try with resourcesmit Using :), Beispiel:

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

oder mit Using.resourcevermeidenTry

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

Weitere Beispiele finden Sie unter Verwenden von doc.

Ein Dienstprogramm zur Durchführung der automatischen Ressourcenverwaltung. Es kann verwendet werden, um eine Operation unter Verwendung von Ressourcen auszuführen, wonach die Ressourcen in umgekehrter Reihenfolge ihrer Erstellung freigegeben werden.

Chengpohi
quelle
Könnten Sie bitte auch die Using.resourceVariante hinzufügen ?
Daniel C. Sobral
@ DanielC.Sobral, klar, habe es gerade hinzugefügt.
Chengpohi
Wie würden Sie das für Scala 2.12 schreiben? Hier ist eine ähnliche usingMethode:def using[A <: AutoCloseable, B](resource: A) (block: A => B): B = try block(resource) finally resource.close()
Mike Slinn
75

Chris Hansens Blogeintrag 'ARM Blocks in Scala: Revisited' vom 26.03.09 spricht über Folie 21 von Martin Oderskys FOSDEM-Präsentation . Dieser nächste Block stammt direkt von Folie 21 (mit Genehmigung):

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

--end quote--

Dann können wir so nennen:

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

Was sind die Nachteile dieses Ansatzes? Dieses Muster scheint 95% der Probleme zu lösen, bei denen ich ein automatisches Ressourcenmanagement benötigen würde ...

Bearbeiten: Code-Snippet hinzugefügt


Edit2: Erweiterung des Designmusters - Inspiration aus der Python- withAnweisung und Adressierung:

  • Anweisungen, die vor dem Block ausgeführt werden sollen
  • Ausnahme je nach verwalteter Ressource erneut auslösen
  • Umgang mit zwei Ressourcen mit einer einzigen using-Anweisung
  • ressourcenspezifische Behandlung durch Bereitstellung einer impliziten Konvertierung und einer ManagedKlasse

Dies ist mit 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
quelle
2
Es gibt Alternativen, aber ich wollte nicht implizieren, dass daran etwas nicht stimmt. Ich möchte nur all diese Antworten hier auf Stack Overflow. :-)
Daniel C. Sobral
5
Wissen Sie, ob die Standard-API so etwas enthält? Es scheint eine lästige Pflicht zu sein, dies die ganze Zeit für mich selbst schreiben zu müssen.
Daniel Darabos
Es ist schon eine Weile her, seit dies veröffentlicht wurde, aber die erste Lösung schließt den inneren Stream nicht, wenn der Out-Konstruktor wirft, was hier wahrscheinlich nicht passieren wird, aber es gibt andere Fälle, in denen dies schlecht sein kann. Der Schluss kann auch werfen. Auch keine Unterscheidung zwischen schwerwiegenden Ausnahmen. Der zweite hat überall Code-Gerüche und hat keine Vorteile gegenüber dem ersten. Sie verlieren sogar die tatsächlichen Typen und wären daher für so etwas wie einen ZipInputStream nutzlos.
Steinybot
Wie empfehlen Sie dies, wenn der Block einen Iterator zurückgibt?
Jorge Machado
62

Daniel,

Ich habe kürzlich die Scala-Arm-Bibliothek für die automatische Ressourcenverwaltung bereitgestellt. Die Dokumentation finden Sie hier: https://github.com/jsuereth/scala-arm/wiki

Diese Bibliothek unterstützt (derzeit) drei Verwendungsstile:

1) Imperativ / Ausdruck:

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

2) Monadischer Stil

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) Begrenzter Fortsetzungsstil

Hier ist ein "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()
    }
  }
}

Der Code verwendet ein Ressourcentyp-Merkmal, sodass er sich an die meisten Ressourcentypen anpassen kann. Es hat einen Fallback, strukturelle Typisierung für Klassen mit einer Close- oder Dispose-Methode zu verwenden. Bitte lesen Sie die Dokumentation und lassen Sie mich wissen, wenn Sie an nützliche Funktionen denken, die Sie hinzufügen können.

jsuereth
quelle
1
Ja, ich habe das gesehen. Ich möchte den Code durchsehen, um zu sehen, wie Sie einige Dinge erreichen, aber ich bin gerade viel zu beschäftigt. Da das Ziel der Frage darin besteht, einen Verweis auf zuverlässigen ARM-Code bereitzustellen, mache ich dies zur akzeptierten Antwort.
Daniel C. Sobral
18

Hier ist die James Iry- Lösung mit Fortsetzungen:

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

Hier sind die Lösungen mit und ohne Fortsetzung zum Vergleich:

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
}

Und hier ist Tiark Rompfs Verbesserungsvorschlag:

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
quelle
Hat die Verwendung von (neuer BufferedWriter (neuer FileWriter ("test_copy.txt"))) keine Probleme, wenn der BufferedWriter-Konstruktor ausfällt? Jede Ressource sollte in einen Verwendungsblock eingewickelt werden ...
Jaap
@Jaap Dies ist der von Oracle vorgeschlagene Stil . BufferedWriterlöst keine geprüften Ausnahmen aus. Wenn also eine Ausnahme ausgelöst wird, wird nicht erwartet, dass das Programm diese wiederherstellt.
Daniel C. Sobral
7

Ich sehe eine schrittweise 4-Stufen-Entwicklung für ARM in Scala:

  1. Kein ARM: Dreck
  2. Nur Abschlüsse: Besser, aber mehrere verschachtelte Blöcke
  3. Fortsetzung Monade: Verwenden Sie Für, um die Verschachtelung, aber unnatürliche Trennung in 2 Blöcken zu glätten
  4. Direkte Stilfortsetzungen: Nirava, aha! Dies ist auch die typsicherste Alternative: Eine Ressource außerhalb des withResource-Blocks ist ein Typfehler.
Mushtaq Ahmed
quelle
1
Wohlgemerkt, CPS in Scala werden durch Monaden implementiert. :-)
Daniel C. Sobral
1
Mushtaq, 3) Sie können die Ressourcenverwaltung in einer Monade durchführen, die nicht die Monade der Fortsetzung ist. 4) Die Ressourcenverwaltung mit meinem mit Ressourcen / Ressourcen getrennten Fortsetzungscode ist nicht mehr (und nicht weniger) typsicher als "Verwenden". Es ist immer noch möglich zu vergessen, eine Ressource zu verwalten, die sie benötigt. Vergleiche mit (new Resource ()) {first => val second = new Resource () // oops! // Ressourcen verwenden} // nur zuerst wird geschlossen mitResources {val first = resource (neue Ressource ()) val second = neue Ressource () // oops! // Ressourcen verwenden ...} // wird erst geschlossen
James Iry
2
Daniel, CPS in Scala ist wie CPS in jeder funktionalen Sprache. Es sind begrenzte Fortsetzungen, die eine Monade verwenden.
James Iry
James, danke, dass du es gut erklärt hast. Als ich in Indien saß, konnte ich mir nur wünschen, ich wäre für dein BASE-Gespräch da. Ich warte darauf zu sehen, wann du diese Folien online
stellst
6

In besseren Dateien ist ein leichtes ARM (10 Codezeilen) enthalten. Siehe: 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

So wird es implementiert, wenn Sie nicht die gesamte Bibliothek möchten:

  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
quelle
Das ist ziemlich nett. Ich habe etwas Ähnliches wie dieser Ansatz gewählt, aber eine mapund flatMap-Methode für die CloseableOps anstelle von foreach definiert, damit zum Verständnis kein Traversable erhalten wird.
EdgeCaseBerg
1

Wie wäre es mit Typklassen

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
quelle
1

Eine andere Alternative ist Choppys Lazy TryClose-Monade. Es ist ziemlich gut mit Datenbankverbindungen:

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
}

Und mit 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
}

Weitere Informationen hier: https://github.com/choppythelumberjack/tryclose

ChoppyTheLumberjack
quelle
0

Hier ist die Antwort von @ chengpohi, die so geändert wurde, dass sie mit Scala 2.8+ funktioniert, anstatt nur mit Scala 2.13 (ja, sie funktioniert auch mit 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
quelle