Какие альтернативы автоматическому управлению ресурсами существуют для Scala?

102

Я видел много примеров ARM (автоматического управления ресурсами) в Интернете для Scala. Написание одного из них кажется своеобразным обрядом, хотя большинство из них очень похожи друг на друга. Я сделал увидеть довольно прохладный пример использования продолжений, хотя.

Во всяком случае, у многих из этого кода есть недостатки того или иного типа, поэтому я подумал, что было бы неплохо иметь ссылку здесь на Stack Overflow, где мы можем проголосовать за наиболее правильные и подходящие версии.

Дэниел С. Собрал
источник
Сгенерил бы этот вопрос больше ответов, если бы это не была вики сообщества? Обратите внимание, что если проголосовали ответы в сообществе wiki Award, репутация ...
huynhjl
2
уникальные ссылки могут добавить в ARM еще один уровень безопасности, чтобы гарантировать, что ссылки на ресурсы будут возвращены менеджеру до вызова close (). thread.gmane.org/gmane.comp.lang.scala/19160/focus=19168
ретроним
@retronym Я думаю, что плагин уникальности будет настоящей революцией, в большей степени, чем продолжением. И, на самом деле, я думаю, что это одна из вещей в Scala, которая вполне может оказаться перенесенной на другие языки в не столь отдаленном будущем. Когда это выйдет, давайте обязательно отредактируем ответы соответствующим образом. :-)
Дэниел С. Собрал
1
Поскольку мне нужно иметь возможность вкладывать несколько экземпляров java.lang.AutoCloseable, каждый из которых зависит от успешного создания экземпляра предыдущего, я наконец нашел шаблон, который оказался для меня очень полезным. Я написал это как ответ на аналогичный вопрос StackOverflow: stackoverflow.com/a/34277491/501113
chaotic3quilibrium

Ответы:

10

На данный момент Scala 2.13 наконец-то поддерживает: try with resourcesс использованием Using :), Пример:

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

или используя Using.resourceизбежатьTry

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

Вы можете найти больше примеров в Использование doc.

Утилита для автоматического управления ресурсами. Его можно использовать для выполнения операции с использованием ресурсов, после чего он освобождает ресурсы в порядке, обратном их созданию.

оборота ченгпохи
источник
Не могли бы вы добавить еще и Using.resourceвариант?
Дэниел С. Собрал,
@ DanielC.Sobral, конечно, только что добавил.
chengpohi
Как бы вы написали это для Scala 2.12? Вот похожий usingметод:def using[A <: AutoCloseable, B](resource: A) (block: A => B): B = try block(resource) finally resource.close()
Майк Слинн 09
75

В записи блога Криса Хансена « Блоки ARM в Scala: новый взгляд» от 26 марта 2009 года говорится о слайде 21 презентации FOSDEM Мартина Одерски . Следующий блок взят прямо из слайда 21 (с разрешения):

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

- конец цитаты -

Тогда мы можем позвонить так:

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

В чем недостатки такого подхода? Этот шаблон, похоже, подходит для 95% случаев, когда мне нужно автоматическое управление ресурсами ...

Изменить: добавлен фрагмент кода


Edit2: расширение шаблона проектирования - вдохновение из withинструкции python и адресация:

  • операторы для запуска перед блоком
  • повторная генерация исключения в зависимости от управляемого ресурса
  • обработка двух ресурсов с помощью одного оператора using
  • обработка, зависящая от ресурса, путем предоставления неявного преобразования и Managedкласса

Это в 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
источник
2
Есть альтернативы, но я не имел в виду, что с этим что-то не так. Мне просто нужны все эти ответы здесь, на Stack Overflow. :-)
Дэниел С. Собрал
5
Вы знаете, есть ли что-то подобное в стандартном API? Похоже на утомительную работу постоянно писать это для себя.
Даниэль Дарабос
Прошло некоторое время с тех пор, как это было опубликовано, но первое решение не закрывает внутренний поток, если конструктор out выдает, что, вероятно, не произойдет здесь, но есть другие случаи, когда это может быть плохо. Конец тоже может бросить. Также нет различий между фатальными исключениями. У второго код везде пахнет и нет никаких преимуществ перед первым. Вы даже теряете фактические типы, поэтому это будет бесполезно для чего-то вроде ZipInputStream.
steinybot
Как вы рекомендуете это делать, если блок возвращает итератор?
Хорхе Мачадо,
62

Даниэль,

Недавно я развернул библиотеку scala-arm для автоматического управления ресурсами. Вы можете найти документацию здесь: https://github.com/jsuereth/scala-arm/wiki

Эта библиотека поддерживает три стиля использования (в настоящее время):

1) Императив / выражение для выражения:

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

2) Монадический стиль

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) Стиль продолжения с разделителями

Вот 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()
    }
  }
}

В коде используется характеристика типа ресурса, поэтому он может адаптироваться к большинству типов ресурсов. У него есть возможность использовать структурную типизацию для классов с помощью метода close или dispose. Пожалуйста, ознакомьтесь с документацией и дайте мне знать, если вы думаете о добавлении каких-либо полезных функций.

jsuereth
источник
1
Да, я это видел. Я хочу просмотреть код, чтобы увидеть, как вы справляетесь с некоторыми вещами, но сейчас я слишком занят. В любом случае, поскольку цель вопроса - предоставить ссылку на надежный код ARM, я делаю это принятым ответом.
Дэниел С. Собрал,
18

Вот решение Джеймса Айри с использованием продолжений:

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

Вот решения с продолжениями и без них для сравнения:

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
}

А вот предложение Тиарка Ромпфа по улучшению:

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
}
Дэниела С. Собрала
источник
Не возникает ли проблем при использовании (new BufferedWriter (new FileWriter ("test_copy.txt"))) при сбое конструктора BufferedWriter? каждый ресурс должен быть заключен в блок using ...
Jaap
@Jaap Это стиль, предложенный Oracle . BufferedWriterне генерирует проверенных исключений, поэтому, если возникает какое-либо исключение, не ожидается, что программа восстановится после него.
Дэниел С. Собрал,
7

Я вижу постепенное четырехэтапное развитие ARM в Scala:

  1. Нет руки: грязь
  2. Только замыкания: лучше, но несколько вложенных блоков
  3. Монада продолжения: используйте For, чтобы сгладить вложение, но неестественное разделение на 2 блока
  4. Прямые продолжения стиля: Нирава, ага! Это также наиболее безопасная альтернатива: ресурс вне блока withResource будет иметь ошибку типа.
Муштак Ахмед
источник
1
Имейте в виду, что CPS в Scala реализованы через монады. :-)
Дэниел С. Собрал
1
Mushtaq, 3) Вы можете управлять ресурсами в монаде, которая не является монадой продолжения. 4) Управление ресурсами с использованием моего кода продолжения с разделителями withResources / ресурсов не более (и не менее) типобезопасен, чем "using". Еще можно забыть управлять ресурсом, который в этом нуждается. сравнить using (new Resource ()) {first => val second = new Resource () // упс! // использование ресурсов} // только сначала закрывается withResources {val first = resource (new Resource ()) val second = new Resource () // упс! // использовать ресурсы ...} // закрывается только сначала
Джеймс Ири,
2
Даниэль, CPS в Scala похож на CPS на любом функциональном языке. Это ограниченные продолжения, использующие монаду.
Джеймс Ири
Джеймс, спасибо, что хорошо это объяснил. Находясь в Индии, я мог только пожелать, чтобы я присутствовал на вашем BASE-выступлении. Жду, когда вы выложите эти слайды в Интернет :)
Муштак Ахмед,
6

В состав better-files входит облегченная (10 строк кода) ARM. См. 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

Вот как это реализовано, если вам не нужна вся библиотека:

  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()
      }
    }
  }
патикрит
источник
Это очень мило. Я использовал что-то похожее на этот подход, но определил метод mapand flatMapдля CloseableOps вместо foreach, чтобы для понимания не получился обходной путь.
EdgeCaseBerg
1

Как насчет использования классов типов

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

Другой альтернативой является монада Lazy TryClose от Choppy. С подключениями к базе данных неплохо:

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
}

И с потоками:

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
}

Подробнее здесь: https://github.com/choppythelumberjack/tryclose

ChoppyTheLumberjack
источник
0

Вот ответ @chengpohi, измененный так, чтобы он работал со Scala 2.8+, а не только со Scala 2.13 (да, он также работает со 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
  }
Майк Слинн
источник