Search code examples
rubyenumerable

Ruby Enumerable: first truthy value of a block


In ruby we can do something like:

stuff_in_trash.detect(&:eatable?)
=> :pack_of_peanuts

stuff_in_trash.detect(&:drinkable?)
=> nil

But what if we are interested in the value of the block the first time it is truthy, rather than the first item for which the block takes a truthy value?

That is convert the following code:

def try_to_make_artwork_from(enumerable)
  enumerable.each do |item|
    result = make_artwork_from item
    return result if result
 end
   nil
end

To something like:

def try_to_make_artwork_from(enumerable)
  enumerable.try_with { |item| make_artwork_from item }
end

What is desirable in the initial code is:

  1. It returns nil if the block never takes a truthy value;
  2. It returns the value of the block the first time it is truthy;
  3. It stops after finding the first match;
  4. It does not call make_artwork_from again (let us say it is not guaranteed to return the same result the next time you call it).

What is not so desirable, is that uses result three times, yet it has nothing to do with the story.

EDIT: Sorry, the initial implementation was incorrect, it needed to return nil in the case the block value was never true.

enumerable.lazy.map(&:block).detect(&:itself)

does the job, but is the simplest way? Is it efficient compare to simply using an each and caching the value?


Solution

  • it does the job, but is the simplest way? Is it efficient compare to simply using a each and caching the value?

    Simplest way?

    We can define this method :

    def first_truthy_block(enumerable, &block)
      enumerable.lazy.map(&block).find(&:itself)
    end
    

    Here in action :

    array = [0,1,2,3,4,:x5,'abc']
    
    puts first_truthy_block(array) { |x|
      if x ** 2 > 10 then
        "ARTWORK with #{x}!!!"
      end
    } 
    #=> ARTWORK with 4!!!
    

    Could it be simpler?

    • enumerable is needed, it's the object you're working on.
    • lazy is needed, it wouldn't stop as soon as possible, and would throw an exception with :x5**2.
    • map is needed, you need to apply some method to your element
    • find is needed to extract one value at most out of your enumerable

    With standard Enumerable methods, I don't see how it could be any simpler.

    Is it efficient?

    It is slower than your each method. It does basically the same thing and should have the same complexity but it does use more method calls and creates more objects :

    require 'fruity'
    
    def first_truthy_block_lazy(enumerable, &block)
      enumerable.lazy.map(&block).find(&:itself)
    end
    
    def first_truthy_block_each(enumerable, &block)
      enumerable.each do |item|
        result = block.call(item)
        return result if result
     end
       nil
    end
    
    big_array = Array.new(10_000){rand(4)} + [5] + Array.new(10_000){rand(20)} + [:x, :y, 'z']
    
    compare do
      _lazy_map do
        first_truthy_block_lazy(big_array) { |x|
          if x ** 2 > 10 then
            "ARTWORK with #{x}!!!"
          end
        }
      end
    
      _each do       
        first_truthy_block_each(big_array) { |x|
          if x ** 2 > 10 then
            "ARTWORK with #{x}!!!"
          end
        }
      end
    end
    

    fruity returns :

    Running each test once. Test will take about 1 second.
    _each is faster than _lazy_map by 3x ± 0.1