the blog
Back to -Blog

Ruby Blocks, Parameters and ’for’ Loops

Some twisty little corners of scoping...
by Huw Collingbourne
Wednesday 29 August 2007.

There are some odd gnarly little corners of Ruby which those of us who spend our waking lives writing Ruby lexers, parsers, debuggers and IntelliSense systems are rather keenly aware of.

However, a knowledge of these quirks will be useful even if you aren’t writing an IDE! I thought it might be interesting, therefore, to explore some of the foggy little backwaters of Ruby from time to time and point out a few of things that may catch you out if you aren’t prepared for them.

First, block variables...

It’s well known that Matz, the creator of Ruby, has acknowledged that the scoping of variables within blocks exhibits what he has called ‘regrettable behaviour’. In particular, block parameters – the local variables declared between upright bars – are not scoped tightly within the block itself. Since a block is, in principle, a kind of ‘nameless method’, you would expect its block parameters to behave like the arguments to a regular method.

Let’s take an example, here’s a version in which each integer in an array is passed to a method named (inventively) aMethod:

x = 100
def aMethod( x )
  puts( x )
end

for i in [1,2,3] do
  aMethod( i )
end
print("x is now: ",  x )

The argument to aMethod is named x. So x is initialized with the values 1, 2 and 3. Outside this method is a local variable, which is also named x. This has the value 100. When we run this program, there are no surprises. As you would surely expect, the argument x to aMethod is local to the scope of that method and has no effect on the local variable, x. This is the output of the program...

1
2
3
x is now: 100

Now let’s rewrite this using a block instead of a method:

x = 100
[1,2,3].each {
  |x|
  puts( x )
}
print("x is now: ",  x )

Here the each method passes each value from the array, [1,2,3] into the block causing the block parameter, x to be initialized with the values, 1, 2 and 3, just as the method parameter, x, was initialized with those values in the last example. But now look at the output:

1
2
3
x is now: 3

The local variable, x has been initialized by the block parameter, x! This is normal behaviour in Ruby 1.8.x. If there happens to be local variable outside the block with the same name as a local variable (or block parameter) inside the block, its value will be affected by any assignment to the variable inside the block. As I said, this quirk is pretty well known among Ruby programmers, it has been highlighted as a flaw by Matz and it will be changed in future versions - from 1.9 onwards, I believe - of Ruby.

See:
- http://eigenclass.org/hiki.rb?Changes+in+Ruby+1.9
- http://rubygarden.org/ruby/page/show/Rite.

But there is another little foible of blocks and scoping which is, perhaps, not quite so well known. It concerns for loops.

Now, for a long time, I laboured under the illusion that a for loop provided an alternative syntax to the each method but that its behaviour was, to all intents and purposes, identical. I suspect this misapprehension resulted from my perusal of the online Ruby manual in my early days of Ruby programming. This is now quite an old document but it’s still online and is still frequently referenced by other writers, so I thought it safe to take its description of the for loop literally. This is what it says:

Syntax:
        for lhs... in expr [do]
          expr..
        end

Executes body for each element in the result of expression. for is the syntax sugar for:
        (expr).each `{' `|' lhs..`|' expr.. `}'

See:
- The Online Ruby Manual: http://docs.huihoo.com/ruby/ruby-man-1.4/syntax.html#for

So, a for loop, I assumed, was nothing more nor less than ‘syntax sugar’ for an each iterator. That led me to believe that I could use for loops and each iterators indiscriminately in the sure and certain knowledge that they would have an identical effect in my code.

Not so!

The big difference between for and each is their effects on local variables. Look at this method...

def foo
  a = 100
  [1,2,3].each do |b|
     c = b
     a = b
     print("a=#{a}, b=#{b}, c=#{c}\n")
  end
  print("Outside block: a=#{a}\n")
end

Here, neither b nor c are declared outside of the block so I am unable to print their values after the end block delimiter. The local variable, a is declared outside the block, however, and this is initialized with the value (3) assigned inside the block.

Now, look at the method rewritten using for:

def foo2
  a = 100
  for b in [1,2,3] do
     c = b
     a = b
     print("a=#{a}, b=#{b}, c=#{c}\n")
  end
  print("Outside block: a=#{a}, b=#{b}, c=#{b}\n")       
end

Once again, a is declared outside the block and it takes on the value (3) assigned to a inside the block. And once again, b and c are declared inside the block (at least, c is created inside the block, while b should, if this is truly just ‘syntax sugar’ for each, take on the role of the ‘block parameter’, b, from the previous example).

But this time, all three variables, a, b and c are accessible from outside the block and their values can be printed after the block’s end delimiter.

This is the kind of subtle, non-obvious behaviour that can cause headaches to those of us writing Ruby IDEs and could equally cause all kinds of weird side-effects (unless you take appropriate defensive measures such as, for example, making sure your local variable names are unique) in your own Ruby programs.

Here’s the code if you want to try this out for yourself...

Bookmark and Share   Keywords:  ruby  Ruby Tips and Tricks
  • Ruby Blocks, Parameters and ’for’ Loops
    29 September 2008, by bbiker

    I have read and understood what your trying to demonstrate — at least I think

    I went through the excersize of copying the code and run it under ruby 1.8.6 and macruby (ruby 1.9.0)

    code:

    #!/usr/local/bin macruby -w # ref: http://www.sapphiresteel.com/Ruby-B... def foo a = 100 [1,2,3].each do |b| c = b a = b print("a = #a, b = #b, c = #c\n") end print("outside block: a = #a\n") end

    def foo2 a = 100 for b in [1,2,3] do c = b a = b print("a = #a, b = #b, c = #c\n") end print("outside block: a = #a, b = #b, c=#c\n") end

    def a_method(x) puts x end

    puts( "macruby ====== each calling a_metnod =======" ) x = 100 [1,2,3].each do |x| a_method(x) end print("x is now: #x\n")

    puts( "macruby ======= for calling a_method =======" ) x = 100 for x in [1,2,3] do a_method( x ) end print("x is now: #x\n" )

    ============== the only difference between the macruby code and the ruby: "ruby" replaces "macruby"

    Here are the results:

    first ruby 1.9.0 renard$ macruby foo.rb foo.rb:25: warning: shadowing outer local variable - x macruby ====== each calling a_metnod ======= foo.rb:26:in `block in

    ’: undefined method `a_method’ for main:NSObject (NoMethodError) from foo.rb:25:in `each’ from foo.rb:25:in `
    ’ renard$ macruby foo.rb foo.rb:29: warning: shadowing outer local variable - x macruby ====== each calling a_metnod ======= 1 2 3 x is now: 100 macruby ======= for calling a_method ======= 1 2 3 x is now: 3 macruby ======== each =========== a = 1, b = 1, c = 1 a = 2, b = 2, c = 2 a = 3, b = 3, c = 3 outside block: a = 3 macruby ======== for =========== a = 1, b = 1, c = 1 a = 2, b = 2, c = 2 a = 3, b = 3, c = 3 outside block: a = 3, b = 3, c=3 renard$ macruby -v MacRuby version 0.3 (ruby 1.9.0 2008-06-03) [universal-darwin9.0] renard$

    next ruby 1.8.6

    renard$ ruby foo.rb ruby ====== each calling a_metnod ======= 1 2 3 x is now: 3 ruby ======= for calling a_method ======= 1 2 3 x is now: 3 ruby ======== each =========== a = 1, b = 1, c = 1 a = 2, b = 2, c = 2 a = 3, b = 3, c = 3 outside block: a = 3 ruby ======== for =========== a = 1, b = 1, c = 1 a = 2, b = 2, c = 2 a = 3, b = 3, c = 3 outside block: a = 3, b = 3, c=3 renard$ ruby -v ruby 1.8.6 (2008-08-11 patchlevel 287) [i686-darwin9.4.0]

    ruby 1.8.6 behaves as expected

    However I am confused by ruby 1.9 behavior

    As expected ruby 1.9 issues a warning that the x variable "declared" in main is overshadowed in an "each" block but it does not issued a warning if the x is declared within the foo method even though the x variable is overwritten within the each block

    Is this the normal result or is it an inconsistency in ruby 1.9.0?

    Thank for anything clarification you can provide

    • Ruby Blocks, Parameters and ’for’ Loops
      30 September 2008, by Huw Collingbourne

      The Book Of Ruby only describes the behaviour of Ruby 1.8.x which is the ’standard’ version of Ruby. Ruby 1.9 is generally regarded as experimental and even Matz has changed his mind on how variables inside blocks should behave in that version. This is a recent thread on the Ruby forum in which Matz talks about that: http://www.ruby-forum.com/topic/166992.

      Apart from some mentions in the text of The Book of Ruby about ’expected behaviour’ of blocks in Ruby 2.0, I make no attempt to anticipate that behaviour or provide Ruby 1.9 compatible sample code. Currently, Ruby 1.9->2.0 is too much of a ’moving target’ though maybe I’ll revise The Book Of Ruby when a stable version of Ruby 2.0 is released.

      best wishes

      Huw

© SapphireSteel Software 2014