An "append-only" / "write-only" hash in Ruby

189 Views Asked by At

I'm looking for a kind of "append-only" hash where keys may only be set once.

For example:

capitals = AppendOnlyHash.new
capitals['france'] = 'paris'
capitals['japan'] = 'tokyo'
capitals['france'] = 'nice' # raises immutable exception

Any library recommendations or ideas how to achieve this?

(Use case is a logging type object which will be passed to numerouis loosely connected classes, and wanting to detect if any use the same key.)

3

There are 3 best solutions below

1
Aleksei Matiushkin On BEST ANSWER

There are 10 methods, directly mutating the hash:

Hash.instance_methods.grep(/.+!\z/) << %i|[]= delete keep_if|
#⇒ [:select!, :filter!, :reject!, :compact!, delete, keep_if,
#   :transform_keys!, :transform_values!, :merge!, :[]=]

Also, there is a possibility to mutate values themselves (capitals['france'] << ' and Lyon',) so we are to prevent this as well.

class MyHash < Hash; end

MyHash.prepend(
  Module.new do
    (Hash.instance_methods.grep(/.+!\z/) | %i|delete keep_if|).each do |method|
      define_method(method) do |*args|
        raise "Method #{method} is restricted since it is mutating"
      end
    end
    def []=(key, val)
      raise "This hash is immutable" if key?(key)
      super(key, val.freeze) # to prevent inplace mutations
    end
  end
)

One needs to derive from Hash because otherwise we are to break all the hashes.

I did not test this code but it should work out of the box, (if not, the idea should be clear.)

2
iGian On

First idea, I did not considered any drawback:

class HashImmutable < Hash
  def []=(key,val)
    if self[key].frozen?
      super(key,val)
    else
      # self[key]
      raise 'Immutable'
    end
  end
end

hh = HashImmutable.new

hh[:france] = 'Paris'
hh[:italy] = 'Roma'
hh #=> {:france=>"Paris", :italy=>"Roma"}
hh[:italy] = 'Brescia'
#=> Immutable (RuntimeError)
0
Tom Lord On

Here is a naive attempt to create such a class. It seems to work fine for "basic" usage:

class AppendOnlyHash < Hash
  def []=(key, value)
    raise "APPEND ONLY!!" if keys.include?(key)
    super
  end
end

However, this certainly has some flaws.

Firstly, what happens if you call a destructive method on the object, which tries to delete some keys? Perhaps you could override all such methods - i.e. filter!, keep_if, delete, compact!, reject!, select!, transform_keys! and transform_values!. (Did I miss any?...)

Then, what to do with Hash#merge!? I guess that could be handled specially too; since it's valid to use if the no keys are being redefined.

And lastly, how can you ensure that the "append-only" hash values are never mutated? Consider the following:

capitals = AppendOnlyHash.new
str = "paris"
capitals['france'] = str
str << " CHANGED"

You could call .freeze on each value as it gets added to the hash, but even that's not 100% bulletproof - since the value may in turn be another Hash, which is susceptible to the same behaviour.


So in summary, I think this is possible via my basic implementation above, but I'd be cautious of increasingly-complex edge cases caused by objected mutation in "weird ways".