Search code examples
pythonrubylanguage-featureswith-statementcontrol-flow

equivalent of Python's "with" in Ruby


In Python, the with statement is used to make sure that clean-up code always gets called, regardless of exceptions being thrown or function calls returning. For example:

with open("temp.txt", "w") as f:
    f.write("hi")
    raise ValueError("spitespite")

Here, the file is closed, even though an exception was raised. A better explanation is here.

Is there an equivalent for this construct in Ruby? Or can you code one up, since Ruby has continuations?


Solution

  • Ruby has syntactically lightweight support for literal anonymous procedures (called blocks in Ruby). Therefore, it doesn't need a new language feature for this.

    So, what you normally do, is to write a method which takes a block of code, allocates the resource, executes the block of code in the context of that resource and then closes the resource.

    Something like this:

    def with(klass, *args)
      yield r = klass.open(*args)
    ensure
      r.close
    end
    

    You could use it like this:

    with File, 'temp.txt', 'w' do |f|
      f.write 'hi'
      raise 'spitespite'
    end
    

    However, this is a very procedural way to do this. Ruby is an object-oriented language, which means that the responsibility of properly executing a block of code in the context of a File should belong to the File class:

    File.open 'temp.txt', 'w' do |f|
      f.write 'hi'
      raise 'spitespite'
    end
    

    This could be implemented something like this:

    def File.open(*args)
      f = new(*args)
      return f unless block_given?
      yield f
    ensure
      f.close if block_given?
    end
    

    This is a general pattern that is implemented by lots of classes in the Ruby core library, standard libraries and third-party libraries.


    A more close correspondence to the generic Python context manager protocol would be:

    def with(ctx)
      yield ctx.setup
    ensure
      ctx.teardown
    end
    
    class File
      def setup; self end
      alias_method :teardown, :close
    end
    
    with File.open('temp.txt', 'w') do |f|
      f.write 'hi'
      raise 'spitespite'
    end
    

    Note that this is virtually indistinguishable from the Python example, but it didn't require the addition of new syntax to the language.