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.