Have you ever come across this Ruby quirk?
foo if foo=1
You might expect it to return 1, but what it actually does is fail with the message NameError: undefined local variable or method `foo' for main:Object
There is a reason, and it makes a certain kind of sense. Allow me to explain.
To start: Ruby reads scripts from left-to-right, top-to-bottom.
Consider the interpreter. When it sees a word like foo it has to decide whether it’s a local variable or a function call (it already knows it’s not a keyword, constant, instance variable, global variable, etc. because of syntax rules.) If we’re in a method, the initial set of local variables are the method parameters; otherwise it’s empty. Thereafter, local variables are added whenever the interpreter sees an assignment (e.g. foo = 1)
Now back to the code: let’s step through an approximation of how the Ruby interpreter sees it.
parsed: [] vars: [] code: foo if foo=1 cursor:^
Since vars is empty, foo can’t be a variable, therefore it must be a function.
parsed: [function('foo')]
vars: []
code: foo if foo=1
cursor: ^
if is a keyword, which is allowed as a modifier after a function call.
parsed: [function('foo'), modifier('if',...)]
vars: []
code: foo if foo=1
cursor: ^
foo= is an assignment, so we can add foo to the list of variables.
parsed: [function('foo'), modifier('if',assign('foo',...))]
vars: ['foo']
code: foo if foo=1
cursor: ^
The right-hand side of the assignment is an integer literal.
parsed: [function('foo'), modifier('if',assign('foo',1))]
vars: ['foo']
code: foo if foo=1
cursor: ^
We’ve hit the end of the input; we’re done.
If I were to convert that ‘parsed’ input into an unambiguous canonical form, according to standard precedence and actual execution order, it might look something like this:
tmp = (foo = 1) if tmp foo() end
We can verify that the interpreter is reading that foo as a function by actually creating said function, and demonstrating that it’s being executed:
def foo() :bar end foo if foo=1 #=> :bar
To “fix” it we have to tell the parser it’s a variable, by assigning beforehand. We could do this the ugly way:
foo = nil foo if foo=1
...but a better way would be to be more explicit. Assignment in a condition is a bit dodgy (and Ruby even spits out a warning saying “found = in conditional, should be ==” – a strong hint that this is something to avoid), and in this case in particular, the intention of the line is a bit unclear. We can simultaneously fix it and make it more prosaic, without adding too much verbosity, thus:
foo=1 foo if foo
As a rule of thumb, only use simple predicates in modifiers.