darkash
darkash

Reputation: 161

What does shorthand operator like (+=) in ruby really does?

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

Answers (2)

Todd A. Jacobs
Todd A. Jacobs

Reputation: 84353

Analysis

Defining our Terms

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.

Defining the Problem

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

Solutions

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

rohit89
rohit89

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

Related Questions