Blocks are one of the most powerful and often overlooked features of ruby. I must confess that it took me a while to figure out how ruby blocks work and how they can be useful in practice.
There is something about yield that makes blocks very hard to understand at first. I’m going to talk about some of the concepts and provide a few examples so by the end of this post you’ll have a solid understanding of ruby blocks.
What is a ruby block?
A ruby block is one or more lines of code that you put inside the do and end keywords (or { and } for inline blocks). It allows you to group code into a standalone unit that you can use as a method argument.
Oh well, I guess there are a few more things to say about blocks.
Both the multi-line version and the inline will do the exact same thing so it’s up to you which one you choose. As a general rule of thumb, it’s better to use the do/end version if your code has more than one line, just to make it easier to read.
Here’s a basic example of a multi-line block:
[1, 2, 3].each do |n|
# Prints out a number
puts "Number #{n}"
end
It’s called a multi-line block because it’s not inline, not because it’s got more than one line of code. The same exact thing can be written inline.
[1, 2, 3].each {|n| puts "Number #{n}"}
Both versions will print numbers 1, 2 and 3 in that order. The little n between the pipes (|n|) is called a block parameter and it’s value in this case is going to be each of the numbers in turn, in the order they are listed inside the array (you can learn more about the each method here).
So for the first iteration, the value of n will be 1, then for the second iteration, the value will be 2, and then 3.
Number 1
Number 2
Number 3
One thing to note is that any method can receive a block. Doesn’t matter if it uses it or not. Here’s an example.
def my_method
puts "something"
end
my_method { "hello" } # => "something"
In the example above, the block is ignored. But no one stops you from passing the block to the method.
How yield works
This keyword is responsible for most of my confusion around ruby blocks. It took me forever to get it.
def my_method
puts "reached the top"
yield
puts "reached the bottom"
end
my_method do
puts "reached yield"
end
# output
reached the top
reached yield
reached the bottom
So basically when the execution of my_method reaches the line with the call to yield, the code inside the block gets executed. Then, when the code inside the block finishes, the execution of my_method continues.
Passing blocks to methods
Remember how you can pass a block to a method whether it wants it or not?
Well, if you call yield inside the method, then the block parameter becomes mandatory and the method will raise an exception if it doesn’t receive a block.
If you want to make the block an optional, you can use the block_given? method which will return either true or false depending on if a block was passed in to the method or not.
Yield takes parameters too
Any parameter passed to yield will serve as a parameter to the block. So when the block runs, it can use the parameters passed in from the original method. Those parameters can be variables local to the method in which yield lives in.
The order of the arguments is important because the order you use to pass in the parameters is the order in which the block receives them.
One thing to note here is that the parameters inside the block (i.e. name and age) are local to the block. That means you can’t use them outside of the block. Let’s try it.
def my_method
yield("John", 2)
puts "Hi #{name}"
end
my_method { |name, age| puts "#{name} is #{age} years old" }
# output
John is 2 years old
NameError: undefined local variable or method `name' for #<IRB::...>
As you can see, name is not available to my_method because it’s local to the block.
Return value
yield returns the last evaluated expression (from inside the block). So in other words, the value that yield returns is the value the block returns.
def my_method
value = yield
puts "value is: #{value}"
end
my_method do
2
end
value is 2
What does &block (ampersand parameter) mean?
Here’s what &object does:
- if the object is a block, it converts it to a Proc.
- if the object is a Proc, it converts it to a block.
- if the object is something else, it calls to_proc on it, and then converts it to a block.
So let’s test this. First by examining the object as a block.
def a_method(&block)
block
end
a_method { "x" } # => #<Proc:...>
Now let’s see what happens when the object is a Proc.
a_proc = Proc.new { "x" }
a_method(&a_proc) # => #<Proc:...>
Since the argument is already a Proc, it’s converted to a block.
As a sidenote here, if the object is a Proc, it’s lambda? status is perserved. Meaning, you get the benefits of a lambda. Namely argument checking, and having them return values.
a_lambda = -> () { "x" } => #<Proc:... (lambda)>
a_method(&a_lambda) # => #<Proc:... (lambda)>
And finnaly, when you pass it a something that’s not a block or a Proc.
a_method(&:even?) # => #<Proc:...>
This is because calling Symbol#to_proc returns a Proc that can take an object and calls the method you specified on it. That’s confusing, I know. So let’s see an example.
a_proc = :foobar.to_proc
a_proc.call("some string")
# => NoMethodError: undefined method `foobar' for "some string":String
So let’s look again at what that does.
- Calling to_proc on the symbol :foobar returns a new Proc (i.e. a_proc).
- a_proc will call the foobar method on any object you send it.
Here’s what to_proc would look like if you were to redefine it in Ruby.
class Symbol
def to_proc
Proc.new { |obj, *args| obj.send(self, *args) }
end
end
How does .map(&:something) work?
Map makes for a good example of using the ampersand notation. If you want to learn all about map, head over to How to Use the Ruby Map Method to read more.
But the gist of it is, the &:something syntax is a shorter way of writing the following.
.map { |element| element.something }
Iterators and how to build one yourself
You can call yield
as many times as you want inside a method. That’s basically how iterators work. Calling yield
for each of the elements in the array mimics the behavior of the built in ruby iterators.
Let’s see how you can write a method similar to the map
method in ruby.
def my_map(array)
new_array = []
for element in array
new_array.push yield element
end
new_array
end
my_map([1, 2, 3]) do |number|
number * 2
end
# output
2
4
6
Initialize objects with default values
A cool pattern you can use with ruby blocks is to initialize an object with default values. You’ve probably seen this pattern if you’ve ever ventured into a .gemspec
file from any ruby gem.
The way it works is, you have an initializer that calls yield(self)
. In the context of the initialize
method, self
is the object being initialized.
class Car
attr_accessor :color, :doors
def initialize
yield(self)
end
end
car = Car.new do |c|
c.color = "Red"
c.doors = 4
end
puts "My car's color is #{car.color} and it's got #{car.doors} doors."
# output
My car's color is Red and it's got 4 doors.
Ruby blocks examples
Examples are all the rage these days so let’s try to find a few interesting ways of using blocks in real world (or as close to real world as possible) scenarios.
Wrap text in html tags
Blocks are the perfect candidate whenever you need to wrap a chunk of dynamic code within some static code. So for example if you want to generate an html tag for some text. The text is the dynamic part (cause you never know what you’ll want to wrap) and the tags are the static part, they never change.
def wrap_in_h1
"<h1>#{yield}</h1>"
end
wrap_in_h1 { "Here's my heading" }
# => "<h1>Here's my heading</h1>"
wrap_in_h1 { "Ha" * 3 }
# => "<h1>HaHaHa</h1>"
Note that the power of using blocks over methods is when you need to reuse some of the behavior but do something slightly different with it. So let’s say you have a string you want to wrap inside html tags and then do something different with it.
def wrap_in_tags(tag, text)
html = "<#{tag}>#{text}</#{tag}>"
yield html
end
wrap_in_tags("title", "Hello") { |html| Mailer.send(html) }
wrap_in_tags("title", "Hello") { |html| Page.create(:body => html) }
In the first case we’re sending the <title>Hello</title>
string via email and in the second case we’re creating a Page
record. Both cases use the same method but they do different things.
Take a note
Let’s say you want to build a way to quickly store ideas into a database table. For that to work you want to pass in the note and have the method deal with the database connections. Ideally we’d like to call Note.create { "Nice day today" }
and not worry about opening and closing database connections. So let’s do this.
class Note
attr_accessor :note
def initialize(note=nil)
@note = note
puts "@note is #{@note}"
end
def self.create
self.connect
note = new(yield)
note.write
self.disconnect
end
def write
puts "Writing \"#{@note}\" to the database."
end
private
def self.connect
puts "Connecting to the database..."
end
def self.disconnect
puts "Disconnecting from the database..."
end
end
Note.create { "Foo" }
# output
Connecting to the database...
@note is Foo
Writing "Foo" to the database.
Disconnecting from the database...
The implementation details of connecting, writing and disconnecting to and from the database were left out since they’re out of the scope of this article.
Find divisible elements of an array
It seems like I’m getting further and further away from “the real world scenario” but anyways, I’m gonna shoot one last example. So let’s say you want to get every element of an array that is divisible by 3 (or any number you choose), how would you do that with ruby blocks?
class Fixnum
def to_proc
Proc.new do |obj, *args|
obj % self == 0
end
end
end
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].select(&3)
puts numbers
# output
3
6
9
Conclusion
You can think of blocks as simply a chunk of code, and yield
allows you to inject that code at some place into a method. That means you can have one method work in different ways, you don’t have to write multiple methods (you can reuse one method to do different things).
You’ve made it! Having read the whole post means you’re on your way to find more creative uses of ruby blocks. If for some reason you’re still confused or feel like there’s something missing from the text please let me know in the comments.
Share this article if you’ve learned something new about ruby blocks.