TPR
TPR

Reputation: 111

Why doesn't assignment to variable y work in a Ruby block?

I assign the value "1" to the variables x and y outside the block, and then I reference the variable x of the same name outside the block in a block, so x is both 1.

Next, I assign the value of x to y in the block. In theory, y should also be 1, but the most trusted result is x = 1, y = 3.

Please tell me why.

x = 1               
y = 1               
ary = [1, 2, 3]

ary.each do |x|     
    y = x           
end

p [x, y]            

Upvotes: 1

Views: 196

Answers (3)

DickieBoy
DickieBoy

Reputation: 4956

Because you are redefining x in the scope of the block. ary.each do |x| does this.

Upvotes: 2

steenslag
steenslag

Reputation: 80065

Block parameters (the stuff between the pipes like |x,y|) are local to the block. But pre-existing variables are not.

total = 0
[1,5].each{|n| total += n} #works because total starts out as 0
puts total # => 6

If you want a block-local total then follow the parameters by a semicolon and a comma-separated list of variables

total = 0
[1,5].each{|n; total, _unused| total += n} #Error because total is nil
puts total 

Upvotes: 3

BroiSatse
BroiSatse

Reputation: 44695

The source of confusion here is so-called shadowing. You have two local variables called x here, but only one called y. The second x variable is shadowing the first one - meaning that any attempt to access or assign to the first variable is doomed to fail.

When you do ary.each do |x|, you are creating new variable, which has nothing (except for the name) to do with the variable x you have created in the outer scope.

y on the other hand is not shadowed and the loop binding is accessing (and assigning to) the variable defined in the parent scope. Unfortunately there is no way (at least that I am aware of) of explicit local variable creation like var/let/const in javascript.

In short, your code executes as follow:

x = 1               
y = 1               
ary = [1, 2, 3]

ary.each do |x2|     
  y = x2         
end

p [x, y]

So in fact, assignment to y works just as expected. You assign it value of 1 before the block, and then you assign 3 more times within the block. The last assignment is y=3 so that's its final value.

BEWARE, THE HACK:

Well there actually is a way of forcing the local variable creation, and it is to add an extra yielding argument to each block. This should never be done in the actual code, and I only present it for completeness. You should actually avoid any variable shadowing whenever possible.

x = y = 1
ary = [2,3,4]

ary.each do |x, y| # this creates new local variables x and y, y is always set to nil for each iteration.
  y = x # y here is a shadowing variable, not the y of the parent scope
  puts [x,y].inspect
end

puts [x,y].inspect

# OUTPUTS:
  
[2, 2]
[3, 3]
[4, 4]
[1, 1]

Upvotes: 3

Related Questions