sunless sunless - 7 days ago 4
Ruby Question

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?

Answer

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