2017-01-10 6 views
3

В рубина мы можем сделать что-то вроде:Руби Enumerable: первое truthy значение блока

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

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

Но что, если мы заинтересованы в значении блока в первый раз это truthy, а не первый элемент, для которого блок имеет истинное значение?

Это конвертировать следующий код:

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 (скажем, он не может возвратить тот же результат при следующем его вызове).

Что не так желательно, это то, что использует result три раза, но это не имеет ничего общего с историей.

EDIT: Извините, первоначальная реализация была неправильной, необходимо было вернуть nil в случае, когда значение блока никогда не было истинным.

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

выполняет эту работу, но является самым простым способом? Эффективно ли сравнение с использованием each и кэширование значения?

+1

Что вы имеете в виду под «значением блока ", вы имеете в виду аргумент, переданный блоку или возвращаемый блок? Может быть, мне что-то не хватает, но это не цель ['Enumerable # find'] (http://ruby-doc.org/core-2.4.0/Enumerable.html # method-i-find) – Anthony

+1

Возможно, это только я, но я не понимаю ваш вопрос. Не будет 'enumerable.detect {| item | make_artwork_from (item)} 'точно возвращает желаемый результат? – spickermann

+0

@spickermann OP хочет, чтобы метод возвращал значение 'make_artwork_from (item)'. Ваш код выше вернет значение 'item'. –

ответ

3

Он выполняет эту работу, но является самым простым способом? Эффективно ли это сравнение с , просто используя каждый и кешируя значение?

Простейший способ?

Мы можем определить этот метод:

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 

фруктовых возвращается:

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

Downvoter: конструктивная критика всегда приветствуется! –

0

Вы можете продлить перечислимых с желаемым итератора:

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 
+0

Вы * можете * расширять Enumerable, но исправление основных классов почти никогда не является ответом. – meagar

+0

@meagar Правда, исправление обезьян - это, как правило, плохая идея;) – fl00r

+0

Идентичность автора этого ответа стала очевидной, когда я читал «yield (i) и возвращал r' (в отличие от« yield (i) && return r'). :-) –

1

Вызов метода дважды

Глядя на ваш первый пример stuff_in_trash.detect(&:eatable?), вы могли бы сделать следующее:

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

Обратите внимание на использование safe navigation operator (&.), доступный как рубин v2.3+, который охватывает край случай detect возвращения nil.

Плюсы:

  • Вы не пробегаем по полной stuff_in_trash списке, как метод detect? останавливается на первом truthy пункта.

Минусы:

  • Вы называете метод eatable? дважды, на truthy объекта. (Возможная проблема с производительностью и, как правило, плохая практика.)
  • Код может стать уродливым/запутанным; особенно если метод, который вы применяете в блоке 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) 

Pros:

  • Вы не пробегаем по полной items список, так как ленивый перечислитель запрашивает «минимальное» оцепенение er элементов для вычисления конечного результата цепи метода.
  • Вы звоните только make_artwork_from(item)один раз на «правдивый» объект.

Минусы:

  • Это a bit more complicated как именно этот подход работает под капотом ...

Расширение 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 s если truthy значение найдено.
  • Вы звоните только 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) } 

Pros:

То же, что и выше.

Против:

  • Это не очень объектно-ориентированных; так что, возможно, не «рубиновый» способ справиться с этим. Ничто не мешает вам передать неперечислимый объект в detect_result, что может привести к ошибкам во время выполнения!
  • В других, статически типизированных языках (C++, Java, Rust, Scala, ...) вышеуказанный con был бы не вопросом.

Лично я считаю, используя ленивый нумератор самый элегантный, общее решение. Но я хотел включить некоторые альтернативы для сравнения.

+0

1. 'items.lazy.select {| item | make_artwork_from (item)}. first' не будет работать; 2. Вы можете заменить 'enumerable.each' на' enumerable.detect', чтобы избежать избыточного 'return' (я тоже поймал это). – fl00r

+0

и 3. В Ruby нет функций :) – fl00r

+0

1. Да, это так! !! Попробуй! 2. Да, правда. Хорошо, я отредактирую. 3. * squirm * ... Вид ... Сорт .... Иш ...: D Может быть, я должен был написать «функцию» в кавычках –

Смежные вопросы