views:

45

answers:

1

In order to implement auto-vivification of Ruby hash, one can employ the following class

class AutoHash < Hash
  def initialize(*args)
    super()
    @update, @update_index = args[0][:update], args[0][:update_key] unless 
args.empty?
  end

  def [](k)
    if self.has_key?k
      super(k)
    else
      AutoHash.new(:update => self, :update_key => k)
    end
  end

  def []=(k, v)
    @update[@update_index] = self if @update and @update_index
    super
  end

  def few(n=0)
    Array.new(n) { AutoHash.new }
  end
end

This class allows to do the following things

a = AutoHash.new
a[:a][:b] = 1
p a[:c] # => {}             # key :c has not been created
p a     # => {:a=>{:b=>1}}  # note, that it does not have key :c

a,b,c = AutoHash.new.few 3
b[:d] = 1
p [a,b,c] # => [{}, {:d=>1}, {}]  # hashes are independent

There is a bit more advanced definition of this class proposed by Joshua, which is a bit hard for me to understand.

Problem

There is one situation, where I think the new class can be improved. The following code fails with the error message NoMethodError: undefined method '+' for {}:AutoHash

a = AutoHash.new
5.times { a[:sum] += 10 }

What would you do to handle it? Can one define []+= operator?


Related questions

  1. Is auto-initialization of multi-dimensional hash array possible in Ruby, as it is in PHP?
  2. Multiple initialization of auto-vivifying hashes using a new operator in Ruby ruby hash initialization r
  3. still open: How to create an operator for deep copy/cloning of objects in Ruby?
+4  A: 

There is no way to define a []+= method in ruby. What happens when you type

x[y] += z

is

x[y] = x[y] + z

so both the [] and []= methods are called on x (and + is called on x[y], which in this case is an AutoHash). I think that the best way to handle this problem would be to define a + method on AutoHash, which will just return it's argument. This will make AutoHash.new[:x] += y work for just about any type of y, because the "empty" version of y.class ('' for strings, 0 for numbers, ...) plus y will almost always equal y.

class AutoHash
  def +(x); x; end
end

Adding that method will make both of these work:

# Numbers:
a = AutoHash.new
5.times { a[:sum] += 10 }
a[:sum] #=> 50

# Strings:
a = AutoHash.new
5.times { a[:sum] += 'a string ' }
a[:sum] #=> "a string a string a string a string a string "

And by the way, here is a cleaner version of your code:

class AutoHash < Hash
  def initialize(args={})
    super
    @update, @update_index = args[:update], args[:update_key]
  end

  def [](k)
    if has_key? k
      super(k)
    else
      AutoHash.new :update => self, :update_key => k
    end
  end

  def []=(k, v)
    @update[@update_index] = self if @update and @update_index
    super
  end

  def +(x); x; end

  def self.few(n)
    Array.new(n) { AutoHash.new }
  end
end

:)

Adrian
Cool! Thanks a lot, Adrian! For a moment I was thinking about the case when `a[:sum]` has already a value of a different type, but then I realize that such raise of exception is a good thing to have.
Andrey
@Andrei: When `a[:sum]` has a value that is not a `AutoHash`, the exception will be raised. For example: `a = AutoHash.new; a[:x] = 5; a[:x] += 'str'` will raise an exception.
Adrian