2014/6/30
TL;DR
Use Proc.new
Calling Enumerators - normal use
You're writing some code which calls an Enumerator - a function that makes repeated calls to the block of code that you provide.
def yield_me_2_things yield 'Thing 1' yield 'Thing 2' end yield_me_2_things { |x| puts x }
This will print:
Thing 1 Thing 2
The values are supplied by yield_me_2_things
and the printing is done in the block, { |x| puts }
, that is passed to that method.
Generalize
I can now make a generalized method, to handle any number of things:
def yield_me_n_things(n) 1.upto(n) do |i| thing = "Thing #{i}" yield thing end end yield_me_n_things(2) { |x| puts x }
...the output is the same.
An alternative: use a block
I could equally have implemented the method using a `&block` parameter - for the caller, it makes no difference:
def call_this_block_with_n_things(n, &block) 1.upto(n) do |i| thing = "Thing #{i}" block.call thing end end call_this_block_with_n_things(2) { |x| puts x }
...the output is the same.
The problem
What if I want one Enumerator to call another?
What if I want to keep the specific version (yield_me_2_things
) but just make it call the generalized method?
def enumerate_n_things(n) # How do I receive the block? 1.upto(n) do |i| thing = "Thing #{i}" # How do I call the block? end end def enumerate_2_things enumerate_n_things(2) # How do I forward the block? end enumerate_2_things { |x| puts x } enumerate_2_things(2) { |x| puts x }
How should I write the two methods, while keeping both usable independently?
Attempt 1: Forward using yield
With `yield`, you don't explicitly receive the block, you just call it.
Does that work across two levels? I.e., does the block get passed to method I call?
def enumerate_n_things(n) 1.upto(n) do |i| thing = "Thing #{i}" yield thing end end def enumerate_2_things enumerate_n_things(2) end enumerate_2_things { |x| puts x }
No, doesn't work, enumerate_n_things
doesn't receive a block.
I get this error:
no block given (yield) (LocalJumpError)
Attempt 2: Forward using a block
def enumerate_n_things(n, block) # Note: no '&' 1.upto(n) do |i| thing = "Thing #{i}" block.call thing end end def enumerate_2_things(&block) enumerate_n_things(2, block) end enumerate_2_things { |x| puts x }
Prints:
Thing 1 Thing 2
But we can no longer pass a block to the generalized method:
enumerate_n_things(2) { |x| puts x }
enumerate_n_things
now expects the block as a normal parameter.
I get this error:
wrong number of arguments (1 for 2)
Solution: Proc.new
def enumerate_n_things(n, block = Proc.new) 1.upto(n) do |i| thing = "Thing #{i}" block.call thing end end def enumerate_2_things(block = Proc.new) enumerate_n_things(2, block) end enumerate_2_things { |x| puts x } enumerate_n_things(2) { |x| puts x }
Both calls now work!
Proc.new
transforms any block passed to a method into a Proc
.
If we use that as the default value for a block parameter we can call methods directly with blocks, or forward blocks between enumerators.