This is an excerpt from my latest book, The Ruby Closures Book. If you like it, consider buying it! <3

Managing Resources with Blocks

Blocks are an excellent way to abstract pre and post processing. A wonderful example is how resource management is managed. Examples include opening and closing file handlers, socket connections, database connections etc.

In other languages (C and Java, I’m looking at you), remembering to open and close the resource is a largely manual affair. This is both painful and ugly:

f = File.open('Leo Tolstoy - War and Peace.txt', 'w')
f << "Well, Prince, so Genoa and Lucca" 
f << " are now just family estates of the Buonapartes."
f.close

If you omit f.close, the file will remain open until the script terminates. In other words, you get a resource leak. If you have a long running application like a daemon or web application, then this is bad news. That’s because the operating system can only handle a finite number of file handles. If you the long running daemon continuously opens files and doesn’t close them, soon enough the file handles run out, and you’ll get a 3 a.m. call. Happy times.

If you think about it, what we really want is to write to the file. Having to remember to close the file handle is a hassle.

Ruby has a very elegant way of doing this:

File.open('Leo Tolstoy - War and Peace.txt', 'w') do |f|
  f << "Well, Prince, so Genoa and Lucca" 
  f << " are now just family estates of the Buonapartes."
end

By passing in a block into File.open, Ruby helps you, the over-burdened (and downright lazy) developer, to close the file handle when you are done writing the program. Notice that the file handle is nicely scoped within the block.

Implementing File.open

How is this done? Let’s learn to do this ourselves. First of all, the Ruby documentation provides and excellent overview of File.open:

With no associated block, File.open is a synonym for ::new. If the optional code block is given, it will be passed the opened file as an argument and the File object will automatically be closed when the block terminates. The value of the block will be returned from File.open.

This tells us everything we need to implement File.open:

a) If there’s no block given, File.open is the same as File.new:

class File
  def self.open(name, mode)
      new(name, mode) unless block_given?
  end
end

b) If there’s a block, the block is then passed the opened file as an argument …

class File
  def self.open(name, mode, &block)
    file = new(name, mode)
    return file unless block_given?
    yield(file)
  end
end

c) … and the file is automatically closed when the block terminates

class File
  def self.open(name, mode, &block)
    file = new(name, mode)
    return file unless block_given?
    yield(file)
    file.close
  end
end

There’s a subtlety to this. What happens if an exception is raised in the block? file.close will not be called! Thankfully, that’s an easy fix with the ensure keyword:

class File
  def self.open(name, mode, &block)
    file = new(name, mode)
    return file unless block_given?
    yield(file)
  ensure
    file.close
  end
end

Now, file.close is always guaranteed to close properly.

d) The value of the block will be returned from File.open.

Since yield(file) is the last line, the value of the block will be returned from File.open.

In the book, I placed little exercises at the end of sections that let you test your understanding of the concepts that were just presented. Solutions are also included!

Exercises

  1. Implement File.open. Start off with the Ruby Documentation on File.open. The key here is to understand where to put pre and post processing code, where to put yield, and ensuring that resources are cleared up.

  2. Real-world Ruby code Ruby Redis Library: Here is some code adapted from the Ruby Redis library:

module Redis
  class Server
    # ... more code ...

    def run
      loop do
        session = @server.accept

        begin
          return if yield(session) == :exit
        ensure
          session.close
        end
      end
    rescue => ex
      $stderr.puts "Error running server: #{ex.message}"
      $stderr.puts ex.backtrace
    ensure
      @server.close
    end

    # ... more code ...
  end
end

Notice the similarities to the File.open example. Does run require a block to be passed in? How is the return result of the block used? How could this code be called?

Solutions

  1. Your final code should look something like this:
class File
  def self.open(name, mode, &block)
    file = new(name, mode)
    return file unless block_given?
    yield(file)
  ensure
    file.close
  end
end
  1. Let’s go through the answers in order:

a) Does run require a block to be passed in?

Yes. There is no block_given?, and yield is called without any conditionals.

b) How is the return result of the block used?

The return result of the block is compared with :exit.

c) How could this code be called?

The key here is that the block passed has exactly one argument:

Server.new.run do |session|
  # do something with session
end

Thanks for Reading!

Hope you learned something! For more block patterns and other fun learnings, do check out the book that I put together.