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:
- Define a method
a()
- The parser then encounters
a
as part of the then body so it tags thisa
as a method call (a()
) becausea
is not a local variable at this point and the ruby syntax allows for omission of parentheses in method calls. - The parser then encounters the test expression and here it marks
a
as a local variable, due to the assignment (=
) - The test expression is executed and in process it assigns
a
the value oftrue
and the test passes - The then body is now executed which calls the
a()
method, because this is how the referencea
was identified in #2, which causes this expression(a if a = true)
to return1
. - 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