What is the specific difference between block variable and block-local variable in Ruby?

Viewed 185

I have an example, there are 3 variables with the same name inside and outside the block, and if they are printed inside and outside the block, there will be different results. Here is the code:

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

ary.each do |x; y|    
    y = x           
    z = x              
    p [x, y, z]         
end
puts
p [x, y, z]             

the result of p [x, y, z] within the block

[1, 1, 1]
[2, 2, 2]
[3, 3, 3]

the result of p [x, y, z] outside the block

[0, 0, 3]

However, I am unable to understand some issues.

First of all, x and y in the first line of the block are both parameters inside the block and have nothing to do with things with the same name outside. The result is the same whether y is a block variable (, y) or a block-local variable (; y). why? Does this mean that block variables are not actually different from block-local variables?

Second, even if I remove the parameter "; y", the result of p [x, y, z] inside the block is still unchanged, while the result of p [x, y, z] outside the block becomes [ 0, 3, 3], why?

Third, the result of p [x, y, z] outside the block, why is z not 0? x, y, z are all assigned to 0 in the first line.

2 Answers

Unfortunately there are no suitable keywords in ruby which might explain it beyond the doubt. So, let me translate it to javascript! (Javascript local variables are practically identical to ruby variables - with an exception for explicit creation keywords)

But before, few notes about scopes in javascript. Even though I'll be writing using JS, all the notes are also correct for ruby - except for a lack of the explicit variable keyword.

So: let keyword - let creates a new variable on the current scope. Once created, given variable can be read only when we are inside the same lexical scope or its child scopes:

let x = 0;

x #=> 0;

function() { # function creates a new child scope
  return x; # access variable of the parent scope
}() #=> 0

It is important to understand that the scope is lexical, not dynamic - this means that variables resolves in a static context, which depends on the structure of the code rather than how the code is being called. This creates so-called closures (again - closures also exists in Ruby, however, unlike in JS, it's best to avoid them).

When searching for a variable, we always look at the current scope first and, if we have nothing defined in current scope, move to the parent scope. If no parent scope is found, exception is thrown.

So, let's translate your code into javascript:

let x = 0, y = 0, z = 0        
let ary = [1, 2, 3]

ary.forEach(function(x) { # x is now function argument
  let y; # This is because of `|...; y|` in your block 

  # In this scope we have 3 variables:
  #    x - is an argument of a function
  #    y - is locally scoped variable
  #    z - is undefined in this scope, so it'll reference z from the parent scope
  y = x           
  z = x              
  console.log(x, y, z)
})

console.log(x, y, z)

Let's analyze. In the above code, there are two scopes - top scope and child scope. There are 4 variables defined in your top scope (x, y, z and ary) and 2 variables defined in the child scope (x, y).

When you do y = x you first read the value of x - since such a variable exists in the current scope (and it is passed as an argument), we take that. In first iteration the value of x is 1, so expression evaluates to:

y = 1

Now we need to find variable y we want to assign to - there is variable y in the current scope so we assign to that one. The y variable in the main scope is not affected by this assignment

Next expression: z = x - x resolves the same way as before, so in first iteration we have:

z = 1

But now, there is no z variable in the current scope, so we look for a variable in the parent scope. In result, this assignment modifies the variable in the main scope

Next, we're printing x, y and z resolved in the current scope - there's no surprise here, they are all 1s.

Second iteration, everything repeats, but this time x resolves to 2. Again, we set the value of a y in a local, child scope and z of the parent scope. And then, third iteration.

In last expression we print the values of the variables, but in the parent scope. x is 0 as we have never done a single assignment to it, similarly y is 0 - because all the assignments were done against the variable defined in the child scope so, in fact, we didn't make a single assignment to it. z on the other hand was assigned to on each iteration. On the last iteration it received value 3 which is what it currently holds.

In my mind and unless I'm misunderstanding, this can all be simplified if you understand that any and all Block Parameters (|*parameters|) define variables that ONLY exist inside the block. In addition, Block Parameters will always be used as "block variables" (or "block arguments") and passed directly to the method UNLESS you specify otherwise by assigning them as "block-local variables" using |*block_variables; *block_local_variables| syntax, or by explicitly assigning values using either |x, y = value| or |x, y: value| syntax.

NOTE: Only one semicolon is required to specify any number of the aforementioned block-local variables. Everything after the semicolon is essentially being defined as a block-local and is NOT passed to the method.

The only reason for a block-local variable that I am aware of is to re-use a variable name without affecting the variable that exists outside the block.

Let's address your specific questions:


x and y in the first line of the block are both parameters inside the block and have nothing to do with things with the same name outside. The result is the same whether y is a block variable (, y) or a block-local variable (; y). why?

In your specific example the results might be the same, but consider this example:

x, y, z = 0,0,0

arr = [
[1, 2, 3],
[4, 5, 6]
]


arr.map {|x, y| y = x; z = x}
[x, y, z] #=>  [0, 0, 4]

whereas;

arr.map {|x; y| y = x; z = x}
[x, y, z] #=>  [0, 0, [4, 5, 6]]

even if I remove the parameter "; y", the result of p [x, y, z] inside the block is still unchanged, while the result of p [x, y, z] outside the block becomes [ 0, 3, 3], why?

Consider what happens if you modify your example to place the ; BEFORE your block variable:

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

ary.each do |;x|    
    y = x           
    z = x              
    p [x, y, z]         
end
puts
p [x, y, z] #=>  [0, nil, nil]

In that example, we told Ruby not to modify x outside the block and since x is no longer being passed to the method, is simply being assigned as a block-local variable, and is not being assigned a value, the resulting value of x inside the block is nil.

the result of p [x, y, z] outside the block, why is z not 0? x, y, z are all assigned to 0 in the first line.

z is not zero because you did not protect it as a block-local variable like you did with y. Consider this version though:

ary.each do |x; z|    
    y = x           
    z = x                       
end
puts
p [x, y, z] #=>  [0, 3, 0]         
Related