How to iterate over an Enumerable, returning the first truthy result of a block ("map-find")

Updated . Posted . Visible to the public. Repeats.

Ruby has Enumerable.find(&block), which returns the first item in the collection for which the block evaluates to true.

first_post_with_image = posts.find do |post|
  post.image
end

However, sometimes it's not the item you're interested in, but some value depening on it – e.g. the value the block evaluated to. You could first map the collection and then take the first truthy value, but this way you need to process the whole collection twice:

first_image_url = posts.map(&:image).find(&:present?).url

If the mapping is a costly operation or has undesirable side effects, you need to do it in a single iteration instead.

Single iteration solution with break

As you know, #find loops through a collection, yielding the block with each item. Calling break with an argument will exit the whole block evaluation, returning whatever argument was given to break:

first_image_url = posts.find do |post|
  break post.image.url if post.image.present?
end

While the condition (post.image.present?) is false, the block returns nil and #find will try the next item in the collection. If the condition is never true, #find will return nil.

Single iteration solution with lazy

Ruby can iterate enumerables lazily, processing a chain for each entry instead of mapping a whole collection in multiple steps. With lazy, a solution would be:

first_image_url = posts.lazy.filter_map do |post|
  post.image.url if post.image.present?
end.first

Proposals for built-in Ruby method

There is no one-liner in Ruby or ActiveSupport with the behavior described above.

There have been discussions about adding a method like Enumerable#find_map:

Profile picture of Dominik Schöler
Dominik Schöler
Last edit
Dominik Schöler
License
Source code in this card is licensed under the MIT License.
Posted by Dominik Schöler to makandra dev (2015-09-04 08:29)