Simple code which I can not explain to myself:
puts a if a = 1
This results in
warning: found = in conditional, should be ==
NameError: undefined local variable or method 'a' for main:Object
Though, now upon checking a
we can see, that it has been defined:
a #=> 1
Why does a
get assigned to 1
despite the exception thrown?
From the docs:
The confusion comes from the out-of-order execution of the expression. First the local variable is assigned-to then you attempt to call a nonexistent method [
a
].
This part is still confusing - why does interpreter not detecting already defined local variable a
and still tries to call a "nonexisting" method? Should it not check for local variables as well, find defined local variable a
and print 1
?
When an error in the code is encountered, an exception is "raised" or "thrown" and the program shuts down by default. Ruby publishes an exception hierarchy with predefined classes. NameErrors are in the StandardError class, along with RuntimeError, ThreadError, RangeError, ArgumentError and others.
We enclose the code that could raise an exception in a begin/end block and use rescue clauses to tell Ruby the types of exceptions we want to handle. Everything from begin to rescue is protected. If an exception occurs during the execution of this block of code, control is passed to the block between rescue and end.
When an error in the code is encountered, an exception is "raised" or "thrown" and the program shuts down by default. Ruby publishes an exception hierarchy with predefined classes.
You'll see this error when the code refers to a class or module that it can't find, often because the code doesn't include require, which instructs the Ruby file to load the class. In Ruby, variables/methods begin with lowercase letters, while classes begin with uppercase letters.
Let's take a look at Ruby's abstract syntax tree for modifier if
:
$ ruby --dump=parsetree -e 'puts a if a = 1'
# @ NODE_SCOPE (line: 1, code_range: (1,0)-(1,15))
# +- nd_tbl: :a
# +- nd_args:
# | (null node)
# +- nd_body:
# @ NODE_PRELUDE (line: 1, code_range: (1,0)-(1,15))
# +- nd_head:
# | (null node)
# +- nd_body:
# | @ NODE_IF (line: 1, code_range: (1,0)-(1,15))
# | +- nd_cond:
# | | @ NODE_DASGN_CURR (line: 1, code_range: (1,10)-(1,15))
# | | +- nd_vid: :a
# | | +- nd_value:
# | | @ NODE_LIT (line: 1, code_range: (1,14)-(1,15))
# | | +- nd_lit: 1
# | +- nd_body:
# | | @ NODE_FCALL (line: 1, code_range: (1,0)-(1,6))
# | | +- nd_mid: :puts
# | | +- nd_args:
# | | @ NODE_ARRAY (line: 1, code_range: (1,5)-(1,6))
# | | +- nd_alen: 1
# | | +- nd_head:
# | | | @ NODE_VCALL (line: 1, code_range: (1,5)-(1,6))
# | | | +- nd_mid: :a
# | | +- nd_next:
# | | (null node)
# | +- nd_else:
# | (null node)
# +- nd_compile_option:
# +- coverage_enabled: false
And for standard if
:
$ ruby --dump=parsetree -e 'if a = 1 then puts a end'
# @ NODE_SCOPE (line: 1, code_range: (1,0)-(1,24))
# +- nd_tbl: :a
# +- nd_args:
# | (null node)
# +- nd_body:
# @ NODE_PRELUDE (line: 1, code_range: (1,0)-(1,24))
# +- nd_head:
# | (null node)
# +- nd_body:
# | @ NODE_IF (line: 1, code_range: (1,0)-(1,24))
# | +- nd_cond:
# | | @ NODE_DASGN_CURR (line: 1, code_range: (1,3)-(1,8))
# | | +- nd_vid: :a
# | | +- nd_value:
# | | @ NODE_LIT (line: 1, code_range: (1,7)-(1,8))
# | | +- nd_lit: 1
# | +- nd_body:
# | | @ NODE_FCALL (line: 1, code_range: (1,14)-(1,20))
# | | +- nd_mid: :puts
# | | +- nd_args:
# | | @ NODE_ARRAY (line: 1, code_range: (1,19)-(1,20))
# | | +- nd_alen: 1
# | | +- nd_head:
# | | | @ NODE_DVAR (line: 1, code_range: (1,19)-(1,20))
# | | | +- nd_vid: :a
# | | +- nd_next:
# | | (null node)
# | +- nd_else:
# | (null node)
# +- nd_compile_option:
# +- coverage_enabled: false
The only difference is the method argument for puts
:
# | | | @ NODE_VCALL (line: 1, code_range: (1,5)-(1,6))
# | | | +- nd_mid: :a
vs:
# | | | @ NODE_DVAR (line: 1, code_range: (1,19)-(1,20))
# | | | +- nd_vid: :a
With modifier if
, the parser treats a
as a method call and creates a NODE_VCALL
. This instructs the interpreter to make a method call (although there is a local variable a
), resulting in a NameError
. (because there is no method a
)
With standard if
, the parser treats a
as a local variable and creates a NODE_DVAR
. This instructs the interpreter to look up a local variable which works as expected.
As you can see, Ruby recognizes local variables at the parser level. That's why the documentation says: (emphasis added)
the modifier and standard versions [...] are not exact transformations of each other due to parse order.
Ruby parses code left-to-right. Local variables get defined when the first assignment to them is being parsed. At puts a
, no assignment to a
has been parsed yet, thus the local variable a
doesn't exist yet, and Ruby assumes a
is a method call. The local variable only exists to the right and below the assignment.
At runtime, Ruby has to evaluate the condition in order to figure out whether to execute the puts
, so a
gets initialized to 1
.
You seem to be executing that code within some kind of REPL. Usually, REPLs rescue exceptions instead of terminating, which is why your code keeps executing instead of terminating, and since we are now below the assignment, the variable is defined, and since the assignment was executed, the variable is initialized.
If the distinction between definition and initialization of a variable is unclear to you, meditate on this:
foo
# NameError
if false
foo = 42
end
foo
#=> nil
foo = :bar
foo
#=> :bar
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With