[Proposal] Add Hash#transform_value to ActiveSupport (Hash-only #bury)

Hey everyone, there is a closed proposal about adding bury method into Ruby, which is an opposite of dig. The issue with it is that it can’t work for both Array and Hash classes as dig does.

But Hash-only usecase still looks very relevant to me, that is why I thought that adding something like this to ActiveSupport may be concidered.

I think this method shouldn’t be named bury to not cause confusion about it being the opposite of dig. For me Hash#transform_value or Hash#deep_transform_value seems to be a good name.

Some usecases:

  • Adding nested value:
{}.transform_value(:a, :b) { 'c' }
# => { :a => { :b => "c" } }
  • Updating nested value:
{ a: { b: '1' } }.transform_value(:a, :b) { |value| value.to_i }
# => { :a => { :b => 1 } }

What do you think about this?

IMHO, the transform part of your proposed names don’t seem obvious to me.

In the case where you are adding a new nested value there is no transformation happening at all. In the case of updating a nested value the caller could use the argument of the block to transform the value. Or they might just have the block return something unrelated to the existing value (much like with adding a value).

I.E. transforming seems like a nice feature of such a function but it seems to be a secondary feature and therefore I wouldn’t put it in the name of the method. I would think something like deep_set or deep_insert or something might be better.

Separate from naming there is also the question of does having a special method like this provide enough value over the current options in Ruby.

For updating an existing value we can obviously do that pretty well with:

hsh[:a][:b] = 'foo'

Even transforming we can do in limited ways. For example:

hsh[:a][:b] += 1

If the value cannot mutate its state or there isn’t a syntax shortcut like += it becomes a bit more verbose:

hsh[:a][:b] = hsh[:a][:b].to_i

While there is a bit of redundancy here it doesn’t seem that the following is significantly better:

hsh.deep_set :a, :b, &:to_i

With regard to deep inserting a new value I think your proposal has more value. There is one existing solution out there to consider. We can use the hash default value to auto-create nested hashes for us:

class RecursiveHash < Hash
  def initialize
    super() { |h, k| h[k] = RecursiveHash.new }

With this we can now do things like:

hsh = RecursiveHash.new
hsh[:a][:b] = 'c'

You could even upgrade an existing hash by changing it’s default_proc:

hsh = {}
hsh.default_proc = proc { |k, v| k[v] = RecursiveHash.new }
hsh[:a][:b] = 'c'

This isn’t really the same thing as what you have designed. It auto-creates hashes as part of the intrinsic nature of the hash vs it being a one-time operation. But there might be overlap between what you have designed and this sort of structure making it a suitable replacement in some situations.

Just my two cents. If such a method was added I wouldn’t find it problematic but I also would probably rarely if ever use it as I don’t think I typically do that sort of deep setting as often as I do deep getting.

1 Like

I agree that Hash#deep_set or Hash#deep_insert are more clear.

The real world use-case that made me think about this method was a little bit different, it was similar to this (but keys were longer :grin:):

hash[:some_key] ||= {}
hash[:some_key][:some_nested_key] = hash[:some_key][:some_nested_key]&.to_i || default_value 

Using this method it can be rewritten to:

hash.deep_set(:some_key, :some_nested_key) { _1&.to_i || default_value }