0

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?

ybakos
  • 8,152
  • 7
  • 46
  • 74
  • It is entirely contrived to provide a bare-bones, simple example to expose the problem, nothing more. Thank you for contributing an answer, and feel free to edit my post. – ybakos Feb 05 '23 at 06:03

3 Answers3

4
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.

Holger Just
  • 52,918
  • 14
  • 115
  • 123
  • Ohhhhh. I see where I messed up. Function f's parameter is named `a`, so it is not nil. This really threw me off. Had I defined `def f(b)`, I would have observed the expected behavior. Human fault again!! – ybakos Feb 03 '23 at 01:48
1

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
engineersmnky
  • 25,495
  • 2
  • 36
  • 52
1
def g(b)
  return a = b if !a.nil?
  return 'oh'
end

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

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

There are several potential reasons but the more important question to answer first is this; Why were you expecting 42?

  1. Is it because you thought a was already being explicitly defined in the prior method definition and so it should already have some value? If so, it should be noted that methods starting with a lower case letter are local variables that cannot be accessed outside the method in which they are defined. I'm assuming that this is not your issue and that you already have a proper understanding of variable scope. I thought it might be worth mentioning though just in case--especially for other users who might have a similar problem in the future.
  2. Is it because you thought a was implicitly being assigned a value other than nil? If so, its not. You need to define a before you can test against it. As mentioned by others, you should have probably expected an error if anything since you can't even test against a to see whether or not its nil unless it was already defined.

I can't see any logical reason why someone would expect to see 42 outside those 2 scenarios but maybe I'm missing something.

What is the order of evaluation here that is the difference between f and g, and between g and h?Why is a always evaluating to anything other than nil (!nil?) ?

Method g has already been discussed by myself and others. The only reason it works at all instead of throwing an error though is because a is being implicitly assigned a value of nil by lexical parsing/order of operations (see answers already posted by others).

Method f is using a parameter named a so a is being explicitly defined before you test against it.

Method h is also explicitly defining a (a = b and not a = b if) before testing against it.