x ||= y, Redux

Posted by Rick DeNatale Sat, 26 Apr 2008 15:11:00 GMT

A while back there was quite a thread on the Ruby-lang mailing list about the real semantics of the expression.

a[x] ||= some_value

In many books and articles on Ruby the form:


a <op>= b

Where <op> is an operator like + Is described as syntactic sugar:


a = a <op> b

While this is true in most cases, it isn’t’ true if the operator is ||.


This comes to light when the left hand side is a method call, to an accessor, or accessor-like method. For example


h = Hash.new("hello")
h[:fred] ||= ""
h #=> {}

Some find it surprising that the assignment doesn’t cause the hash to have :fred as a key. What this code snippet shows is that the assignment doesn’t actually assign anything if the left hand expression returns a logically true value. A Hash with a default value will return the default value when accessed by any key which is not present in the hash. Since h[:fred] returns the default value, the assignment doesn’t happen.


This affects any object which has ‘accessor’ methods. Here’s a class cooked up just to explore this aspect of Ruby.


class ChattyCathyclass ChattyCathy

def initialize(x=nil) @x = x puts "created x is now #{x.inspect}" end def x puts "x read x is #{@x.inspect}" @x end def x=(val) puts "x written, now #{val.inspect}" @x = val end

end


The purpose of this class is simply to let us see exactly when the x attribute is read and written. Now if we run this code


c = ChattyCathy.new(42)
puts "about to evaluate c.x ||= 43"
c.x ||= 43
c = ChattyCathy.new
puts "about to evaluate c.x ||= 43"
c.x ||= 43

We get the following output:


<br /> created x is now 42<br /> about to evaluate c.x ||= 43<br /> x read x is 42<br /> created x is now nil<br /> about to evaluate c.x ||= 43<br /> x read x is nil<br /> x written, now 43</p>

Which clearly illustrates just when the assignment actually happens.

The real expansion of x ||= y

Matz explains that the real expansion of x ||= y is:

x || x = y

The expectation that x ||= y is the same as x = x || y, does seem reasonable to someone ‘coming from’ C or one of it’s derivative languages. As far as I can determine, C introduced the notion of assignment operators like += and -=. And K&R defined these assignment operators as a shorthand for x = x + y, etc.

On the other hand, although C has logical operators || and && which, like Ruby have ‘short-circuit’ evaluation, it doesn’t allow ||=, or &&= as assignment operators.

Since || is a ‘short-circuit’ boolean operator, the right hand operand expression is only evaluated if the left hand operand expression evaluates to a logically false value, i.e. either nil or false.

The way that Matz included ||= as an assignment operator makes perfect sense to me. The ||= assignment operator reserves the short-circuit nature of ||.

So what about x &&= y

Although I haven’t seen this discussed anywhere, &&= in Ruby has similar behavior:

c = ChattyCathy.new
puts "about to evaluate c.x &&= true"
c.x &&= true
puts "about to evaluate c.x = \"hi\""
c.x = "Hi"
puts "about to evaluate c.x &&= true"
c.x &&= true
<p>created x is now nil<br /> about to evaluate c.x &amp;&amp;= true<br /> x read x is nil<br /> about to evaluate c.x = &#8220;hi&#8221;<br /> x written, now &#8220;Hi&#8221;<br /> about to evaluate c.x &amp;&amp;= true<br /> x read x is &#8220;Hi&#8221;<br /> x written, now true</p>

So the expansion of x &&= y is x && x = y

Update: 19 January 2010

Thanks to Colin Bartlett, who pointed out that I don’t know my left from my right, which is something I’ve known all my life! I’ve corrected the references to left and right in the description of how || short-circuits.


Trackbacks

Use the following link to trackback from your own site:
http://talklikeaduck.denhaven2.com/trackbacks?article_id=495

Comments

  1. Federico about 2 hours later:

    Some find it surprising that the assignment doesn’t cause the hash to have :fred as a key. What this code snippet shows is that the assignment doesn’t actually assign anything if the left hand expression returns a logically true value.

    Maybe I’m misreading your post but I don’t quite get why the example fails to show the expansion:

    h = Hash.new(“foo”) h[:bar] ||= “bla”

    If I expand that to:

    h[:bar] = h[:bar] || “bla”

    Then it makes perfect sense that the assignment doesn’t happen. As I see it, h[:bar] does exists (it’s != nil) so it never has to evaluate the second choice (“bla”).

    Carrying on with it, the K&R expansion for x ||= y would be:

    x = x || y

    We can say that that is:

    x = x || x = y

    And x = x <=> x (in Ruby those 2 are the same thing), so:

    x || x = y

    Which I think shows that the propsed expansion by Matz that you quote is the same as the original one.

    Maybe I missed something?

  2. she/she@hotmail.com about 22 hours later:

    You know what?

    I think this brain power of figuring out what it does should be better spent on designing and documenting ;)

    Reminds me of the old C adage where people didnt use enough ()

    Luckily I can write my ruby code without having to worry about that.

    Btw your way sets .default for hash, I think not every visitor will know that:

    h = Hash.new(“hello”) # => {} h[“a”] # => “hello”

    You see every key, even if it does not exist, will return that default value

    And knowing this, the h[:fred] ||= “” doesnt look as confusing anymore (although I still think it looks damn ugly.)

  3. Jeff 1 day later:

    So is the main advantage of the unexpected implementation, @x || x = y@, the fact that the interpreter can sometimes avoid the assignment operation that would must always occur for @x = x || y@?

    Thanks!

  4. Rick DeNatale 2 days later:

    Federico,

    In your expansion h[:bar] = h[:bar] || “bla” Just because the “bla” doesn’t get evaluated, doesn’nt mean that the assignment doesn’t happen. Let’s compare using my ChattyCathy class:

    c = ChattyCathy.new(1)
    puts "about to evaluate c.x = c.x || 2"
    c.x = c.x || 2
    puts "about to evaluate c.x ||= 3"
    c.x ||= 3

    Which outputs:



    created x is now 1
    about to evaluate c.x = c.x || 2
    x read x is 1
    x written, now 1
    about to evaluate c.x ||= 3
    x read x is 1

    So in your expansion, the assignment does in fact happen. Note here that I’m using assignment in the broad sense here. Since in this form of assignment, the Ruby parser is turning the assignment in to a method call to the x= method.

    Also for what it’s worth, there is no K&R expansion of x ||= y, because while C allows x |= y and x &= y, it does not allow x ||= y or x &&= y

  5. Federico 2 days later:

    Rick,

    You’re right, it will in fact call x= and I hadn’t noticed that.

    And yes, C has ho ||=, that’s why I said “would” as in “following the usual rules for the other operators, it would be…” but I should’ve expressed in a clearer way :)