Alternatif Manajemen Sumber Daya Otomatis apa yang ada untuk Scala?

102

Saya telah melihat banyak contoh ARM (manajemen sumber daya otomatis) di web untuk Scala. Tampaknya menjadi ritus peralihan untuk menulis satu, meskipun sebagian besar terlihat sangat mirip satu sama lain. Saya memang melihat contoh yang cukup keren menggunakan lanjutan.

Bagaimanapun, banyak dari kode itu memiliki kekurangan dari satu jenis atau lainnya, jadi saya pikir akan menjadi ide yang baik untuk memiliki referensi di Stack Overflow, di mana kita dapat memilih versi yang paling benar dan sesuai.

Daniel C. Sobral
sumber
Apakah pertanyaan ini akan menghasilkan lebih banyak jawaban jika itu bukan wiki komunitas? Perhatikan jika memilih jawaban dalam reputasi penghargaan wiki komunitas ...
huynhjl
2
referensi unik bisa menambahkan tingkat keamanan lain ke ARM untuk memastikan bahwa referensi ke sumber daya dikembalikan ke manajer sebelum close () dipanggil. thread.gmane.org/gmane.comp.lang.scala/19160/focus=19168
retronym
@retronym Saya pikir plugin keunikan akan menjadi sebuah revolusi, lebih dari sekedar kelanjutan. Dan, pada kenyataannya, saya pikir ini adalah satu hal di Scala yang kemungkinan besar akan dipindahkan ke bahasa lain dalam waktu yang tidak terlalu lama. Saat ini keluar, pastikan untuk mengedit jawaban yang sesuai. :-)
Daniel C. Sobral
1
Karena saya harus dapat menumpuk beberapa instance java.lang.AutoCloseable, yang masing-masing bergantung pada instance sebelumnya yang berhasil dibuat, akhirnya saya menemukan pola yang sangat berguna bagi saya. Saya menulisnya sebagai jawaban atas pertanyaan StackOverflow serupa: stackoverflow.com/a/34277491/501113
chaotic3quilibrium

Jawaban:

10

Untuk saat Scala 2.13 akhirnya didukung: try with resourcesdengan menggunakan Menggunakan :), Contoh:

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

atau menggunakan Using.resourcehindariTry

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

Anda dapat menemukan lebih banyak contoh dari Menggunakan doc.

Utilitas untuk menjalankan manajemen sumber daya otomatis. Ini dapat digunakan untuk melakukan operasi menggunakan sumber daya, setelah itu melepaskan sumber daya dalam urutan terbalik pembuatannya.

chengpohi
sumber
Bisakah Anda menambahkan Using.resourcevarian juga?
Daniel C. Sobral
@ DanielC.Sobral, tentu, baru saja menambahkannya.
chengpohi
Bagaimana Anda menulis ini untuk Scala 2.12? Berikut adalah usingmetode yang serupa :def using[A <: AutoCloseable, B](resource: A) (block: A => B): B = try block(resource) finally resource.close()
Mike Slinn
75

Entri blog Chris Hansen 'ARM Blocks in Scala: Revisited' dari 3/26/09 berbicara tentang slide 21 presentasi FOSDEM Martin Odersky . Blok berikutnya ini diambil langsung dari slide 21 (dengan izin):

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

--selesai kutipan--

Kemudian kita bisa memanggil seperti ini:

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

Apa kelemahan dari pendekatan ini? Pola itu tampaknya menangani 95% tempat saya memerlukan pengelolaan sumber daya otomatis ...

Edit: menambahkan cuplikan kode


Sunting2: memperluas pola desain - mengambil inspirasi dari withpernyataan python dan menangani:

  • pernyataan untuk dijalankan sebelum blok
  • membuang kembali pengecualian bergantung pada sumber daya yang dikelola
  • menangani dua sumber daya dengan satu pernyataan penggunaan tunggal
  • penanganan khusus sumber daya dengan menyediakan konversi implisit dan Managedkelas

Ini dengan 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
sumber
2
Ada alternatif lain, tapi saya tidak bermaksud menyiratkan ada yang salah dengan itu. Saya hanya ingin semua jawaban itu di sini, di Stack Overflow. :-)
Daniel C. Sobral
5
Apakah Anda tahu jika ada yang seperti ini di API standar? Sepertinya tugas menulis ini untuk diriku sendiri sepanjang waktu.
Daniel Darabos
Sudah lama sejak ini diposting tetapi solusi pertama tidak menutup aliran dalam jika konstruktor keluar melempar yang mungkin tidak akan terjadi di sini tetapi ada kasus lain di mana ini bisa menjadi buruk. Tutup juga bisa melempar. Tidak ada perbedaan antara pengecualian fatal juga. Yang kedua memiliki bau kode di mana-mana dan tidak memiliki keunggulan dibandingkan yang pertama. Anda bahkan kehilangan tipe sebenarnya sehingga tidak berguna untuk sesuatu seperti ZipInputStream.
steinybot
Bagaimana Anda merekomendasikan untuk melakukan ini jika blok mengembalikan iterator?
Jorge Machado
62

Daniel,

Saya baru saja menerapkan pustaka skala-lengan untuk manajemen sumber daya otomatis. Anda dapat menemukan dokumentasinya di sini: https://github.com/jsuereth/scala-arm/wiki

Pustaka ini mendukung tiga gaya penggunaan (saat ini):

1) Imperatif / untuk-ekspresi:

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

2) Bergaya monadik

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) Gaya Lanjutan Dibatasi

Berikut adalah server 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()
    }
  }
}

Kode tersebut menggunakan sifat tipe Sumber Daya, sehingga dapat beradaptasi dengan sebagian besar jenis sumber daya. Ini memiliki fallback untuk menggunakan pengetikan struktural terhadap kelas-kelas dengan metode tutup atau buang. Silakan lihat dokumentasinya dan beri tahu saya jika Anda memikirkan fitur praktis untuk ditambahkan.

jsuereth
sumber
1
Ya, saya melihat ini. Saya ingin melihat-lihat kodenya, untuk melihat bagaimana Anda mencapai beberapa hal, tapi saya terlalu sibuk sekarang. Bagaimanapun, karena tujuan pertanyaannya adalah untuk memberikan referensi ke kode ARM yang andal, saya menjadikan ini jawaban yang diterima.
Daniel C. Sobral
18

Inilah solusi James Iry menggunakan lanjutan:

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

Berikut solusi dengan dan tanpa kelanjutan untuk perbandingan:

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
}

Dan inilah saran perbaikan 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
sumber
Tidak menggunakan (BufferedWriter baru (FileWriter baru ("test_copy.txt"))) mengalami masalah saat konstruktor BufferedWriter gagal? setiap sumber daya harus dibungkus dalam blok penggunaan ...
Jaap
@Jaap Ini adalah gaya yang disarankan oleh Oracle . BufferedWritertidak membuang pengecualian yang dicentang, jadi jika ada pengecualian yang dilemparkan, program tidak diharapkan untuk pulih darinya.
Daniel C. Sobral
7

Saya melihat evolusi 4 langkah bertahap untuk melakukan ARM di Scala:

  1. Tanpa ARM: Kotoran
  2. Hanya penutupan: Lebih baik, tetapi beberapa blok bersarang
  3. Lanjutan Monad: Gunakan Untuk untuk meratakan sarang, tetapi pemisahan yang tidak wajar dalam 2 blok
  4. Kelanjutan gaya langsung: Nirava, aha! Ini juga merupakan alternatif yang paling aman bagi tipe: sumber daya di luar blok withResource akan menjadi kesalahan tipe.
Mushtaq Ahmed
sumber
1
Pikiran Anda, CPS di Scala diimplementasikan melalui monad. :-)
Daniel C. Sobral
1
Mushtaq, 3) Anda dapat melakukan manajemen sumber daya dalam monad yang bukan monad kelanjutan 4) Pengelolaan sumber daya menggunakan kode kelanjutan yang dibatasi withResources / sumber daya tidak lebih (dan tidak kurang) jenis aman daripada "menggunakan". Masih mungkin lupa untuk mengelola sumber daya yang membutuhkannya. bandingkan menggunakan (new Resource ()) {first => val second = new Resource () // oops! // gunakan sumber daya} // hanya pertama-tama ditutup dengan Sumber daya {val first = sumber daya (Sumber daya baru ()) val kedua = Sumber daya baru () // Ups! // gunakan sumber daya ...} // hanya tutup pertama
James Iry
2
Daniel, CPS di Scala seperti CPS dalam bahasa fungsional apa pun. Ini adalah kelanjutan terbatas yang menggunakan monad.
James Iry
James, terima kasih telah menjelaskannya dengan baik. Duduk di India Saya hanya bisa berharap saya ada di sana untuk BASE talk Anda. Menunggu untuk melihat kapan Anda meletakkan slide tersebut secara online :)
Mushtaq Ahmed
6

Ada ARM ringan (10 baris kode) yang disertakan dengan file yang lebih baik. Lihat: 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

Berikut adalah cara penerapannya jika Anda tidak menginginkan seluruh pustaka:

  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
sumber
Ini cukup bagus. Saya mengambil sesuatu yang mirip dengan pendekatan ini tetapi mendefinisikan mapdan flatMapmetode untuk CloseableOps daripada foreach sehingga untuk pemahaman tidak akan menghasilkan sebuah traversable.
EdgeCaseBerg
1

Bagaimana kalau menggunakan kelas 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
sumber
1

Alternatif lain adalah monad Choppy's Lazy TryClose. Ini cukup bagus dengan koneksi database:

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
}

Dan dengan aliran:

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
}

Info lebih lanjut di sini: https://github.com/choppythelumberjack/tryclose

ChoppyTheLumberjack
sumber
0

Berikut adalah jawaban @ chengpohi, dimodifikasi sehingga berfungsi dengan Scala 2.8+, bukan hanya Scala 2.13 (ya, ini juga berfungsi dengan 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
sumber