Feature #16739
openAllow Hash#keys and Hash#values to accept a block for filtering output
Description
I often see code like the following:
hash.select { |_, v| v == :some_value }.keys
hash.keys.select { |k| k.nil? || k.even? }
hash.select { |k, v| valid_key?(k) && valid_value?(v) }.values
Each of these code snippets must allocate an intermediate data structure. I propose allowing Hash#keys
and Hash#values
to accept optional block parameters that take both key and value. For example, the above code could be rewritten as:
hash.keys { |_, v| v == :some_value }
hash.keys { |k, _| k.nil? || k.even? }
hash.values { |k, v| valid_key?(k) && valid_value?(v) }
This behavior:
- Does not break any existing code (since
Hash#keys
andHash#values
do not currently accept blocks). - Is very readable—it's obvious what it does at a glance.
- Is more efficient than current alternatives.
- Is more concise than current alternatives.
- Is flexible and useful in a variety of scenarios, because the block has access to both key and value (unlike the behavior proposed in #14788).
Updated by sawa (Tsuyoshi Sawada) over 4 years ago
All it does is saves you from typing select
. It does not look like the proposed feature makes much difference unless such situation is frequently met. Do you have any use case?
Also, the purpose of a block following the methods keys
and values
does not seem to be immediately clear.
Updated by mame (Yusuke Endoh) over 4 years ago
I doubt if it is obvious. See the following code. I believe that many people expect .map
.
hash.keys {|k| k.to_s }
If you want to avoid an intermediate array, you may want to use .each_key
or .filter_map
.
hash.each_key.select {|k| k.nil? || k.even? }
hash.filter_map {|k, v| k if valid_key?(k) && valid_value?(v) }
Personally I like the following explicit code, though.
keys = []
hash.each do |k, v|
keys << k if valid_key?(k) && valid_value?(v)
end
Updated by jacobevelyn (Jacob Evelyn) over 4 years ago
All it does is saves you from typing
select
. It does not look like the proposed feature makes much difference unless such situation is frequently met. Do you have any use case?
I see code that could be improved with this all the time, including in projects like Ruby, JRuby, Rails, Discourse, GitLab, Metasploit, Active Admin, and Airbnb's Nerve (see links).
As I mentioned in my original post, it does not just save you from typing select
—it also avoids an unnecessary Hash allocation, making it more efficient as well as more concise.
If you want to avoid an intermediate array, you may want to use
.each_key
or.filter_map
.
Many of these use cases aren't supported by .each_key
or .each_value
because they require looking at the one that's not being returned (or both keys and values). Honestly I wasn't aware of .filter_map
; you're right that it's an option but I find it a bit verbose and hard to read.
I doubt if it is obvious. See the following code. I believe that many people expect
.map
.
Obviously we can disagree about this. I think about it like Enumerable#count
with a param vs. a block—the method's meaning doesn't change, but it becomes more flexible and powerful when you use a block.
Updated by Eregon (Benoit Daloze) over 4 years ago
Thank you for the examples and links.
filter_map
looks good enough for this to me.
In general I think we avoid adding blocks to core methods, because indeed it's not clear if people expect #map, #select or #filter_map behavior, and it's so much clearer with the explicit call.
Updated by sawa (Tsuyoshi Sawada) over 4 years ago
Includes a duplicate of #14788.
Updated by jacobevelyn (Jacob Evelyn) over 4 years ago
sawa (Tsuyoshi Sawada) wrote in #note-5:
Includes a duplicate of #14788.
I just want to note that this is proposal is for a more powerful feature than what's proposed in #14788, because both the key and value would be available to the block.
Eregon (Benoit Daloze) wrote in #note-4:
In general I think we avoid adding blocks to core methods, because indeed it's not clear if people expect #map, #select or #filter_map behavior, and it's so much clearer with the explicit call.
That's fair! Would calling these methods filter_keys
/filter_values
or select_keys
/select_values
be more explicit?