Ruby idiom to shortcircuit to return first non-nil using each and map
Asked Answered
W

4

13

This is the essence of what I'm trying to do but the 'break' doesn't feed right:

needle = nil
haystacks.each do |haystack|
  needle = haystack.look_for_needle()
  break if needle
end

This is shorter, but it will iterate over every haystack (without looking at least) even though it doesn't need to:

needle = nil
haystacks.each do |haystack|
  needle ||= haystack.look_for_needle()
end

This is a one-liner but (I believe) it is not lazy and so it does unnecessary work:

needle = hackstacks.map{|h| h.look_for_needle()}.detect{|x| !x.nil?}

I feel like there should be a one liner, but I'm not sure it would be:

needle = hackstacks.some_find_each_first_detect_lazy_map_thingee {|h| h.look_for_needle()}
Whimsical answered 29/8, 2016 at 20:7 Comment(3)
I think the answers given so far ignore that you have nested lists, right? I think you could improve the question by giving one or two small examples.Underpinnings
have you tried haystacks.detect(&:look_for_needle).look_for_needle. detect will return the first haystack with a needle and the secondary call to look_for_needle will return the Needle (assumption). Not sure how intensive look_for_needle is though.Relique
Felix - The problem doesn't really have nested lists. It has a list of objects where an expensive operation is performed on each item. engineersmnky - Yes, looking for needle in a haystack is an intensive operation.Whimsical
N
12

With Ruby 2.x lazy enumerators:

needle = haystacks.lazy.map(&:look_for_needle).reject(&:nil?).first

Or:

needle = haystacks.lazy.map(&:look_for_needle).detect(&:itself)

Or (@DavidMoles):

needle = haystacks.lazy.filter_map(&:look_for_needle).first
Nadabb answered 29/8, 2016 at 20:38 Comment(3)
I haven't verified it yet, but my gut says that this is what I was looking for.Whimsical
Thumbs up for a lesson in lazy enumeratorsDelastre
As of 2.7 you can use filter_map to skip the detect/reject step: haystacks.lazy.filter_map(&:look_for_needle).firstAugmenter
D
1
haystack.find &:itself
haystack.index &:itself

Which one do you prefer?

Delastre answered 29/8, 2016 at 20:10 Comment(6)
he is not looking for nil but rather the first instance where look_for_needle is not nil.Relique
Oops, my mistake. Corrected. The OP confused me with that needless initialization of needle.Delastre
To clarify, I don't want to return the first haystack that has a needle, I want to return the first needle that I find.Whimsical
Then #find is your method of choice.Delastre
Nice except that this will return a haystack and not a needle. Also might want to caveat itself as this was not an Object method until 2.2Relique
People should move on from Ruby 2.1.7 already :-) Method #find returns needle, not the haystack, but now I realize that the OP has multiple haystacks to search. Fortunately he knows how to iterater over them.Delastre
A
1

I assume that both find_proverbial_needle_in_a_haystack and look_for_needle return the needle or nil, the latter if no haystack contains the needle.

class Haystack
  def initialize(haystack)
    @haystack = haystack
  end

  # Suppose look_for_needle is defined as follows
  def look_for_needle
    @haystack.include?(:straw) && :straw
  end 
end

def find_proverbial_needle_in_a_haystack(haystacks)
  needle = nil # can be anything
  haystacks.find { |haystack| needle = haystack.look_for_needle } && needle
end

find returns the first haystack for which the block evaluates true, or nil if no needle is found in any haystack.

haystacks = [Haystack.new([:ball, :top]),
             Haystack.new([:fluff, :straw]),
             Haystack.new([:dog, :cat])]
  #=> [#<Haystack:0x007fdaaa0f6860 @haystack=[:ball, :top]>,
  #    #<Haystack:0x007fdaaa0f67e8 @haystack=[:fluff, :straw]>,
  #    #<Haystack:0x007fdaaa0f6590 @haystack=[:dog, :cat]>] 
find_proverbial_needle_in_a_haystack(haystacks)
  #=> :straw 

haystacks = [Haystack.new([:ball, :top]),
             Haystack.new([:fluff, :yellow_stuff]),
             Haystack.new([:dog, :cat])]
  #=> [#<Haystack:0x007fdaaa082f50 @haystack=[:ball, :top]>,
  #    #<Haystack:0x007fdaaa082f00 @haystack=[:fluff, :yellow_stuff]>,
  #    #<Haystack:0x007fdaaa082eb0 @haystack=[:dog, :cat]>]     
find_proverbial_needle_in_a_haystack(haystacks)
  #=> nil
Aculeus answered 29/8, 2016 at 21:24 Comment(0)
B
1

For Ruby 2.0 and above, I extended Enumerable with this method:

module Enumerable
  TRUTHY_LAMBDA = lambda {|result| !!result }
  def first_result(detect_result_lambda=TRUTHY_LAMBDA, &block)
    self.lazy.collect(&block).detect {|result| detect_result_lambda.call(result) }
  end
end

With that you could call:

haystacks.first_result {|haystack| haystack.find_needle() }

..and it will return the needle.

If you needed to make sure it was a quilting needle, you could do this:

haystacks.first_result(lambda {|needle| needle&.for_quilting? }) {|haystack| haystack.find_needle() }

or the same code formatted with a do block:

haystacks.first_result(lambda {|needle| needle&.for_quilting? }) do |haystack|
  haystack.find_needle()
end

P.S. - if anyone can tell me how to improve this code to support the following syntax, that would be amazing (I get the error "SyntaxError: both block arg and actual block given" which suggests to me that Ruby is assigning "&:for_quilting?" to the &block rather than the first argument)

haystacks.first_result(&:for_quilting?) {|haystack| haystack.find_needle() }
Biblio answered 4/9, 2020 at 20:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.