Jakie alternatywy automatycznego zarządzania zasobami istnieją dla Scala?

102

Widziałem wiele przykładów ARM (automatycznego zarządzania zasobami) w Internecie dla Scala. Wydaje się, że napisanie takiego rytuału jest rytuałem przejścia, chociaż większość z nich wygląda bardzo podobnie. Widziałem jednak całkiem fajny przykład z użyciem kontynuacji.

W każdym razie, wiele z tego kodu ma wady tego czy innego typu, więc pomyślałem, że dobrym pomysłem byłoby umieszczenie tutaj odniesienia do Stack Overflow, gdzie możemy głosować na najbardziej poprawne i odpowiednie wersje.

Daniel C. Sobral
źródło
Czy to pytanie przyniosłoby więcej odpowiedzi, gdyby nie była wiki społeczności? Pamiętaj, że jeśli głosowałeś odpowiedzi na reputację nagrody wiki społeczności ...
huynhjl
2
unikalne odwołania mogą dodać kolejny poziom bezpieczeństwa do ARM, aby zapewnić, że odwołania do zasobów zostaną zwrócone do menedżera przed wywołaniem metody close (). thread.gmane.org/gmane.comp.lang.scala/19160/focus=19168
retronym
@retronym Myślę, że wtyczka wyjątkowości będzie sporą rewolucją, bardziej niż kontynuacją. I faktycznie, myślę, że jest to jedna rzecz w Scali, która prawdopodobnie zostanie przeniesiona na inne języki w niezbyt odległej przyszłości. Kiedy to się pojawi, pamiętajmy, aby odpowiednio edytować odpowiedzi. :-)
Daniel C. Sobral
1
Ponieważ potrzebuję mieć możliwość zagnieżdżenia wielu instancji java.lang.AutoCloseable, z których każda zależy od pomyślnego utworzenia instancji poprzedniej, w końcu trafiłem na wzorzec, który był dla mnie bardzo przydatny. Napisałem to jako odpowiedź na podobne pytanie StackOverflow: stackoverflow.com/a/34277491/501113
chaotic3quilibrium

Odpowiedzi:

10

Na razie Scala 2.13 wreszcie obsługuje: try with resourcesprzy użyciu użycia :), Przykład:

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

lub używając Using.resourceunikajTry

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

Więcej przykładów można znaleźć w sekcji Korzystanie z doc.

Narzędzie do wykonywania automatycznego zarządzania zasobami. Może służyć do wykonywania operacji z wykorzystaniem zasobów, po czym zwalnia zasoby w kolejności odwrotnej do ich tworzenia.

chengpohi
źródło
Czy mógłbyś dodać również Using.resourcewariant?
Daniel C. Sobral
@ DanielC.Sobral, oczywiście, właśnie to dodał.
chengpohi
Jak byś to napisał dla Scali 2.12? Oto podobna usingmetoda:def using[A <: AutoCloseable, B](resource: A) (block: A => B): B = try block(resource) finally resource.close()
Mike Slinn,
75

Wpis Chrisa Hansena „ARM Blocks in Scala: Revisited” z 26.03.2009 mówi o slajdzie 21 z prezentacji FOSDEM Martina Odersky'ego . Następny blok pochodzi bezpośrednio ze slajdu 21 (za zgodą):

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

- wyślij wycenę -

Wtedy możemy zadzwonić w ten sposób:

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

Jakie są wady tego podejścia? Wydaje się, że ten wzorzec dotyczy 95% obszarów, w których potrzebowałbym automatycznego zarządzania zasobami ...

Edycja: dodano fragment kodu


Edit2: rozszerzenie wzorca projektowego - czerpanie inspiracji z withinstrukcji Pythona i adresowanie:

  • instrukcje do uruchomienia przed blokiem
  • ponowne zgłaszanie wyjątku w zależności od zarządzanego zasobu
  • obsługa dwóch zasobów za pomocą jednej instrukcji using
  • obsługa specyficzna dla zasobów poprzez dostarczenie niejawnej konwersji i Managedklasy

To jest w przypadku 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
źródło
2
Są alternatywy, ale nie chciałem sugerować, że jest w tym coś nie tak. Chcę tylko tych odpowiedzi tutaj, w Stack Overflow. :-)
Daniel C. Sobral
5
Czy wiesz, czy w standardowym API jest coś takiego? Pisanie tego przez cały czas wydaje mi się przykrym obowiązkiem.
Daniel Darabos,
Minęło trochę czasu, odkąd to zostało opublikowane, ale pierwsze rozwiązanie nie zamyka strumienia wewnętrznego, jeśli konstruktor wyjściowy wyrzuci, co prawdopodobnie nie nastąpi tutaj, ale są inne przypadki, w których może to być złe. Zamknięcie może również rzucić. Nie ma też rozróżnienia między fatalnymi wyjątkami. Drugi kod pachnie wszędzie i ma zerową przewagę nad pierwszym. Tracisz nawet rzeczywiste typy, więc byłby bezużyteczny dla czegoś takiego jak ZipInputStream.
steinybot
Jak radzisz to zrobić, jeśli blok zwraca iterator?
Jorge Machado
62

Daniel,

Niedawno wdrożyłem bibliotekę Scala-Arm do automatycznego zarządzania zasobami. Dokumentację można znaleźć tutaj: https://github.com/jsuereth/scala-arm/wiki

Ta biblioteka obsługuje trzy style użytkowania (obecnie):

1) Tryb rozkazujący / do wyrażenia:

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

2) Styl monadyczny

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) Styl z ograniczonymi kontynuacjami

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

Kod wykorzystuje cechę typu zasobu, dzięki czemu jest w stanie dostosować się do większości typów zasobów. Ma rezerwę do używania typowania strukturalnego dla klas z metodą close lub dispose. Zapoznaj się z dokumentacją i daj mi znać, jeśli myślisz o przydatnych funkcjach do dodania.

jsuereth
źródło
1
Tak, widziałem to. Chcę przejrzeć kod, aby zobaczyć, jak wykonujesz pewne rzeczy, ale jestem teraz zbyt zajęty. Zresztą, ponieważ celem pytania jest podanie odniesienia do wiarygodnego kodu ARM, uważam to za akceptowaną odpowiedź.
Daniel C. Sobral
18

Oto rozwiązanie Jamesa Iry'ego wykorzystujące kontynuacje:

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

Oto rozwiązania z kontynuacjami do porównania i bez nich:

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
}

A oto sugestia Tiarka Rompfa dotycząca poprawy:

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
źródło
Czy używanie (new BufferedWriter (new FileWriter ("test_copy.txt"))) nie powoduje problemów, gdy konstruktor BufferedWriter nie działa? każdy zasób powinien być opakowany w blok using ...
Jaap,
@Jaap To styl sugerowany przez Oracle . BufferedWriternie zgłasza sprawdzonych wyjątków, więc jeśli zostanie zgłoszony jakikolwiek wyjątek, program nie powinien go odzyskać.
Daniel C. Sobral
7

Widzę stopniową 4-stopniową ewolucję robienia ARM w Scali:

  1. Brak ARM: brud
  2. Tylko domknięcia: lepsze, ale wiele zagnieżdżonych bloków
  3. Monada kontynuacyjna: Użyj For, aby spłaszczyć zagnieżdżenie, ale nienaturalne oddzielenie w 2 blokach
  4. Kontynuacja stylu bezpośredniego: Nirava, aha! Jest to również najbardziej bezpieczna alternatywa: zasób poza blokiem withResource będzie miał błąd typu.
Mushtaq Ahmed
źródło
1
Pamiętaj, że CPS w Scali są implementowane przez monady. :-)
Daniel C. Sobral
1
Mushtaq, 3) Możesz zarządzać zasobami w monadzie, która nie jest monadą kontynuacji. 4) Zarządzanie zasobami przy użyciu mojego kodu kontynuacji z podziałem na zasoby / zasoby nie jest bardziej (i nie mniej) bezpieczne niż „używanie”. Nadal można zapomnieć o zarządzaniu zasobem, który tego potrzebuje. porównaj używając (new Resource ()) {first => val second = new Resource () // ups! // użyj zasobów} // tylko najpierw zostanie zamknięte za pomocąResources {val first = resource (new Resource ()) val second = new Resource () // ups! // użyj zasobów ...} // tylko pierwsze zostanie zamknięte
James Iry,
2
Daniel, CPS w Scali jest jak CPS w każdym języku funkcjonalnym. To ograniczone kontynuacje, które używają monady.
James Iry,
James, dzięki za dobre wyjaśnienie. Siedząc w Indiach, mógłbym tylko żałować, że nie jestem tam na rozmowie z BASE. Czekam, kiedy umieścisz te slajdy w sieci :)
Mushtaq Ahmed
6

Jest lekki (10 linii kodu) ARM dołączony do lepszych plików. Zobacz: 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

Oto jak to jest zaimplementowane, jeśli nie chcesz całej biblioteki:

  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
źródło
To jest całkiem miłe. Podjąłem coś podobnego do tego podejścia, ale zdefiniowałem a mapi flatMapmetodę dla CloseableOps zamiast foreach, aby dla zrozumienia nie dawała przemierzalnego.
EdgeCaseBerg
1

Co powiesz na używanie klas 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
źródło
1

Inną alternatywą jest monada Lazy TryClose Choppy'ego. Całkiem dobrze z połączeniami z bazą danych:

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
}

Oraz ze strumieniami:

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
}

Więcej informacji tutaj: https://github.com/choppythelumberjack/tryclose

ChoppyTheLumberjack
źródło
0

Oto odpowiedź @ chengpohi, zmodyfikowana tak, aby działała ze Scalą 2.8+, zamiast tylko ze Scalą 2.13 (tak, działa również ze Scalą 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
źródło