tags:

views:

265

answers:

5

I have a question regarding the ||= statement in ruby and this is of particular interest to me as I'm using it to write to memcache. What I'm wondering is, does ||= check the receiver first to see if it's set before calling that setter, or is it literally an alias to x = x || y

This wouldn't really matter in the case of a normal variable but using something like:

CACHE[:some_key] ||= "Some String"

could possibly do a memcache write which is more expensive than a simple variable set. I couldn't find anything about ||= in the ruby api oddly enough so I haven't been able to answer this myself.

Of course I know that:

CACHE[:some_key] = "Some String" if CACHE[:some_key].nil?

would achieve this, I'm just looking for the most terse syntax.

A: 
CACHE[:some_key] ||= "Some String"

is equivalent to

CACHE[:some_key] = "Some String" unless CACHE[:some_key]

(which is equivalent to if + nil? unless CACHE[:some_key] is a boolean value).

In other words: yes, ||= will only write if the LHS is nil or false.

sepp2k
A: 

[I removed my example that was less accurate than other's. I leave my answer for the benchmarks that might be interesting to some. My point was:]

So basically

CACHE[:some_key] ||= "Some String"

is the same as

CACHE[:some_key] = "Some String" unless CACHE[:some_key]

I'm more for the first syntax, but then it's up to you since readibility is a bit reduced in that case.


I was curious, so here's some benchmarks:

require "benchmark"
CACHE = {}
Benchmark.bm do |x|
  x.report { 
    for i in 0..100000
      CACHE[:some_key] ||= "Some String" 
    end
  }
  x.report { 
    for i in 0..100000
      CACHE[:some_key] = "Some String" unless CACHE[:some_key] 
    end
  }
end


      user     system      total        real
  0.030000   0.000000   0.030000 (  0.025167)
  0.020000   0.000000   0.020000 (  0.026670)
marcgg
Even though you are right, your example offers no proof. If `CACHE[:some_key] ||= "Some String2"` would expand to `CACHE[:some_key] = CACHE[:some_key] || "Some String2"` it would *also* evaluate to `"Some string"`.
molf
@molf: I'm not sure what's your point
marcgg
A: 

Here's another demonstration that's a bit different than the other answers in that it explicitly shows when the Hash is being written to:

class MyHash < Hash
  def []=(key, value)
    puts "Setting #{key} = #{value}"
    super(key, value)
  end
end

>> h = MyHash.new
=> {}
>> h[:foo] = :bar
Setting foo = bar
=> :bar
>> h[:bar] ||= :baz
Setting bar = baz
=> :baz
>> h[:bar] ||= :quux
=> :baz

And by way of comparison:

// continued from above
>> h[:bar] = h[:bar] || :quuux
Setting bar = baz
=> :baz
Daniel Vandersluis
+12  A: 

This is extremely easy to test:

class MyCache
  def initialize
    @hash = {}
  end

  def []=(key, value)
    puts "Cache key '#{key}' written"
    @hash[key] = value
  end

  def [](key)
    puts "Cache key '#{key}' read"
    @hash[key]
  end
end

Now simply try the ||= syntax:

cache = MyCache.new
cache["my key"] ||= "my value"  # cache value was nil (unset)
# Cache key 'my key' read
# Cache key 'my key' written

cache["my key"] ||= "my value"  # cache value is already set
# Cache key 'my key' read

So we can conclude that no assignment takes place if the cache key already exists.

The following extract from the Rubyspec shows that this is by design and should not be dependent on the Ruby implementation:

describe "Conditional operator assignment 'obj.meth op= expr'" do
  # ...
  it "may not assign at all, depending on the truthiness of lhs" do
    m = mock("object")
    m.should_receive(:foo).and_return(:truthy)
    m.should_not_receive(:foo=)
    m.foo ||= 42

    m.should_receive(:bar).and_return(false)
    m.should_not_receive(:bar=)
    m.bar &&= 42
  end
  # ...
end

In the same file, there is a similar spec for [] and []= that mandates identical behaviour.

Although the Rubyspec is still a work in progress, it has become clear that the major Ruby implementation projects intend to comply with it.

molf
answer and example! I like it. thx very much!!
brad
Interestingly, the current Draft of the ISO Ruby Specification says exactly the opposite: the assignment must *always* occur. However, I believe that to be a bug, since the purpose of the ISO Spec is to describe (a subset of) the current behavior of all Ruby implementations. It also negates the whole purpose of `||=` in the first place.
Jörg W Mittag
+4  A: 
Jörg W Mittag
thanks for the a || a = b explanation. That definitely makes it clear that we're only setting if !a.
brad
@brad: Correct, even though the specification actually says the opposite. I'm currently investigating the issue.
Jörg W Mittag