Home > Software engineering >  Why does a Ruby return with assignment and trailing conditional behave differently with a value assi
Why does a Ruby return with assignment and trailing conditional behave differently with a value assi

Time:02-03

Update: Here's where I messed up my mental model. Method f has a parameter a defined. Had I named this parameter b, then indeed f would return 'oh' and not `2'.

Thanks to @Holgur Just below in helping me re-analyze and see my error.

Consider method f,

def f(a)
  return a = 2 if !a.nil?
  return 'oh'
end

f(42) # 2
f(nil) # 'oh'

And consider method g,

def g(b)
  return a = b if !a.nil?
  return 'oh'
end

g(42) # 'oh'
g(nil) # 'oh'

And consider method h,

def h(b)
  a = b
  return a if !a.nil?
  return 'oh'
end

h(42) # 42
h(nil) # 'oh'

I expected g(42) to return 42 ? Why does g(42) not return 42 ?

What is the order of evaluation here that is the difference between f and g, and between g and h?

CodePudding user response:

return a = b if !a.nil?
return 'oh'

is mostly equivalent to

if !a.nil?
  return a = b
end
return 'oh'

As such, Ruby first tests whether a is not nil (which is false because a is in fact nil there as it had not been assigned a value yet). Because of that, the body of the if is not executed and the execution follows along to the return 'oh'.

The more important question here is however: why did this work at all and did not result in an error such as

NameError: undefined local variable or method `a'

when trying to access the a variable in the if, even though it was not initialized before.

This works because Ruby initializes variables with nil if they appear on the left-hand side of an assignment in the code, even though the variable may not actually be assigned. This behavior is further explained in e.g. https://stackoverflow.com/a/12928261/421705.

Following this logic, your code thus only works with your original inline-if but would fail with the block-form if as with this longer form, a would only be initialized within the if block. Here, you would thus get the NoMethodError.

CodePudding user response:

It is a matter of lexical parsing as @HolgerJust pointed out.

There are some other similarly interesting side effects of using the modifier-[if/unless]

def a; 1; end; 
(a if a = true) == a 
#=> false 

Here's how the parser sees it in a nutshell:

  1. Define a method a()
  2. The parser then encounters a as part of the then body so it tags this a as a method call (a()) because a is not a local variable at this point and the ruby syntax allows for omission of parentheses in method calls.
  3. The parser then encounters the test expression and here it marks a as a local variable, due to the assignment (=)
  4. The test expression is executed and in process it assigns a the value of true and the test passes
  5. The then body is now executed which calls the a() method, because this is how the reference a was identified in #2, which causes this expression (a if a = true) to return 1.
  6. However as pointed out in #4 the assignment to a has also occurred so this comparison becomes (1) == true

Note: If you remove the method definition this will raise a NameError because of #2 however the local variable assignment will still occur.

begin 
  c if c = 1 
rescue NameError 
  puts 'Oh' 
  c
end == c and c == 1 
# 'Oh'
#=> true
  •  Tags:  
  • ruby
  • Related