From: eregontp@... Date: 2020-11-26T14:37:03+00:00 Subject: [ruby-core:101094] [Ruby master Feature#17342] Hash#fetch_set Issue #17342 has been updated by Eregon (Benoit Daloze). Another name for this is `compute_if_absent`. It's notably the name used in concurrent-ruby: http://ruby-concurrency.github.io/concurrent-ruby/1.1.5/Concurrent/Map.html#compute_if_absent-instance_method And in Java, where it's even defined on Map (not just ConcurrentMap): https://docs.oracle.com/javase/8/docs/api/java/util/Map.html#computeIfAbsent-K-java.util.function.Function- Having it as a built-in method, it also makes it possible to avoid computing the key's `#hash` twice, and potentially avoid redoing the lookup in the Hash. Another way to do this pattern is to use the default block: ```ruby RequestStore.store = Hash.new do |h, k| h[k] = !MonitorValue.where('date >= ?', Time.now - 5.minutes).exists? end RequestStore.store[:monitor_value_is_delayed?] ``` Which already works fine. And it has the advantage that if multiple places want to read from the Hash they don't have to repeat the code. Is there a case this pattern wouldn't work and where `Hash#fetch_set` would work? This pattern can be made to work with parallelism too, see [Idiomatic Concurrent Hash Operations](https://eregon.me/blog/assets/research/thesis-thread-safe-data-representations-in-dynamic-languages.pdf), page 83. Regarding concurrency and parallelism, we need to define the semantics if we add this method. Of course, the assignment should not be performed if there is already a key, it must be "put if absent" semantics (`cache.fetch(key) { cache[key] = calculation }` is actually breaking that). The question is whether the given block can be executed multiple times for a given key. If not, it requires synchronization while calling the block, which can lead to deadlocks. If yes, it doesn't require synchronization while calling the block which seems safer, but it means the block can be called multiple times. ---------------------------------------- Feature #17342: Hash#fetch_set https://bugs.ruby-lang.org/issues/17342#change-88771 * Author: MaxLap (Maxime Lapointe) * Status: Open * Priority: Normal ---------------------------------------- I would like to propose adding the `fetch_set` method to `Hash`. It behaves just like `fetch`, but when using the default value (2nd argument or the block), it also sets the value in the Hash for the given key. We often use the pattern `cache[key] ||= calculation`. This pattern however has a problem when the calculation could return false or nil, as in those case, the calculation is repeated each time. I believe the best practice in that case is: ```ruby cache.fetch(key) { cache[key] = calculation } ``` With my suggestion, it would be: ```ruby cache.fetch_set(key) { calculation } ``` In these examples, each part is very short, so the `fetch` case is still clean. But as each part gets longer, the need to repeat cache[key] becomes more friction. Here is a more realistic example: ```ruby # Also using the key argument to the block to avoid repeating the # long symbol, adding some indirection RequestStore.store.fetch(:monitor_value_is_delayed?) do |key| RequestStore.store[key] = !MonitorValue.where('date >= ?', Time.now - 5.minutes).exists? end RequestStore.store.fetch_set(:monitor_value_is_delayed?) do !MonitorValue.where('date >= ?', Time.now - 5.minutes).exists? end ``` There is a precedent for such a method: Python has it, but with a quite confusing name: `setdefault(key, default_value)`. This does not set a default for the whole dictionary as the name would make you think, it really just does what is proposed here. https://docs.python.org/3/library/stdtypes.html#dict.setdefault -- https://bugs.ruby-lang.org/ Unsubscribe: