Feature #20899
openReconsider adding `Array#find_map`
Description
I would like to retry proposing method Array#find_map
that was rejected in 8421 which happened before introduction of filter_map
in 15323.
It would make code nicer whenever there is a need to get the first truthy result of applying some code.
Adapting examples from filter_map
documentation, but if I need only the first value:
(1..9).find_map {|i| i * 2 if i.even? } # => 4
{foo: 0, bar: 1, baz: 2}.find_map {|key, value| key if value.even? } # => :foo
Or an example of getting match group for first successful match:
list = ['some 123', 'list 234', 'of 345', 'strings 456']
list.find_map{ |s| s[/\Aof (\d+)\z/, 1] } # => "345"
Currently I imagine either more code and/or inefficiency (extra calls and/or objects):
# code called twice
list.find{ |s| s[/\Aof (\d+)\z/, 1] }&.then{ |s| s[/\Aof (\d+)\z/, 1] } # => "345"
# more logic
result = nil
list.each do |s|
break if (result = s[/\Aof (\d+)\z/, 1])
end
result # => "345"
# or
result = nil
list.find do |s|
result = s[/\Aof (\d+)\z/, 1]
end
result # => "345"
# extra calls for items which come after item that we were looking for
list.map{ |s| s[/\Aof (\d+)\z/, 1] }.find{ _1 } # => "345"
# using lazy
list.lazy.map{ |s| s[/\Aof (\d+)\z/, 1] }.find{ _1 } # => "345"
# or as suggested by @alexbarret in https://bugs.ruby-lang.org/issues/8421?tab=history#note-7
list.lazy.filter_map{ |s| s[/\Aof (\d+)\z/, 1] }.first # => "345"
# using tricks, as suggested by @zverok in https://bugs.ruby-lang.org/issues/8421?tab=history#note-4
list.find{ |s| result = s[/\Aof (\d+)\z/, 1] and break result } # => "345"
Implementation in ruby can be:
Enumerable.class_eval do
def find_map(&block)
each do |element|
block_result = block.call(element)
return block_result if block_result
end
nil
end
end
An example from another language - scala method collect
works alike filter_map
and collectFirst
would be like find_map
.
Updated by nobu (Nobuyoshi Nakada) 3 days ago
I use “break
from find
block” quite often, and admit such method will be useful.
As for this name, I’m not sure if it is appropriate, though.
Updated by toy (Ivan Kuchin) 2 days ago
nobu (Nobuyoshi Nakada) wrote in #note-1:
As for this name, I’m not sure if it is appropriate, though.
Few more ideas for the name:
# to not explicitly use word "map" if it feels confusing
find_mapped
find_transform
find_transformed
# to connect with `Object#then` method
find_then
find_and_then
# to explicitly connect to filter_map
filter_map_first
Updated by Dan0042 (Daniel DeLorme) 2 days ago
Another idea is filter_map(first: true)
Or filter_map(limit: N)
to return at most N elements (in this case 1).
The idea has floated up before that most Enumerable methods would benefit from a limit
keyword.
Updated by austin (Austin Ziegler) 2 days ago
Dan0042 (Daniel DeLorme) wrote in #note-3:
Another idea is
filter_map(first: true)
Orfilter_map(limit: N)
to return at most N elements (in this case 1).
The idea has floated up before that most Enumerable methods would benefit from alimit
keyword.
Most of the cases where enumerable methods would benefit from a limit
keyword would probably be better served by the use of #lazy
enumerables with either #take
or #first
.
I personally don't find the argument against the extra function calls convincing and feel that this would be hiding complexity that might be hard to profile. If this could be used to produce optimized instructions vs the lazy approach with YJIT or something, then maybe there's an argument for it.
Elixir deprecated Enum.filter_map/2
after beginning with it (if there's ever an Elixir 2, filter_map/2
will be removed; as it is now, it results in compile warnings.) It does have list comprehensions; I’m wondering if Ruby's pattern matching could be used in an efficient way here to emulate list comprehensions for cases like this.