ChrisPhoenix
ChrisPhoenix

Reputation: 1090

How to make Ruby var= return value assigned, not value passed in?

There's a nice idiom for adding to lists stored in a hash table:

(hash[key] ||= []) << new_value

Now, suppose I write a derivative hash class, like the ones found in Hashie, which does a deep-convert of any hash I store in it. Then what I store will not be the same object I passed to the = operator; Hash may be converted to Mash or Clash, and arrays may be copied.

Here's the problem. Ruby apparently returns, from the var= method, the value passed in, not the value that's stored. It doesn't matter what the var= method returns. The code below demonstrates this:

class C
  attr_reader :foo
  def foo=(value)
    @foo = (value.is_a? Array) ? (value.clone) : value
  end
end

c=C.new
puts "assignment: #{(c.foo ||= []) << 5}"
puts "c.foo is #{c.foo}"
puts "assignment: #{(c.foo ||= []) << 6}"
puts "c.foo is #{c.foo}"

output is

assignment: [5]
c.foo is []
assignment: [6]
c.foo is [6]

When I posted this as a bug to Hashie, Danielle Sucher explained what was happening and pointed out that "foo.send :bar=, 1" returns the value returned by the bar= method. (Hat tip for the research!) So I guess I could do:

c=C.new
puts "clunky assignment: #{(c.foo || c.send(:foo=, [])) << 5}"
puts "c.foo is #{c.foo}"
puts "assignment: #{(c.foo || c.send(:foo=, [])) << 6}"
puts "c.foo is #{c.foo}"

which prints

clunky assignment: [5]
c.foo is [5]
assignment: [5, 6]
c.foo is [5, 6]

Is there any more elegant way to do this?

Upvotes: 4

Views: 3969

Answers (3)

Hauleth
Hauleth

Reputation: 23556

The prettiest way to do this is to use default value for hash:

# h = Hash.new { [] }
h = Hash.new { |h,k| h[k] = [] }

But be ware that you cant use Hash.new([]) and then << because of way how Ruby store variables:

h = Hash.new([])
h[:a] # => []
h[:b] # => []
h[:a] << 10
h[:b] # => [10] O.o

it's caused by that Ruby store variables by reference, so as we created only one array instance, ad set it as default value then it will be shared between all hash cells (unless it will be overwrite, i.e. by h[:a] += [10]).

It is solved by using constructor with block (doc) Hash.new { [] }. With this each time when new key is created block is called and each value is different array.

EDIT: Fixed error that @Uri Agassi is writing about.

Upvotes: 1

Danielle
Danielle

Reputation: 158

c.foo ||= []  
c.foo << 5

Using two lines of code isn't the end of the world, and it's easier on the eyes.

Upvotes: 5

J&#246;rg W Mittag
J&#246;rg W Mittag

Reputation: 369458

Assignments evaluate to the value that is being assigned. Period.

In some other languages, assignments are statements, so they don't evaluate to anything. Those are really the only two sensible choices. Either don't evaluate to anything, or evaluate to the value being assigned. Everything else would be too surprising.

Since Ruby doesn't have statements, there is really only one choice.

The only "workaround" for this is: don't use assignment.

Upvotes: 6

Related Questions