Ruby Enumerable:块的第一个truthy值

时间:2017-01-10 14:20:55

标签: ruby enumerable

在红宝石中我们可以做类似的事情:

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

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

但是,如果我们第一次感兴趣的是块的价值,而不是块的真正价值的第一个项目呢?

即转换以下代码:

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

类似于:

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

初始代码中需要的是:

  1. 如果块从不采用真值,则返回nil;
  2. 第一次返回时,它会返回块的值;
  3. 在找到第一场比赛后停止;
  4. 它不会再次调用make_artwork_from(让我们说下次调用时不能保证返回相同的结果)。
  5. 不可取的是,使用result三次,但它与故事无关。

    编辑:抱歉,初始实现不正确,如果块值永远不为真,则需要返回nil

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

    完成这项工作,但这是最简单的方法吗?与仅使用each并缓存值相比,它是否有效?

5 个答案:

答案 0 :(得分:3)

  

它完成了这项工作,但这是最简单的方法吗?比较有效吗?   只需使用每个并缓存值?

最简单的方法?

我们可以定义这个方法:

128 x 15 x 128

这里有效:

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

可能更简单吗?

  • 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!!! 是必需的,它是您正在处理的对象。
  • enumerable是必需的,它不会尽快停止,并会lazy抛出异常。
  • 需要地图,您需要对元素应用某种方法
  • 需要查找从您的可枚举
  • 中提取最多一个值

使用标准:x5**2方法,我不知道它是如何变得更简单。

效率高吗?

它比Enumerable方法慢。它基本上做同样的事情,应该具有相同的复杂性,但它确实使用了更多的方法调用并创建了更多的对象:

each

果味回报:

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

答案 1 :(得分:1)

调用方法两次

查看stuff_in_trash.detect(&:eatable?)的第一个示例,您可以执行以下操作:

stuff_in_trash.detect(&:eatable?)&.eatable?

请注意safe navigation operator&.)的使用,自红宝石v2.3+开始提供,其中涵盖detect返回nil的边缘情况。< / p>

优点:

  • 您没有遍历完整的stuff_in_trash列表,因为detect?方法在第一个真实项目处停止。

缺点:

  • 你在真实对象上调用eatable?方法两次。 (可能的性能问题,通常是不好的做法。)
  • 代码可能变得丑陋/混乱;特别是如果您在detect块中应用的方法更复杂。例如:make_artwork_from( items.detect {|item| make_item_from(item)} - 这甚至没有涵盖detect还原nil的可能问题!

使用惰性枚举器

查看make_artwork_from(item)的第二个示例,您可以执行以下操作:

items.lazy.map {|item| make_artwork_from(item)}.detect(&:itself)

优点:

  • 您没有遍历完整的items列表,因为 lazy 枚举器会查询“最小”项数以计算最终方法链的结果。
  • 您只是在“真实”对象上调用make_artwork_from(item) 一次

缺点:

扩展Enumerable类

相当不言自明 - 你可以定义一个方法:

module Enumerable
  def detect_result
    self.detect do |i|
      if result = yield(i)
        return result
      end
    end
  end
end

# Usage:
items.detect_result { |item| make_artwork_from(item) }

优点:

  • 如果找到真值,则您没有循环遍历完整的items列表,因为扩展类方法过早return
  • 您只是在“真实”对象上调用make_artwork_from(item) 一次

缺点:

  • 全球扩展这样的核心课程通常是个坏主意!您可以考虑将其设为refinement,但这些并未广泛使用。

将其写为函数,而不是方法

...我的意思是,将Enumerable对象作为方法参数传递,而不是在对象上调用方法。与上面类似,但实现如下:

def detect_result(enumerable)
  enumerable.detect do |i|
    if result = yield(i)
      return result
    end
  end
end

# Usage:
detect_result(items) { |item| make_artwork_from(item) }

优点:

与上述相同。

缺点:

  • 这不是面向对象的;所以可以说这不是处理这个问题的“红宝石方式”。没有什么可以阻止您将不可枚举的对象传递给detect_result,这可能会导致运行时错误!
  • 在其他静态类型的语言(C++JavaRustScala,...)中,上述内容将不是问题。< / LI>

就我个人而言,我认为使用懒惰的枚举器是最优雅,通用的解决方案。但我想提供一些替代方案进行比较。

答案 2 :(得分:1)

array.inject(nil) {|c,v| c or m(v)}

答案 3 :(得分:0)

您可以使用所需的迭代器扩展Enumerable:

module Enumerable
  def detect_return
    self.detect do |i|
      r = yield(i) and return r
    end
  end
end

[1, 2, 3].detect_return do |i|
   if i + 1 >= 2
     puts "I will be printed only once"
     "ok, here I am"
   end
end
# I will be printed only once
# => "ok, here I am"

就我们都认为猴子修补是一件坏事而言,让我们提供更少的伤害变体:

def detect_return(enumerable)
  enumerable.detect do |i|
    r = yield(i) and return r
  end
end

detect_return([1, 2, 3]) do |i|
  if i + 1 >= 2
    puts "I will be printed only once"
    "ok, here I am"
  end
end
# I will be printed only once
# => "ok, here I am"

detect_return([1, 2, 3]) do |i|
  if i + 1 >= 42
    puts "I will be printed only once"
    "ok, here I am"
  end
end
# => nil

答案 4 :(得分:0)

(来自https://makandracards.com/makandra/35569-how-to-iterate-over-an-enumerable-returning-the-first-truthy-result-of-a-block-map-find

您可以简单地使用break

# "pseudo" code
my_enum.find do |item|
  calculated_result = begin
    # some stuf
  end

  break calculated_result if my_condition_is_true
end


# Or from the given link exemple 
first_image_url = posts.find do |post|
  break post.image.url if post.image.present?
end