Quelles alternatives de gestion automatique des ressources existent pour Scala?

102

J'ai vu de nombreux exemples d'ARM (gestion automatique des ressources) sur le Web pour Scala. Cela semble être un rite de passage pour en écrire un, bien que la plupart se ressemblent beaucoup. J'ai cependant vu un exemple assez cool utilisant des continuations.

En tout cas, une grande partie de ce code a des défauts d'un type ou d'un autre, alors j'ai pensé que ce serait une bonne idée d'avoir une référence ici sur Stack Overflow, où nous pouvons voter pour les versions les plus correctes et appropriées.

Daniel C. Sobral
la source
Cette question générerait-elle plus de réponses si ce n'était pas un wiki communautaire? Notez que si vous
avez
2
les références uniques peuvent ajouter un autre niveau de sécurité à ARM pour garantir que les références aux ressources sont renvoyées au gestionnaire avant l'appel de close (). thread.gmane.org/gmane.comp.lang.scala/19160/focus=19168
retronym
@retronym Je pense que le plugin d'unicité sera une véritable révolution, plus que des suites. Et, en fait, je pense que c'est une chose dans Scala qui est très susceptible de se retrouver portée dans d'autres langues dans un avenir pas trop lointain. Lorsque cela sortira, assurons-nous de modifier les réponses en conséquence. :-)
Daniel C. Sobral
1
Parce que j'ai besoin de pouvoir imbriquer plusieurs instances java.lang.AutoCloseable, dont chacune dépend de l'instanciation réussie de la précédente, j'ai finalement trouvé un modèle qui m'a été très utile. Je l'ai écrit comme réponse à une question StackOverflow similaire: stackoverflow.com/a/34277491/501113
chaotic3quilibrium

Réponses:

10

Pour l'instant, Scala 2.13 a enfin pris en charge: try with resourcesen utilisant Using :), Exemple:

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

ou en utilisant Using.resourceéviterTry

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

Vous pouvez trouver plus d'exemples dans Utilisation de doc.

Un utilitaire pour effectuer une gestion automatique des ressources. Il peut être utilisé pour effectuer une opération utilisant des ressources, après quoi il libère les ressources dans l'ordre inverse de leur création.

chengpohi
la source
Pouvez-vous également ajouter la Using.resourcevariante?
Daniel C.Sobral
@ DanielC.Sobral, bien sûr, vient de l'ajouter.
chengpohi
Comment écririez-vous cela pour Scala 2.12? Voici une usingméthode similaire :def using[A <: AutoCloseable, B](resource: A) (block: A => B): B = try block(resource) finally resource.close()
Mike Slinn
75

L' entrée de blog de Chris Hansen 'ARM Blocks in Scala: Revisited' du 26/03/09 parle de la diapositive 21 de la présentation FOSDEM de Martin Odersky . Ce bloc suivant est tiré directement de la diapositive 21 (avec permission):

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

--end devis--

Ensuite, nous pouvons appeler comme ceci:

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

Quels sont les inconvénients de cette approche? Ce modèle semble répondre à 95% des cas où j'aurais besoin d'une gestion automatique des ressources ...

Edit: extrait de code ajouté


Edit2: étendre le modèle de conception - s'inspirer de la withdéclaration et de l'adressage python :

  • instructions à exécuter avant le bloc
  • relancer une exception en fonction de la ressource gérée
  • gestion de deux ressources avec une seule instruction using
  • gestion spécifique aux ressources en fournissant une conversion implicite et une Managedclasse

C'est avec 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
la source
2
Il existe des alternatives, mais je ne veux pas dire qu'il y a quelque chose qui cloche là-dedans. Je veux juste toutes ces réponses ici, sur Stack Overflow. :-)
Daniel C. Sobral
5
Savez-vous s'il y a quelque chose comme ça dans l'API standard? Cela semble être une corvée d'avoir à écrire ça pour moi tout le temps.
Daniel Darabos
Cela fait un moment que cela a été publié, mais la première solution ne ferme pas le flux interne si le constructeur out lance ce qui ne se produira probablement pas ici, mais il y a d'autres cas où cela peut être mauvais. La fermeture peut également lancer. Aucune distinction non plus entre les exceptions fatales. Le second a des odeurs de code partout et n'a aucun avantage sur le premier. Vous perdez même les types réels, ce serait donc inutile pour quelque chose comme un ZipInputStream.
steinybot
Comment recommandez-vous de faire cela si le bloc renvoie un itérateur?
Jorge Machado
62

Daniel,

Je viens de déployer récemment la bibliothèque scala-arm pour la gestion automatique des ressources. Vous pouvez trouver la documentation ici: https://github.com/jsuereth/scala-arm/wiki

Cette bibliothèque prend en charge trois styles d'utilisation (actuellement):

1) Impératif / pour-expression:

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

2) de style monadique

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) style Continuations délimitées

Voici un serveur 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()
    }
  }
}

Le code utilise un trait de type Resource, il est donc capable de s'adapter à la plupart des types de ressources. Il a une alternative pour utiliser le typage structurel contre les classes avec une méthode close ou dispose. Veuillez consulter la documentation et laissez-moi savoir si vous pensez à des fonctionnalités utiles à ajouter.

jsuereth
la source
1
Oui, j'ai vu ça. Je veux regarder le code, pour voir comment vous accomplissez certaines choses, mais je suis bien trop occupé en ce moment. Quoi qu'il en soit, puisque le but de la question est de fournir une référence à un code ARM fiable, j'en fais la réponse acceptée.
Daniel C.Sobral
18

Voici la solution James Iry utilisant des continuations:

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

Voici les solutions avec et sans suite pour comparaison:

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
}

Et voici la suggestion d'amélioration 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
la source
L'utilisation de (new BufferedWriter (new FileWriter ("test_copy.txt"))) ne souffre-t-elle pas de problèmes lorsque le constructeur BufferedWriter échoue? chaque ressource doit être enveloppée dans un bloc using ...
Jaap
@Jaap C'est le style suggéré par Oracle . BufferedWriterne lève pas d'exceptions vérifiées, donc si une exception est levée, le programme ne devrait pas en récupérer.
Daniel C.Sobral
7

Je vois une évolution progressive en 4 étapes pour faire ARM dans Scala:

  1. Pas de bras: saleté
  2. Uniquement les fermetures: mieux, mais plusieurs blocs imbriqués
  3. Continuation Monad: utilisez For pour aplatir l'emboîtement, mais séparation non naturelle en 2 blocs
  4. Continuation directe du style: Nirava, aha! C'est également l'alternative la plus sûre de type: une ressource en dehors du bloc withResource sera une erreur de type.
Mushtaq Ahmed
la source
1
Remarquez que les CPS dans Scala sont implémentés via des monades. :-)
Daniel C. Sobral
1
Mushtaq, 3) Vous pouvez faire de la gestion des ressources dans une monade qui n'est pas la monade de la continuation 4) La gestion des ressources en utilisant mon code de continuations délimitées withResources / resource n'est ni plus (ni moins) de type sûr que "using". Il est encore possible d'oublier de gérer une ressource qui en a besoin. comparer en utilisant (new Resource ()) {first => val second = new Resource () // oops! // utiliser les ressources} // se ferme d'abord uniquement avec les ressources {val first = resource (new Resource ()) val second = new Resource () // oops! // utilise des ressources ...} // n'est fermé que pour la première fois
James Iry
2
Daniel, CPS dans Scala est comme CPS dans n'importe quel langage fonctionnel. Ce sont des suites délimitées qui utilisent une monade.
James Iry
James, merci de bien l'expliquer. Assis en Inde, je ne pouvais que souhaiter être là pour votre conférence BASE. En attendant de voir quand vous mettez ces diapositives en ligne :)
Mushtaq Ahmed
6

Il y a un ARM léger (10 lignes de code) inclus avec de meilleurs fichiers. Voir: 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

Voici comment il est implémenté si vous ne voulez pas toute la bibliothèque:

  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
la source
C'est plutôt sympa. J'ai pris quelque chose de similaire à cette approche, mais j'ai défini une méthode mapet flatMappour CloseableOps au lieu de foreach afin que pour les compréhensions ne donne pas un traversable.
EdgeCaseBerg
1

Que diriez-vous d'utiliser les classes 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) } 
}
Santhosh Sath
la source
1

Une autre alternative est la monade Lazy TryClose de Choppy. C'est assez bon avec les connexions de base de données:

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
}

Et avec les flux:

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
}

Plus d'informations ici: https://github.com/choppythelumberjack/tryclose

ChoppyTheLumberjack
la source
0

Voici la réponse de @ chengpohi, modifiée pour qu'elle fonctionne avec Scala 2.8+, au lieu de juste Scala 2.13 (oui, cela fonctionne aussi avec 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
la source