Reputation: 161
While I can understand that x += 1
is equivalent to x = x + 1
, I'm interested what it does in the background.
I've tried in irb with x += 1
without first assigning value to x and surely I get an error, it says
NoMethodError (undefined method `+' for nil:NilClass)
When I tried checking x value after it errored, it's now a nil value which isn't quite clear when it was assigned, unless it's done by x += 1
statement.
So, I'm trying to understand how the += does thing as I could not find the documentation for it or it being buried behind the lot of ruby classes. Any pointer would be appreciated.
Upvotes: 0
Views: 65
Reputation: 84353
First of all, +
and +=
are methods, not keywords or operators. More accurately, + is defined as Integer#+, while +=
is syntactic sugar provided by the interpreter as shorthand for a common operation such as incremented assignment.
Secondly, +=
and related operations are called abbreviated assignments in Ruby. The full list of supported assignment shorthand items currently include:
["+=",
"-=",
"*=",
"/=",
"%=",
"**=",
"&=",
"|=",
"^=",
"<<=",
">>=",
"||=",
"&&="]
The only non-method operators in Ruby are:
["=", "..", "...", "!", "not", "&&", "and", "||", "or", "!=", "!~"]
and shorthand assignment operators like +=
are not user-definable.
The behavior you're seeing has to do with scope, and how the parser handles block-local variables. Specifically, the Ruby assignment documentation says:
[A] local variable is created when the parser encounters the assignment, not when the assignment occurs[.]
That means that top-level code such as:
x
#=> NameError (undefined local variable or method `x' for main:Object)
raises a NameError exception because defined? x #=> nil
. However, the parser auto-instantiates local variables when there's an assignment, so that the following works (sort of), at least in the sense that it doesn't raise NameError:
# x is autovivified as nil
x = x + 1
#=> NoMethodError (undefined method `+' for nil:NilClass)
However, nil doesn't have a NilClass#+ method, so you get NoMethodError instead of NameError. This is because:
nil.respond_to? :+
#=> false
The basic solution is to explicitly ensure your variable exists within the current scope, and that it is not nil. You can do this in a variety of ways, but the simplest thing is just to assign it directly. For example:
# assign zero to x unless it's truthy
x ||= 0
# perform incrementing assignment
x += 1
Alternatively, you can drop the shorthand and cast the autovivified x as an Integer, which does have a :+
method. For example, assuming x is currently undefined (e.g. you've restarted irb, or you've set x to nil):
x = x.to_i + 1
#=> 1
Upvotes: 1
Reputation: 5773
Ruby treats identifiers as a variables if it sees assignment (=
) even if the assignment fails.
irb(main):001:0> a
NameError: undefined local variable or method `a' for main:Object
from (irb):1
from /usr/bin/irb:12:in `<main>'
irb(main):002:0> a = 1 if false
=> nil
irb(main):003:0> a
=> nil
irb(main):004:0>
Even though a=1
never executes, a
gets defaulted to nil
.
Upvotes: 3