Block scope in ruby

12,104

Solution 1

This is expected behaviour for ruby 1.8. It was fixed in 1.9. Snippets below are run with ruby 1.9.3

food = ['toast', 'cheese', 'wine']
food.each { |food| puts food.capitalize.inspect} # !> shadowing outer local variable - food
puts food.inspect
# >> "Toast"
# >> "Cheese"
# >> "Wine"
# >> ["toast", "cheese", "wine"]

You are correct, food from the block is scoped to that block and shadows other variables with this name. But if you do something destructive to it, it will be reflected in the original array, because it is reference to array element, not its copy. Observe:

food = ['toast', 'cheese', 'wine']

food.each { |f| f.capitalize} # transform and discard
food # => ["toast", "cheese", "wine"]

food.each { |f| f.capitalize! } # transform destructively (bang-version)
food # => ["Toast", "Cheese", "Wine"]

Solution 2

The block inherits the scope from the context it is defined in. Take a look at this example:

def give_me_a_proc
  test = "foo"
  lambda { puts test }
end

test = "bar"
give_me_a_proc.call

# => "foo"

So it doesn't matter where the proc/block is executed, but rather where it was defined.

In your case the food variable inside the block is indeed shadowing the food array from outside. But you really operate on the actual elements (not duplicates/clones) of food array in the block so any changes to them will be reflected outside.

The reason why food array changes into "Wine" string after the block is finished is that the block operates in the same scope the food array was defined, so it overwrites it with the last element of the array since it is the last object that is assigned to food variable.

Solution 3

This bug has been fixed in Ruby 1.9

food = ['toast', 'cheese', 'wine']
food.each { |food| puts food.capitalize }
puts food # ["toast", "cheese", "wine"]

I think if you want to use local variable in Ruby 1.8, try lambda (Sorry, I don't install Ruby 1.8, so can't test it, but work in 1.9)

food = ['toast', 'cheese', 'wine']
lambda do |food|
    food.each {|food| puts food.capitalize}
end.call(food)
puts food # ["toast", "cheese", "wine"]
Share:
12,104

Related videos on Youtube

18bytes
Author by

18bytes

A hacker.

Updated on July 13, 2022

Comments

  • 18bytes
    18bytes almost 2 years

    My understanding was that ruby blocks have block scope, and all variables created inside block will live only within the block.

    Example case:

     food = ['toast', 'cheese', 'wine']
     food.each { |food| puts food.capitalize}
     puts food
    

    Output:

    "Toast"
    "Cheese"
    "Wine"
    "Wine"
    

    If you take the food variable inside the block (Each block), my understanding was that it has block scope. It lives only within the block scope, and does not have any influence on the outer variable food.

    But the behavior is different, the outer variable named food is modified in this case. Is this understanding correct, In ruby do we have block scope?

  • 18bytes
    18bytes over 11 years
    I use ruby version 1.8.7. I do not get the result you have mentioned in your response. Is this a bug?
  • Sergio Tulentsev
    Sergio Tulentsev over 11 years
    I use 1.9.3. Let me try in 1.8.7
  • 18bytes
    18bytes over 11 years
    I understand that the food variable is shadowed inside the block, so its a different variable than the outer one. When i do not use any destructive update (i am not using bang-version), why does it update the outer variable?
  • Sergio Tulentsev
    Sergio Tulentsev over 11 years
    It should not. Hold on, I'm installing 1.8.7. By the way, why don't you upgrade? Support for 1.8.7 will be dropped in next rails.
  • Sergio Tulentsev
    Sergio Tulentsev over 11 years
    Yes, apparently, this is the expected behaviour in ruby 1.8. A blog post.
  • Sergio Tulentsev
    Sergio Tulentsev over 11 years
    The question was (I guess) why the shadowing persists after each is over.
  • Michał Szajbe
    Michał Szajbe over 11 years
    for the same reason: you operate in the context the block is defined in, so you can overwrite variables there. I'll update my answer.
  • Sergio Tulentsev
    Sergio Tulentsev over 11 years
    Anyway, ruby 1.9 behaves correctly. It does not overwrite names from outer scope (in case of block parameters).
  • Asad Saeeduddin
    Asad Saeeduddin about 10 years
    @SergioTulentsev I'm a bit confused about what's going on here. The unexpected part about OP's output in 1.8 was that the final "Wine" was capitalised, since the capitalise method being invoked is non destructive. Even if the food variable holding an array is overwritten by the food parameter, why is the final value of the parameter "Wine", and not "wine"?
  • Sergio Tulentsev
    Sergio Tulentsev about 10 years
    @Asad: off the top of my head, I don't know. And frankly, I don't want to know. 1.8 is dead.