Search code examples
rubyidioms

Is there an elegant way to iterate through an array and stop before the end?


I've want to write code that does this (well, a much more complex version of this, anyway):

answer = nil
array.each do |e|
  x = complex_stuff_with(e)
  if x.is_the_one_i_want?
    answer = x
    break
  end
end

This is obviously not satisfactory. Iterating through an array like this is a code smell.

Ideally, I want some method on Array that will let me test each element in turn, and return my answer and stop when I have one. Is there such a method?

The closest I've been able to come to it is by using break inside #inject. But I'm still basically doing the iteration by hand!

answer = array.inject do |_,e|
  x = complex_stuff_with(e)
  break x if x.is_the_one_i_want?
end

Update: It would appear that the reason I can't find such a method is because no such method exists.

Update: Spoke too soon! I need the Lazy Operator! Thanks, everyone.


Solution

  • What you are effectively doing is, first mapping the elements of the Array with a transformation, and then finding the first transformed element that satisfies some predicate.

    We can express this like this (I'm re-using the definitions from @iGian's answer, but with an added side-effect so that you can observe the evaluation of the transformation operation):

    def complex_stuff_with(e)
      p "#{__callee__}(#{e.inspect})"
      e**2
    end
    
    ary = [1, 2, 3, 2]
    x_i_want = 4
    
    ary
      .map(&method(:complex_stuff_with))
      .find(&x_i_want.method(:==))
    # "complex_stuff_with(1)"
    # "complex_stuff_with(2)"
    # "complex_stuff_with(3)"
    # "complex_stuff_with(2)"
    #=> 4
    

    This gives us the correct result but not the correct side-effects. You want the operation to be lazy. Well, that's easy, we just need to convert the Array to a lazy enumerator first:

    ary
      .lazy
      .map(&method(:complex_stuff_with))
      .find(&x_i_want.method(:==))
    # "complex_stuff_with(1)"
    # "complex_stuff_with(2)"
    #=> 4
    

    Or, using the definitions from your question:

    array
      .lazy
      .map(&method(:complex_stuff_with))
      .find(&:is_the_one_i_want?)