Project

General

Profile

Feature #16739

Allow Hash#keys and Hash#values to accept a block for filtering output

Added by jacobevelyn (Jacob Evelyn) 5 months ago. Updated 28 days ago.

Status:
Open
Priority:
Normal
Assignee:
-
Target version:
-
[ruby-core:97588]

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:

  1. Does not break any existing code (since Hash#keys and Hash#values do not currently accept blocks).
  2. Is very readable—it's obvious what it does at a glance.
  3. Is more efficient than current alternatives.
  4. Is more concise than current alternatives.
  5. 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) 5 months 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) 5 months 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) 5 months 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) 5 months 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) 4 months ago

Includes a duplicate of #14788.

Updated by jacobevelyn (Jacob Evelyn) 28 days 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?

Also available in: Atom PDF