Project

General

Profile

Feature #13923

Idiom to release resources safely, with less indentations

Added by tagomoris (Satoshi TAGOMORI) 28 days ago. Updated 22 days ago.

Status:
Feedback
Priority:
Normal
Assignee:
-
Target version:
-
[ruby-core:82888]

Description

In programs which grabs and releases resources very often, we need to write so much begin-ensure clauses.

begin
  storage = getStorage()
  begin
    buffer = storage.get(buffer_id)

    # ...
  ensure
    buffer.close if buffer
  end
rescue StorageError => e
  # ...
ensure
  storage.close if storage
end

Such code makes our code fat, and difficult to understand.
I want to write such code like below:

# Class of storage and buffer should include a module (like Closeable)
# or be checked with respond_to?(:close)

begin(storage = getStorage(); buffer = storage.get(buffer_id)
  # ...
rescue StorageError => e
  # ...
end
# (buffer.close if buffer) rescue nil
# (storage.close if storage) rescue nil

Other languages also have similar features:

  • Java: try-with-resources
  • Python: with

History

#1 [ruby-core:82891] Updated by nobu (Nobuyoshi Nakada) 28 days ago

  • Description updated (diff)

Probably a way which is close to it right now would be:

begin
  storage = getStorage()
  buffer = storage.get(buffer_id)

  # ...
rescue StorageError => e
  # ...
ensure
  buffer&.close
  storage&.close
end

#2 [ruby-core:82894] Updated by tagomoris (Satoshi TAGOMORI) 28 days ago

nobu (Nobuyoshi Nakada) wrote:

Probably a way which is close to it right now would be:

begin
  storage = getStorage()
  buffer = storage.get(buffer_id)

  # ...
rescue StorageError => e
  # ...
ensure
  buffer&.close
  storage&.close
end

With this code, storage will not be closed if buffer.close raises exceptions.

#3 [ruby-core:82895] Updated by Eregon (Benoit Daloze) 28 days ago

tagomoris (Satoshi TAGOMORI) wrote:

With this code, storage will not be closed if buffer.close raises exceptions.

IMHO if buffer.close raises an exception then that should be fixed and not silently ignored.
(e.g. if close fails it could mean the contents is not properly flushed and might be very hard to debug).

A more common way to write code using resources in Ruby is to use blocks:

File.open("foo") do |file|
  file.read
end

#4 [ruby-core:82898] Updated by KonaBlend (Kona Blend) 28 days ago

maybe new keyword defer which simply builds an array of blocks to be called at end of scope;
specifically immediately before end of ensure section;
i.e. if there is ensure code then defer underlying support should come afterwards.

begin
  ## underlying support:
  # __deferred = []

  r0 = Expensive.make_resource # might return nil or become nil
  defer { r0.close if r0 } 
  ## underlying support:
  # __deferred.unshift(block)

  ## sugar
  r1 = Expensive.make_resource 
  defer(r1)
  ## equivalent:
  # defer { r1.close if r1 }

  ## sugar
  defer r2 = Expensive.make_resource
  ## equivalent:
  # defer { r2.close if r2 }
ensure
  ## underlying support:
  # __deferred.each { |block| block.call }
end

note: edit: __deferred.push to __deferred.unshift for FILO ordering of block calls

#5 [ruby-core:82900] Updated by matz (Yukihiro Matsumoto) 28 days ago

I like defer idea, although adding a new keyword is hard.

Matz.

#6 [ruby-core:82947] Updated by nobu (Nobuyoshi Nakada) 24 days ago

While ensure in ruby is a syntax and share the scope, defer in go dynamically registers the clean-up code with capturing local variables implicitly.
So a defer in a loop may registers multiple times with different objects, or may not register by a condition.

package main

import (
    "fmt"
)

type foo struct {
    n int
}

func Create(n int) *foo {
    fmt.Printf("Creating %v\n", n)
    return &foo{n}
}

func Delete(f *foo) {
    fmt.Printf("Deleting %v\n", f.n)
}

func main() {
    fmt.Println("Start")
    for i := 1; i <= 3; i++ {
        f := Create(i)
        if i % 2 != 0 {
            defer Delete(f)
        }
    }
    fmt.Println("Done")
}

shows

$ go run test.go 
Start
Creating 1
Creating 2
Creating 3
Done
Deleting 3
Deleting 2
Deleting 1

I think that Kernel#defer in Kona's proposal would be possible, but it would be hard to capture local variables.

#7 [ruby-core:82952] Updated by nobu (Nobuyoshi Nakada) 24 days ago

How about:

class Deferred
  def initialize
    @list = []
    yield self
  ensure
    @list.reverse_each do |res, clean|
      clean.(res)
    end
  end
  def defer(res = true, &clean)
    @list << [res, clean||CLOSE] if res
    res
  end
  CLOSE = :close.to_proc
end

if $0 == __FILE__
  class Resource
    @@number = 0
    def initialize
      @num = @@number += 1
    end
    def close
      puts "#@num closed"
    end
    def delete
      puts "#@num deleted"
    end
  end

  Deferred.new do |d|
    r0 = Resource.new
    d.defer {r0&.close}

    r1 = Resource.new
    d.defer r1

    r2 = d.defer Resource.new
    abort unless Resource === r2

    r3 = d.defer(Resource.new) {|r|r.delete}
  end
end

#8 [ruby-core:82953] Updated by Eregon (Benoit Daloze) 24 days ago

IMHO this defer idea is not appropriate to manage resources at it just moves the release of the resources at the end of the method.
But this is often not appropriate or later than needed.
A block captures more explicitly the scope in which the resource is needed.
Moreover, if moving the release of the resource is needed, moving the closing }/end of the block is enough and much simpler than creating a new method to use defer for a shorter scope.
It is generally unwise to hold resources longer than necessary, particularly locks.

Taking the original example, and using blocks:

begin
  getStorage() do |storage|
    storage.get(buffer_id) do |buffer|
      # ...
    end
  end
rescue StorageError => e
  # ...
end

This naturally prevents using the resource after #close and clearly delimits when the resource is available.

#9 [ruby-core:82996] Updated by shyouhei (Shyouhei Urabe) 23 days ago

  • Status changed from Open to Feedback

We looked at this issue in a developer meeting today and nobu's library solution written in comment #7 was popular there.

Is that OK for you, or you still want a new syntax than a library (and if that is the case; can you tell us why)?

#10 [ruby-core:83006] Updated by KonaBlend (Kona Blend) 23 days ago

shyouhei (Shyouhei Urabe) wrote:

We looked at this issue in a developer meeting today and nobu's library solution written in comment #7 was popular there.

Is that OK for you, or you still want a new syntax than a library (and if that is the case; can you tell us why)?

fwiw, combined with new #12906, comment #7 makes me happy.

#11 [ruby-core:83017] Updated by tagomoris (Satoshi TAGOMORI) 22 days ago

shyouhei (Shyouhei Urabe) wrote:

We looked at this issue in a developer meeting today and nobu's library solution written in comment #7 was popular there.

Is that OK for you, or you still want a new syntax than a library (and if that is the case; can you tell us why)?

I prefer to get resources at once, but #7 looks to work well for my use case.
Deferred is ok for me too.
Can we expect that it's bundled as standard library in ruby?

#12 [ruby-core:83018] Updated by shyouhei (Shyouhei Urabe) 22 days ago

tagomoris (Satoshi TAGOMORI) wrote:

I prefer to get resources at once, but #7 looks to work well for my use case.
Deferred is ok for me too.
Can we expect that it's bundled as standard library in ruby?

No, no plan to bundle it yet. These days it's getting hard to include a new standard library unless there are very good reason for it to avoid being a normal gem. If you think it's worth, please add a separate feature request describing that reason. Thank you in advance.

Also available in: Atom PDF