Why Ruby blocks exist, part III

Never forget to clean up again!

Previous posts in this series used Ruby blocks to iterate over the items in a collection. We’re going to wrap up by showing a very different side of blocks – resource allocation and cleanup.

Close the door!

Here we have a Ruby class representing a refrigerator. Prior to accessing a refrigerator object’s contents via its `contents` method, you have to call its `open` method to open the door. (Sensible enough.)

 
class Refrigerator

  # The "initialize" method gets called when a new instance is created.
  def initialize
    # This instance variable tracks whether the door is open.
    @door_state = "closed"
    # This array will hold the food.
    @contents = []
  end

  # This method controls access to the fridge's contents.
  def contents
    # If the door is open, we can get the food.
    if @door_state == "open"
      return @contents
    # If not, we'll get a warning instead.
    else
      warn "Door is closed!"
    end
  end

  # This method opens the door, granting access to the food.
  def open
    @door_state = "open"
  end

  # This method closes the door.
  def close
    @door_state = "closed"
  end

end

We can create a new refrigerator instance, open its door, fill it with food, and close the door again.

 
fridge = Refrigerator.new
fridge.open
fridge.contents << 'eggs' # Appends to the array returned by "contents".
fridge.contents << 'milk' # Ditto.
fridge.close

If we try and access the contents with the door closed, we’ll get a warning, and will have to open the door before trying again.

 
puts fridge.contents # Prints error "Door is closed!"
fridge.open
puts fridge.contents # Prints "eggs" and "milk".

Observant (and thrifty) readers probably noticed that the last code sample leaves the virtual refrigerator door hanging wide open. Food spoilage and energy waste! We can’t have that. We’ve got to remember to close the door again after we open it.

 
fridge.close

Let the door close itself!

…Or do we? Maybe we could add a method to automatically close the fridge again after we print its contents!

 
class Refrigerator

  def print_contents
    open
    puts contents
    close
  end

end

Of course, if we do that, we should probably add a similar method for adding items to the refrigerator:

 
class Refrigerator

  def add(item)
    open
    contents << item
    close
  end

end

…And maybe another method for removing items, and… Wow, there are a lot of operations where we have to open the fridge first, and close it again after! We can’t possibly write methods for all of them.

Previously, we learned that a Ruby method can hand control off to a block, and pass it a value as well. When the code within the block finishes executing, control returns to the method.

 
def my_method
  puts "In the method!"
  yield "a value"
  puts "Back in the method!"
end

my_method { |value| puts "In the block - got #{value} from my_method!" }

The above code prints:

In the method!
In the block - got a value from my_method!
Back in the method!

Maybe we can use these features of blocks to write a version of open that will let us run whatever code we want on the refrigerator’s contents, then close it again when we’re done…

 
class Refrigerator

  def open
    @door_state = "open" # Open the door, as before.
    yield contents # Pass the refrigerator's contents to the block.
    # When the block completes, control returns here.
    close # Close the door, now that we're done accessing contents.
  end

end

Let’s try it out!

 
fridge = Refrigerator.new

# "open" sets "@door_state", then passes the fridge contents to the block.
fridge.open do |contents|
  contents << 'eggs' # "contents" contains a reference to the array.
  contents << 'milk' # We can do whatever we want with it.
end
# The block exits, and control returns to the "open" method,
# which then calls "close" for us.

# If we call "open" again, we can repeat the whole process.
fridge.open do |contents|
  puts contents # ...But we can include different code inside the block.
end
# The fridge automatically gets closed again when we're done!

This is great! We can use blocks to run any operations we need on the fridge contents, and we still don’t have to worry about calling the close method!

…Okay, knowing how to work with virtual refrigerators efficiently isn’t very useful, we admit. Let’s try something a little more practical…

Close the file!

No matter what programming language you’re coming from, if you’ve worked with reading or writing files before, you’ve (hopefully) been told the importance of remembering to close the file handles when you’re done with them.

 
file = File.open("test.txt") # Opens the named file, returning a file handle.
puts file.read # Uses the file handle to read the file contents.
file.close # Closes the file again, freeing system resources.

Ruby supports this style of coding, like any other language. But Ruby’s File.open method also can take a block, to which it will yield the file handle as a parameter. And just like the open method on our Refrigerator class, File.open will automatically close the file for you when the block exits!

 
# "File.open" opens the file, then passes the new handle to the block.
File.open("test.txt") do |file|
  puts file.read # We can do whatever we want with the file handle.
end
# When the block exits, control returns to "File.open",
# which then closes the file handle for us.

Any time you’ve got a process with a well-defined beginning and end (like allocating and deallocating a resource), but unknown steps in the middle, Ruby blocks are a great way to implement it. With blocks, you’ll never again forget to clean up after yourself!

tags: , , ,