A while back there was quite a thread on the Ruby-lang mailing list about the real semantics of the expression.
a[x] ||= some_valueIn many books and articles on Ruby the form:
a <op>= bWhere <op> is an operator like + Is described as syntactic sugar:
a = a <op> bWhile 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 ||= 43We 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 = yThe 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 &&= truecreated x is now nil
about to evaluate c.x &&= true
x read x is nil
about to evaluate c.x = “hi”
x written, now “Hi”
about to evaluate c.x &&= true
x read x is “Hi”
x written, now true
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





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?
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.)
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!
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 ||= 3Which 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
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 :)