Feature #20818
openAllow passing a block to Hash#store (to update current value)
Description
I would like to propose a block form for Hash#store
. In addition to passing a value, it should also be allowed to pass a block.
If passed a block instead of a value, the block is called with the current value or, if unset, the hash's default value; the block's return value will be the value that is stored.
This is similar to e.g., computeIfPresent
/computeIfAbsent
in Java (I think).
I can think of several situations where this would be useful, in particular for caches and counters.
For instance:
counts = {}
elements.each do |element|
counts.store(element){ (_1 || 0) + element.weight}
end
or even more elegant with a default value:
counts = {}
counts.default = 0
elements.each do |element|
counts.store(element){ _1 + element.weight}
end
Moreover, using the block form we should be able to do operations such as h[k] ||= x
, h[k] -= x
, h[k] += x
, or more generally h[k] = f(h[k])
, with a single "hashing round-trip".
If I'm not mistaken, currently these involve two separate calls to #[]
and #[]=
(with two calls to #hash
?).
Finally, this makes #store
a proper dual of #fetch
which, similarly, can be passed a block.
I have an experimental implementation of this (GitHub PR) at: https://github.com/ruby/ruby/pull/11956
Updated by mame (Yusuke Endoh) about 2 months ago
Discussed at the dev meeting. @matz (Yukihiro Matsumoto) said "I don't like the style so much, but if the patch is safe and actually improves performance, I'm willing to consider it. I'd like you to show the benchmark."
I am not sure if it is safe to execute arbitrary Ruby code in the RHASH_UPDATE_ITER callback. For example, what will happen if we update the hash itself inside the block of Hash#store, like h.store(:key){ h[:another_key] = 1 }
? Does the patch prohibit this? If it is allowed, we need to care the possibility that rehash may occur.
Updated by AMomchilov (Alexander Momchilov) 10 days ago
This is related to my Hash#exchange_value
proposal, so I'll link that here. https://bugs.ruby-lang.org/issues/20300
I guess the difference is whether you already know the replacement you want to use instead, or if you'd like to compute it lazily when the key is already set.