In Ruby, you can conditionally set a variable like this:

  x ||= 1

If x is not initialized—or if it is set to nil or false—it will be assigned 1. If it’s already set to a Booleanly true value (i.e., anything other than nil or false), it will remain unchanged.

Most descriptions of this idiom, including mine, have said that it expands to this:

  x = x || 1

Last year, though, an edge case came to light on the ruby-talk mailing list. It involves hashes with default values.

Trouble in ||=-land

Here’s an irb session illustrating the case. First, we’ll create a hash with a default value of 1.

  >> h = Hash.new(1)
  => {}

Remember that the default value is what the hash returns for non-existent keys. There are no assignment implications; referring to a non-existent key does not result in setting that key in the hash.

  >> h[:x]
  => 1
  >> h
  => {}        # still empty!

Now let’s try the ||= idiom.

  >> h[:x] ||= 2
  => 1

Once again, no assignment has taken place:

  >> h
  => {}

This struck a number of us on the mailing list as weird. It certainly means that ||= does not expand the way we had assumed it did. Here’s the proof: try the expanded version, and you’ll see that it does assign to the hash, as you’d expect.

  >> h[:x] = h[:x] || 2
  => 1
  >> h
  => {:x=>1}

So there we have proof that x ||= y and x = x || y are not interchangeable.

That raises two questions: first, what does x ||= y expand to, and second, do we like it?

The true expansion of ||=

At RubyConf 2007, I was running a “Ruby Clinic”, where people could come and go and ask questions about Ruby. Matz stopped by for a while, and the ||= topic came up. He explained that the real expansion is:

  x or x = y

Note (a day later): It’s been pointed out to me that expansion as x || x = y is more accurate. I believe that’s right, though my examples here didn’t flush it out.

Sure enough, expanding it this way produces parallel results in the hash case. Continuing the same irb session:

  >> h
  => {:x=>1}
  >> h[:y] ||= 3
  => 1

After that conditional assignment, the hash has not changed.

  >> h
  => {:x=>1}

And expanding it in the “or” style also does not change the hash:

See “Note”, above.

  >> h[:y] or h[:y] = 3
  => 1
  >> h
  => {:x=>1}

That clears that up. But do we like the way this plays out with hash defaults?

Editorial comments

I have mixed feelings about it. I can see that it makes sense. On the other hand, I can’t help thinking that when you write this:

  hash[:x] ||= 2

you’re really expecting the hash to end up with an :x key. In other words, my gut feeling is that it should mean:

  hash[:x] = 2 unless hash.has_key?(:x)

However, I understand that having it mean that would involve special-casing the hash case, and in the long run, I’m not in favor of that.

So what it comes down to is: heads up a bit with ||=. It works fine, but you need to know the real expansion.

6 Responses to “A short-circuit (||=) edge case”

  1. David Says:

    It was pointed out on another blog (which rejected my comment for some reason) that x ||= 1 expands to this:

    x || x = 1

    rather than:

    x or x = 1

    I think that’s right. I have a memory of getting the ‘or’ version from a very authoritative source, but I won’t blame my source if I under-tested it :-)

    The evidence for the difference comes when you’re assigning to the whole expression.

    irb(main):015:0> a = nil
    => nil
    irb(main):016:0> b = a ||= 1
    => 1
    irb(main):017:0> b
    => 1
    irb(main):018:0> a
    => 1
    irb(main):019:0> a = nil
    => nil
    irb(main):020:0> c = a or a = 1
    => 1
    irb(main):021:0> c
    => nil
    

    c = a or a = 1 doesn’t assign to c, where as c ||= a does. (Give or take my choice of variable name.)

    I’ll leave comments open on this one, at least for now.

    David

  2. Giles Bowkett Says:

    I actually find the default behavior understandable enough, but at the same time, I think there’s a reasonable argument for monkey-patching ||= here. Tried it, though, and it looks as if it’s one of those operator shortcuts that can’t be redefined (outside of Rubinius).

  3. Koz Says:

    I believe the expansion to x || x = 1 is an optimisation as it avoids doing an assignment if the value is already set.

  4. sam roberts Says:

    I don’t agree it makes sense, or that it works ok, or that making it work as expected would be a “special case for hashes”. Whats happening is a special case optimization for NON hashes, and its producing the wrong results for hashes.

    x op= y

    should have the same behaviour as

    x = x op y

    the fact that in simple assignments

    x = x || y

    is the same as

    x || x = y

    and its more efficient in most cases to do the latter doesn’t excuse that its NOT the same when the operators at play are []= (which is user defineable) instead of simple assignment, = (which is not user defineable).

    What’s that great slogan of the Smalltalk VM implementors? “Its Ok to cheat if nobody notices”?

    Its OK ruby usually cheats, its not OK if you can notice, and if an assignment operator didn’t assign… its noticeable.

    Btw, the same “non-assignment” cheat makes

    x &&= y

    not do assignment, either.

    irb(main):076:0> h=Hash.new(false) => {} irb(main):077:0> h0 &&= 1 => false irb(main):078:0> h => {} irb(main):079:0> h0 = h0 && 1 => false irb(main):080:0> h => {0=>false}

  5. David Says:

    @giles and @kaz - The whole “expansion” thing is something of a programmer-space extrapolation from the behavior, since at least in MRI I believe that ||= is itself an operator and doesn’t actually get pre-parsed. (That’s not a direct answer to anything you guys said - just a kind of follow-up comment.)

    @sam I agree that I would prefer x op= y to always be x = x op y. Although… that would produce a result with hash defaults that was weird in its own way:

    irb(main):001:0> h = Hash.new(1)
    => {}
    irb(main):002:0> h[:x] = h[:x] || 2
    => 1
    irb(main):003:0> h
    => {:x=>1}

    Here, the || 2 part has no effect at all. If h[:x] is set, it remains what it was, and if it wasn’t, it gets set to the default.

    The “special case” I was thinking would be if the above resulted in :x => 2.

  6. sam roberts Says:

    @david

    Absolutely! If we had

    h[:x] => 1
    h[:x] ||= 2
    h[:x] => 2

    that would be even more strange and confusing than the current behaviour!

    lhs ||= rhs doesn’t mean “set the left-hand side if its not set where default hash values are considered not to be set”, it (should) mean “set the left hand side to lhs || rhs”, and 1 || 2 => 1 in ruby.

    Its arguably strange and confusing that you can have

    h[:x] => 1 h.has_key? :x => false

    but the hash docs describe how default values work, and we understand and like them.

    Anybody who “knows” that “x op= y” means “x = x op y” in ruby is capable of figuring out for themselves how ||= will interact with default hash values. Unfortunately, they will be wrong.

    Ah, well, no language is perfect, but IMHO this is unruby-like and surprising, and should be changed in 1.9. Hopefully in some clever way that preserves the optimization!

    Cheers

Sorry, comments are closed for this article.