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 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:

created x is now 42 about to evaluate c.x ||= 43 x read x is 42 created x is now nil about to evaluate c.x ||= 43 x read x is nil x written, now 43

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 left hand operand expression is only evaluated if the right 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
created x is now nil about to evaluate c.x &#38;&#38;= true x read x is nil about to evaluate c.x = &#8220;hi&#8221; x written, now &#8220;Hi&#8221; about to evaluate c.x &#38;&#38;= true x read x is &#8220;Hi&#8221; x written, now true

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

Posted in  | 5 comments | no trackbacks

Comments

  1. Federico said 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 said 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 said 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 said 1 day 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 said 1 day 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 :)

Trackbacks

Use the following link to trackback from your own site:
http://talklikeaduck.denhaven2.com/articles/trackback/495

Comments are disabled