Up to now, you have passed blocks to procedures either anonymously (in which case the block is executed with the yield
keyword) or in the form of a named argument, in which case it is executed using the call
method. There is another way to pass a block. When the last argument in a method’s list of parameters is preceded by an ampersand (&
), it is considered to be a Proc object. This gives you the option of passing an anonymous block to a procedure using the same syntax as when passing a block to an iterator, and yet the procedure itself can receive the block as a named argument. Load 5blocks.rb to see some examples of this.
First, here is a reminder of the two ways you’ve already seen of passing blocks. This method has three parameters, a
, b
, and c
:
5blocks.rb
def abc( a, b, c ) a.call b.call c.call yield end
You call this method with three named arguments (which here happen to be blocks but could, in principle, be anything) plus an unnamed block:
a = lambda{ puts "one" } b = lambda{ puts "two" } c = proc{ puts "three" } abc(a, b, c ){ puts "four" }
The abc
method executes the named block arguments using the call
method and the unnamed block using the yield
keyword. The results are shown in the #=>
comments here:
a.call #=> one b.call #=> two c.call #=> three yield #=> four
The next method, abc2
, takes a single argument, &d
. The ampersand here is significant because it indicates that the &d
parameter is a block. Instead of using the yield
keyword, the abc2
method is able to execute the block using the name of the argument (without the ampersand):
def abc2( &d ) d.call end
So, a block argument with an ampersand is called in the same way as one without an ampersand. However, there is a difference in the way the object matching that argument is passed to the method. To match an ampersand-argument, an unnamed block is passed by appending it to the method name:
abc2{ puts "four" }
You can think of ampersand-arguments as type-checked block parameters. Unlike normal arguments (without an ampersand), the argument cannot match any type; it can match only a block. You cannot pass some other sort of object to abc2
:
abc2( 10 ) # This won't work!
The abc3
method is essentially the same as the abc
method except it specifies a fourth formal block-typed argument (&d
):
def abc3( a, b, c, &d)
The arguments a
, b
, and c
are called, while the argument &d
may be either called or yielded, as you prefer:
def abc3( a, b, c, &d) a.call b.call c.call d.call # first call block &d yield # then yield block &d end
This means the calling code must pass to this method three formal arguments plus a block, which may be nameless:
abc3(a, b, c){ puts "five" }
The previous method call would result in this output (bearing in mind that the final block argument is executed twice since it is both called and yielded):
one two three five five
You can also use a preceding ampersand in order to pass a named block to a method when the receiving method has no matching named argument, like this:
myproc = proc{ puts("my proc") } abc3(a, b, c, &myproc )
An ampersand block variable such as &myproc
in the previous code may be passed to a method even if that method does not declare a matching variable in its argument list. This gives you the choice of passing either an unnamed block or a Proc object:
xyz{ |a,b,c| puts(a+b+c) } xyz( &myproc )
Be careful, however! Notice in one of the previous examples, I have used block parameters (|a,b,c|
) with the same names as the three local variables to which I previously assigned Proc objects: a
, b
, c
:
a = lambda{ puts "one" } b = lambda{ puts "two" } c = proc{ puts "three" } xyz{ |a,b,c| puts(a+b+c) }
In principle, block parameters should be visible only within the block itself. However, it turns out that assignment to block parameters has profoundly different effects in Ruby 1.8 and Ruby 1.9. Let’s look first at Ruby 1.8. Here, assignment to block parameters can initialize the values of any local variables with the same name within the block’s native scope (see What Is a Closure? in What Is a Closure?).
Even though the variables in the xyz
method are named x
, y
, and z
, it turns out that the integer assignments in that method are actually made to the variables a
, b
, and c
when this block:
{ |a,b,c| puts(a+b+c) }
is passed the values of x
, y
, and z
:
def xyz x = 1 y = 2 z = 3 yield( x, y, z ) # 1,2,3 assigned to block parameters a,b,c end
As a consequence, the Proc variables a
, b
, and c
within the block’s native scope (the main scope of my program) are initialized with the integer values of the block variables x
, y
, and z
once the code in the block has been run. So, a
, b
, and c
, which began as Proc objects, end up as integers.
In Ruby 1.9, on the contrary, the variables inside the block are sealed off from the variables declared outside the block. So, the values of the xyz
method’s x
, y
, and z
variables are not assigned to the block’s a
, b
, and c
parameters. That means once the block has executed, the values of the a
, b
, and c
variables declared outside that method are unaffected: They began as Proc objects, and they end up as Proc objects.
Now let’s suppose you execute the following code, remembering that a
, b
, and c
are Proc objects at the outset:
xyz{ |a,b,c| puts(a+b+c) } puts( a, b, c )
In Ruby 1.8, the puts
statement shown earlier displays the end values of a
, b
, and c
, showing that they have been initialized with the integer values that were passed into the block when it was yielded (yield( x, y, z )
) in the xyz
method. As a consequence, they are now integers:
1 2 3
But in Ruby 1.9, a
, b
, and c
are not initialized by the block parameters and remain, as they began, as Proc objects:
#<Proc:0x2b65828@C:/bookofruby/ch10/5blocks.rb:36 (lambda)> #<Proc:0x2b65810@C:/bookofruby/ch10/5blocks.rb:37 (lambda)> #<Proc:0x2b657f8@C:/bookofruby/ch10/5blocks.rb:38>
This behavior can be difficult to understand, but it is worth taking the time to do so. The use of blocks is commonplace in Ruby, and it is important to know how the execution of a block may (or may not) affect the values of variables declared outside the block. To clarify this, try the simple program in 6blocks.rb:
6blocks.rb
a = "hello world" def foo yield 100 end puts( a ) foo{ |a| puts( a ) } puts( a )
Here a
is a string within the scope of the main program. A different variable with the same name, a
, is declared in the block, which is passed to foo
and yielded. When it is yielded, an integer value, 100, is passed into the block, causing the block’s parameter, a
, to be initialized to 100. The question is, does the initialization of the block argument, a
, also initialize the string variable, a
, in the main scope? And the answer is, yes in Ruby 1.8 but no in Ruby 1.9.
Ruby 1.8 displays this:
hello world 100 100
Ruby 1.9 displays this:
hello world 100 hello world
If you want to make sure that block parameters do not alter the values of variables declared outside the block, no matter which version of Ruby you use, just ensure that the block parameter names do not duplicate names used elsewhere. In the current program, you can do this simply by changing the name of the block argument to ensure that it is unique to the block:
foo{ |b| puts( b ) } # the name 'b' is not used elsewhere
This time, when the program is run, Ruby 1.8 and Ruby 1.9 both produce the same results:
hello world 100 hello world
This is an example of one of the pitfalls into which it is all too easy to fall in Ruby. As a general rule, when variables share the same scope (for example, a block declared within the scope of the main program here), it is best to make their names unique in order to avoid any unforeseen side effects. For more on scoping, see Digging Deeper in Digging Deeper.