4

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.)

mahemoff
  • 44,526
  • 36
  • 160
  • 222

3 Answers3

3

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.)

Aleksei Matiushkin
  • 119,336
  • 10
  • 100
  • 160
2

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)
iGian
  • 11,023
  • 3
  • 21
  • 36
  • My concern is that the hash could be updated in other ways. There are methods beyond `[]` that mutate the hash. – mahemoff May 01 '19 at 12:41
  • 1
    Maybe you could add such methods to the class, for example `def transform_values!; raise 'Immutable'; end` – iGian May 01 '19 at 12:51
1

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".

Tom Lord
  • 27,404
  • 4
  • 50
  • 77