Passing Named Proc Arguments

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.15.182.62