Quais alternativas de gerenciamento automático de recursos existem para o Scala?

102

Tenho visto muitos exemplos de ARM (gerenciamento automático de recursos) na web para Scala. Parece ser um rito de passagem escrever um, embora a maioria se pareça muito. Eu fiz ver um exemplo muito legal usando continuações, no entanto.

De qualquer forma, grande parte desse código tem falhas de um tipo ou de outro, então achei uma boa ideia ter uma referência aqui no Stack Overflow, onde podemos votar nas versões mais corretas e apropriadas.

Daniel C. Sobral
fonte
Esta pergunta geraria mais respostas se não fosse um wiki da comunidade? Observe se as respostas votadas na reputação do prêmio wiki da comunidade ...
huynhjl
2
referências exclusivas podem adicionar outro nível de segurança ao ARM para garantir que as referências aos recursos sejam retornadas ao gerenciador antes que close () seja chamado. thread.gmane.org/gmane.comp.lang.scala/19160/focus=19168
retrônimo de
@retronym Acho que o plugin de exclusividade será uma verdadeira revolução, mais do que continuações. E, na verdade, acho que isso é algo no Scala que provavelmente será portado para outras línguas em um futuro não muito distante. Quando isso for divulgado, certifique-se de editar as respostas de acordo. :-)
Daniel C. Sobral
1
Como preciso aninhar várias instâncias java.lang.AutoCloseable, cada uma das quais depende da instanciação bem-sucedida da anterior, finalmente cheguei a um padrão que foi muito útil para mim. Eu escrevi isso como uma resposta a uma pergunta StackOverflow semelhante: stackoverflow.com/a/34277491/501113
chaotic3quilibrium

Respostas:

10

Por enquanto, Scala 2.13 finalmente tem suporte: try with resourcesusando Usando :), Exemplo:

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

ou usando Using.resourceevitarTry

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

Você pode encontrar mais exemplos em Usando o doc.

Um utilitário para executar o gerenciamento automático de recursos. Ele pode ser usado para realizar uma operação usando recursos, após o que ele libera os recursos na ordem inversa de sua criação.

chengpohi
fonte
Você poderia adicionar a Using.resourcevariante também?
Daniel C. Sobral
@DanielC.Sobral, claro, acabei de adicionar.
chengpohi
Como você escreveria isso para Scala 2.12? Aqui está um usingmétodo semelhante :def using[A <: AutoCloseable, B](resource: A) (block: A => B): B = try block(resource) finally resource.close()
Mike Slinn
75

A entrada do blog de Chris Hansen 'ARM Blocks in Scala: Revisited' de 26/03/09 fala sobre o slide 21 da apresentação FOSDEM de Martin Odersky . Este próximo bloco é retirado diretamente do slide 21 (com permissão):

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

--enviar citação--

Então podemos chamar assim:

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

Quais são as desvantagens dessa abordagem? Esse padrão parece abordar 95% de onde eu precisaria de gerenciamento automático de recursos ...

Editar: snippet de código adicionado


Edit2: estendendo o padrão de design - inspirando-se na withinstrução python e abordando:

  • declarações a serem executadas antes do bloco
  • re-lançamento de exceção dependendo do recurso gerenciado
  • lidar com dois recursos com uma única instrução using
  • manipulação de recursos específicos, fornecendo uma conversão implícita e uma Managedclasse

Isso ocorre com o 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
fonte
2
Existem alternativas, mas não quis dizer que há algo errado com isso. Eu só quero todas essas respostas aqui, no Stack Overflow. :-)
Daniel C. Sobral
5
Você sabe se existe algo assim na API padrão? Parece uma tarefa árdua ter que escrever isso para mim o tempo todo.
Daniel Darabos de
Já faz um tempo desde que isso foi postado, mas a primeira solução não fecha o fluxo interno se o construtor de saída lançar, o que provavelmente não acontecerá aqui, mas há outros casos em que isso pode ser ruim. O fechamento também pode jogar. Nenhuma distinção entre exceções fatais também. O segundo tem cheiro de código em todos os lugares e tem zero vantagens sobre o primeiro. Você até perde os tipos reais, então seria inútil para algo como um ZipInputStream.
steinybot
Como você recomenda fazer isso se o bloco retornar um iterador?
Jorge Machado
62

Daniel,

Recentemente, implantei a biblioteca scala-arm para gerenciamento automático de recursos. Você pode encontrar a documentação aqui: https://github.com/jsuereth/scala-arm/wiki

Esta biblioteca oferece suporte a três estilos de uso (atualmente):

1) Imperativo / para expressão:

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 continuação delimitado

Aqui está um 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()
    }
  }
}

O código faz uso de uma característica de tipo de recurso, portanto, é capaz de se adaptar à maioria dos tipos de recursos. Ele tem um fallback para usar tipagem estrutural em classes com um método close ou dispose. Verifique a documentação e deixe-me saber se você pensa em algum recurso útil para adicionar.

jsuereth
fonte
1
Sim, eu vi isso. Quero dar uma olhada no código, para ver como você realiza algumas coisas, mas estou muito ocupado agora. De qualquer forma, como o objetivo da pergunta é fornecer uma referência a um código ARM confiável, estou tornando esta a resposta aceita.
Daniel C. Sobral
18

Aqui está a solução de James Iry usando continuações:

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

Aqui estão as soluções com e sem continuação para comparação:

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
}

E aqui está a sugestão de melhoria 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
fonte
O uso de (new BufferedWriter (new FileWriter ("test_copy.txt"))) sofre de problemas quando o construtor BufferedWriter falha? cada recurso deve ser empacotado em um bloco de uso ...
Jaap
@Jaap Este é o estilo sugerido pela Oracle . BufferedWriternão lança exceções verificadas, portanto, se alguma exceção for lançada, o programa não deverá se recuperar dela.
Daniel C. Sobral
7

Vejo uma evolução gradual de 4 etapas para fazer ARM no Scala:

  1. Sem ARM: Sujeira
  2. Apenas fechamentos: melhor, mas vários blocos aninhados
  3. Continuação Mônada: Use For para nivelar o aninhamento, mas a separação não natural em 2 blocos
  4. Continuações de estilo direto: Nirava, aha! Esta também é a alternativa mais segura de tipo: um recurso fora do bloco withResource será um erro de tipo.
Mushtaq Ahmed
fonte
1
Lembre-se, o CPS no Scala é implementado por meio de mônadas. :-)
Daniel C. Sobral
1
Mushtaq, 3) Você pode fazer o gerenciamento de recursos em uma mônada que não seja a mônada da continuação 4) O gerenciamento de recursos usando meu código de continuações delimitado por withResources / recurso não é mais (e não menos) seguro do que "usando". Ainda é possível esquecer de gerenciar um recurso que precisa dele. compare usando (new Resource ()) {first => val second = new Resource () // opa! // usar recursos} // primeiro é fechado com Recursos {val primeiro = recurso (novo Recurso ()) val segundo = novo Recurso () // oops! // use recursos ...} // só é fechado primeiro
James Iry
2
Daniel, o CPS em Scala é como o CPS em qualquer linguagem funcional. São continuações delimitadas que usam uma mônada.
James Iry
James, obrigado por explicar isso bem. Sentado na Índia, eu só gostaria de estar lá para sua palestra BASE. Esperando para ver quando você colocará esses slides online :)
Mushtaq Ahmed
6

Há um ARM leve (10 linhas de código) incluído com os melhores arquivos. Veja: 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

Aqui está como ele é implementado se você não quiser a biblioteca inteira:

  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()
      }
    }
  }
patikrit
fonte
Isso é muito bom. Eu peguei algo semelhante a essa abordagem, mas defini um método mape flatMappara CloseableOps em vez de foreach, de modo que para compreensões não resultasse um travessível.
EdgeCaseBerg
1

Que tal usar 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
fonte
1

Outra alternativa é a mônada Lazy TryClose de Choppy. É muito bom com conexões de banco de dados:

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

Mais informações aqui: https://github.com/choppythelumberjack/tryclose

ChoppyTheLumberjack
fonte
0

Aqui está a resposta de @chengpohi, modificada para funcionar com Scala 2.8+, em vez de apenas Scala 2.13 (sim, também funciona com 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
fonte