Project

General

Profile

Actions

Feature #20899

open

Reconsider adding `Array#find_map`

Added by toy (Ivan Kuchin) 3 days ago. Updated 2 days ago.

Status:
Open
Assignee:
-
Target version:
-
[ruby-core:119944]

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)
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.

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.

Actions

Also available in: Atom PDF

Like0
Like0Like0Like0Like0