Feature #14580
openHash#store accepts a block
Description
Given a hash
hash = { a: 2 }
I want to update a single value in the hash:
hash[:a] = hash[:a] + 42
hash[:a] #=> 44
But instead, I would like to have a method that yields the current value for a given key and associates the block result with the key (similar to Hash#update). I think that Hash#store can be extended to support a block arg.
hash.store(:a) { |val| val + 42 }
hash[:a] #=> 44
Or it can be something like this:
hash.transform_values(:a, :b) { |val| val + 42 }
hash[:a] #=> 44
Updated by shevegen (Robert A. Heiler) over 6 years ago
If I understood your proposal correctly then you want an
additional way to update an existing value in a hash, correct?
So the comparable syntax parts would be:
hash[:a] = hash[:a] + 42
versus
hash.store(:a) { |val| val + 42 }
right?
So, if this is correct, then as I understand it, the major
point of your proposal, and benefit, is that you omit querying
the old value in the sedond variant, since you just operate
on the block variable called "val" in your case.
If this is indeed the case, and that is your proposal, then I
think I understand what you mean, feature-wise. In this case
you skip the step where you query the old value explicitely
and just tap into the block value for making a modification.
I think this is ok.
It follows my own "philosophy" of "do not make me think" or
"make me think less". :D
I have no idea how matz feels about it; perhaps someone could
suggest it in the upcoming ruby developer meeting in ~a
week or so.
The current documentation for Hash#store can be found at:
https://docs.ruby-lang.org/en/2.5.0/Hash.html#method-i-store
Updated by Eregon (Benoit Daloze) over 6 years ago
What should happen if the given key doesn't exist in Hash?
This looks like a compute-if-present operation.
Updated by Hanmac (Hans Mackowiak) over 6 years ago
hash.transform_values(:a, :b) { |val| val + 42 }
hash[:a] #=> 44
what about the b key? should it:
a) throw exception
b) gives nil
to the block ? which your code would be an NoMethod + for nil
c) will be skipped
Updated by Soilent (Konstantin x) over 6 years ago
Hi Robert,
Thank you for your reply. You understood everything correctly.
Also, I might be wrong, but it seems to me that in the following case
hash[:a] = hash[:a] + 42
Ruby VM will look up the key twice. The proposed method should eliminate the second lookup in this case.
Updated by zverok (Victor Shepelev) over 6 years ago
Maybe a bit off-topic, but I experimented with same ideas in hm gem. It allows code like this:
Hm(hash)
.transform_values(:a) { |val| val + 42 }
.to_h
After trying several approaches in production, the design decision I've made about not found keys is simply ignore them.
Updated by Soilent (Konstantin x) over 6 years ago
Eregon (Benoit Daloze) wrote:
What should happen if the given key doesn't exist in Hash?
This looks like a compute-if-present operation.
Good question, thank you. I think, the result of default_proc or the default value should be yielded.
Updated by Soilent (Konstantin x) over 6 years ago
Hanmac (Hans Mackowiak) wrote:
hash.transform_values(:a, :b) { |val| val + 42 } hash[:a] #=> 44
what about the b key? should it:
a) throw exception
b) givesnil
to the block ? which your code would be anNoMethod + for nil
c) will be skipped
Thanks for the question.
I think that hash.store(:b)
should yield the default value if the key does not exist, i.e. option b.
But in case of hash.transform_values(:a, :b)
, when we want to update several keys, it is best to skip non-existent keys (option c)
Updated by sawa (Tsuyoshi Sawada) over 6 years ago
Why not write hash[:a]+= 42
?
Updated by Soilent (Konstantin x) over 6 years ago
sawa (Tsuyoshi Sawada) wrote:
Why not write
hash[:a]+= 42
?
Good point, but this works only for arithmetic operators (and also does 2 key lookups). Consider another example hash.store(:time) { |ts| Time.parse(ts) }
Updated by mame (Yusuke Endoh) over 6 years ago
I think it is not so simple to optimize the double lookup by this API. Consider:
hash.store(:a) {|val| 10000.times {|n| hash[n] = true }; val + 42 }
or:
hash.store(:a) {|val| hash.rehash; val + 42 }
We need to keep a flag if rehash occurred or not during the block executed.
Updated by Eregon (Benoit Daloze) over 6 years ago
Soilent (Konstantin x) wrote:
Consider another example
hash.store(:time) { |ts| Time.parse(ts) }
That looks weird to me.
Either the Hash is caching String to Time, and then it should use
Hash.new { |h,k| h[k] = Time.parse(k) }
or it contains other data and then there seems to be little reason to first store a String for key :time and then only later parse it to a Time instance.
Updated by Eregon (Benoit Daloze) over 6 years ago
mame (Yusuke Endoh) wrote:
We need to keep a flag if rehash occurred or not during the block executed.
Also, what should happen with:
hash.store(:a) { |v| hash.delete(:a); v + 42 }
"store" starts to feel to me like the wrong name, it sounds more like an "update" of an existing key (but Hash#update is an alias of Hash#merge!).
Updated by Soilent (Konstantin x) over 6 years ago
mame (Yusuke Endoh) wrote:
I think it is not so simple to optimize the double lookup by this API. Consider:
hash.store(:a) {|val| 10000.times {|n| hash[n] = true }; val + 42 }
or:
hash.store(:a) {|val| hash.rehash; val + 42 }
We need to keep a flag if rehash occurred or not during the block executed.
I think that an exception should be thrown if the block modifies the hash.
Updated by Soilent (Konstantin x) over 6 years ago
Eregon (Benoit Daloze) wrote:
Soilent (Konstantin x) wrote:
Consider another example
hash.store(:time) { |ts| Time.parse(ts) }
That looks weird to me.
Either the Hash is caching String to Time, and then it should useHash.new { |h,k| h[k] = Time.parse(k) }
or it contains other data and then there seems to be little reason to first store a String for key :time and then only later parse it to a Time instance.
I see your point, but the example was not about String to Time caching. Let's say you receive an HTTP POST request with the body timestamp=2018-03-08T11:24:44Z&temperature=27
. You might want to validate the request and store it in a database:
begin
params.store(:temperature) { |tm| Integer(tm) }
params.store(:timestamp) { |ts| Time.parse(ts) }
rescue ArgumentError => err
# Handle invalid request
end
# Do something with `params`
db[:events].insert(params)
I think that Hash#store with a block arg looks quite natural with the rest of the methods from the Hash API.