Blocks are an excellent way to abstract pre- and post-processing. A wonderful example of that is resource management. Examples of resources that require extra care include file handles, socket connections, and database connections. For example, failure to close a database connection means that down the line, another connection attempt might be refused, since the number of connections that a database can handle is finite and limited.
Remembering to open and close the resource is a largely manual affair. This is error-prone and requires a bit of boilerplate. In the following example, the programmer is trying to open a file and write a few lines to it. The last line is where the programmer closes the file handle:
| 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 |
What happens if the programmer forgets to close the file with f.close? The severity depends on how long the program runs. If this code were to be part of a one-off script, then the situation wouldn’t be that bad. The file handle would be terminated once the script finished execution. But 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 support a finite number of file handles. If the long-running daemon continuously opens files and doesn’t close them, soon enough the file handles will run out, and you’ll get a call or page in the middle of the night. In other words, you have a resource leak on your hands.
If you think about it, the only thing 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, using blocks:
| 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’re done writing the program. Notice that the file handle is nicely scoped within the block. In other words, f only exists within the confines of the block. But where exactly is the file closing taking place? Let’s find out.
Let’s unravel the mysteries of File.open. First of all, the Ruby documentation[3] provides an excellent overview of File.open. If you read carefully, it even provides hints of how it is implemented:
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 description alone is enough to kickstart our File.open implementation. Create file_open.rb and follow along.
If no block is given, File.open is the same as File.new:
| class File |
| def self.open(name, mode) |
| new(name, mode) unless block_given? |
| end |
| end |
If there’s a block, the block is then passed the opened file as an argument...
| class File |
| def self.open(name, mode) |
| file = new(name, mode) |
| return file unless block_given? |
» | yield(file) |
| end |
| end |
...and the file is automatically closed when the block terminates...
| class File |
| def self.open(name, mode) |
| file = new(name, mode) |
| return file unless block_given? |
| yield(file) |
» | file.close |
| end |
| end |
There’s a gotcha here. What happens if an exception is raised in the block? file.close will not be called, which defeats the whole point of this exercise. Thankfully, this is an easy fix with the ensure keyword:
| class File |
| def self.open(name, mode) |
| 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.
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.
Let’s see if this works. Open file_open.rb in irb:
| % irb -r ./file_open.rb |
Let’s get meta and open the file you just opened:
| >> File.open("file_open.rb", "r") do |f| |
| >> puts f.path |
| >> puts f.ctime |
| >> puts f.size |
| >> end |
| file_open.rb |
| 2016-11-13 08:32:24 +0800 |
| 238 |
| => nil |
With a little bit of work, File.open frees you from having to remember to close file handles, handles exceptional cases, and to top it off, lets you do this in a simple and beautiful API. Speaking of beautiful, blocks are also great for object initialization, as you will soon see in the next section.