Writing a Method That Accepts a Block
Problem
You want to write a method that can accept and call an attached code block: a method that works like Array#each, Fixnum#upto, and other built-in Ruby methods.
Solution
You don't need to do anything special to make your method capable of accepting a block. Any method can use a block if the caller passes one in. At any time in your method, you can call the block with yield:
def call_twice puts "I'm about to call your block." yield puts "I'm about to call your block again." yield end call_twice { puts "Hi, I'm a talking code block." } # I'm about to call your block. # Hi, I'm a talking code block. # I'm about to call your block again. # Hi, I'm a talking code block.
Another example:
def repeat(n) if block_given? n.times { yield } else raise ArgumentError.new("I can't repeat a block you don't give me!") end end repeat(4) { puts "Hello." } # Hello. # Hello. # Hello. # Hello. repeat(4) # ArgumentError: I can't repeat a block you don't give me!
Discussion
Since Ruby focuses so heavily on iterator methods and other methods that accept code blocks, it's important to know how to use code blocks in your own methods.
You don't have to do anything special to make your method capable of taking a code block. A caller can pass a code block into any Ruby method; it's just that there's no point in doing that if the method never invokes yield.
puts("Print this message.") { puts "And also run this code block!" } # Print this message.
The yield keyword acts like a special method, a stand-in for whatever code block was passed in. When you call it, it's exactly as the code block were a Proc object and you had invoked its call method.
This may seem mysterious if you're unfamiliar with the practice of passing blocks around, but it is usually the preferred method of calling blocks in Ruby. If you feel more comfortable receiving a code block as a "real" argument to your method, see Recipe 7.3.
You can pass in arguments to yield (they'll be passed to the block) and you can do things with the value of the yield statement (this is the value of the last statement in the block).
Here's a method that passes arguments into its code block, and uses the value of the block:
def call_twice puts "Calling your block." ret1 = yield("very first") puts "The value of your block: #{ret1}" puts "Calling your block again." ret2 = yield("second") puts "The value of your block: #{ret2}" end call_twice do |which_time| puts "I'm a code block, called for the #{which_time} time." which_time == "very first" ? 1 : 2 end # Calling your block. # I'm a code block, called for the very first time. # The value of your block: 1 # Calling your block again. # I'm a code block, called for the second time. # The value of your block: 2
Here's a more realistic example. The method Hash#find takes a code block, passes each of a hash's key-value pairs into the code block, and returns the first key-value pair for which the code block evaluates to true.
squares = {0=>0, 1=>1, 2=>4, 3=>9} squares.find { |key, value| key > 1 } # => [2, 4]
Suppose we want a method that works like Hash#find, but returns a new hash containing all the key-value pairs for which the code block evaluates to true. We can do this by passing arguments into the yield statement and using its result:
class Hash def find_all new_hash = Hash.new each { |k,v| new_hash[k] = v if yield(k, v) } new_hash end end squares.find_all { |key, value| key > 1 } # => {2=>4, 3=>9}
As it turns out, the Hash#delete_if method already does the inverse of what we want. By negating the result of our code block, we can make Hash#delete_if do the job of Hash#find_all. We just need to work off of a duplicate of our hash, because delete_if is a destructive method:
squares.dup.delete_if { |key, value| key > 1 } # => {0=>0, 1=>1} squares.dup.delete_if { |key, value| key <= 1 } # => {2=>4, 3=>9}
Hash#find_all turns out to be unnecessary, but it made for a good example.
You can write a method that takes an optional code block by calling Kernel#block_given? from within your method. That method returns true only if the caller of your method passed in a code block. If it returns false, you can raise an exception, or you can fall back to behavior that doesn't need a block and never uses the yield keyword.
If your method calls yield and the caller didn't pass in a code block, Ruby will throw an exception:
[1, 2, 3].each # LocalJumpError: no block given
See Also
- Recipe 7.3, " Binding a Block Argument to a Variable"