Итераторы высшего порядка в Ruby? - PullRequest
5 голосов
/ 17 августа 2010

Я читал вопрос Ruby об итераторе .each, и кто-то сказал, что использование .each может быть неприятным запахом кода, если итераторы более высокого порядка лучше подходят для этой задачи. Какие итераторы высшего порядка в Ruby?

edit: Jörg W Mittag, автор ответа StackOverflow, на который я ссылался, упомянул, что он намеревался писать более высокие итераторы level , но также объяснил намного ниже.

Ответы [ 5 ]

23 голосов
/ 18 августа 2010

К сожалению.Я имел в виду итераторы более высокого уровня уровня , а не высшего порядка . Каждый итератор, конечно, по определению является высшим порядком.

По сути, итерация является концепцией очень низкого уровня.Цель программирования - сообщить о намерениях другим заинтересованным лицам в команде.«Инициализация пустого массива, затем итерация по другому массиву и добавление текущего элемента этого массива в первый массив, если он делится на два без остатка» - это не намерение связи.«Выбор всех четных чисел» равен .

В общем, вы почти никогда не выполняете итерацию по коллекции только ради итерации.Вы либо хотите

  • каким-либо образом преобразовать каждый элемент (обычно это называется map, в Ruby и Smalltalk это collect, а в .NET и SQL - Select),
  • уменьшает всю коллекцию до некоторого единственного значения, например, вычисляя сумму или среднее значение или стандартное отклонение списка футбольных оценок (в теории категорий это называется катаморфизм , в функциональном программировании этоэто fold или reduce, в Smalltalk это inject:into:, в Ruby это inject и в .NET Aggregate),
  • отфильтровывает все элементы, которые удовлетворяют определенному условию (filterв большинстве функциональных языков select в Smalltalk и Ruby, find_all в Ruby, Where в .NET и SQL),
  • отфильтровывают все элементы, которые не удовлетворяютусловие (reject в Smalltalk и Ruby)
  • найти первый элемент, который удовлетворяет условию (find в Ruby)
  • считать элементы, которые удовлетворяют условию (count вRuby)
  • проверить, все ли элементы (all?),хотя бы один элемент (any?) или ни одного элемента (none?) удовлетворяет условию
  • группирует элементы в сегменты на основе некоторого дискриминатора (group_by в Ruby, .NET и SQL)
  • разбить коллекцию на две коллекции на основе некоторого предиката (partition)
  • отсортировать коллекцию (sort, sort_by)
  • объединить несколько коллекций в одну (zip)
  • и так далее, и так далее ...

Почти никогда - ваша цель просто перебрать коллекцию.

В частностиreduce ака.inject ака.fold ака.inject:into: ака.Aggregate ака. катаморфизм ваш друг.Есть причина, почему у него такое причудливое звучание математического названия: оно чрезвычайно мощное.Фактически, большинство из того, что я упомянул выше, может быть реализовано в терминах reduce.

По сути, то, что делает reduce, это «сводит» всю коллекцию к одному значению, используя некоторыефункция.У вас есть какое-то значение аккумулятора, а затем вы берете значение аккумулятора и первый элемент и вводите его в функцию.Результатом этой функции становится новый аккумулятор, который вы соединяете со вторым элементом и передаете в функцию и т. Д.

Наиболее очевидным примером этого является суммирование списка чисел:

[4, 8, 15, 16, 23, 42].reduce(0) {|acc, elem|
  acc + elem
}

Итак, аккумулятор начинается как 0, и мы передаем первый элемент 4 в функцию +.Результат - 4, который становится новым аккумулятором.Теперь мы передаем следующий элемент 8 и в результате получаем 12.И это продолжается до последнего элемента, и в результате они все время были мертвы.Нет, подождите, результат равен 108.

Ruby фактически позволяет нам использовать несколько ярлыков: если тип элемента совпадает с типом аккумулятора, вы можете опустить аккумулятор, а Ruby простопередайте первый элемент в качестве первого значения для аккумулятора:

[4, 8, 15, 16, 23, 42].reduce {|acc, elem|
  acc + elem
}

Также мы можем использовать Symbol#to_proc здесь:

[4, 8, 15, 16, 23, 42].reduce(&:+)

И на самом деле, если вы передадите reduce aSymbol аргумент, который будет обрабатываться как имя функции, используемой для операции сокращения:

[4, 8, 15, 16, 23, 42].reduce(:+)

Однако суммирование - это не все, что reduce может сделать. На самом деле, я считаю этот пример немного опасным. Все, кому я это показал, сразу поняли: «Ааа, так что это , что такое reduce», но, к сожалению, некоторые также подумали, что сумма суммирования равна all reduce, и это определенно не тот случай. Фактически, reduce - это общий метод итерации , под которым я подразумеваю, что reduce может делать все, что может делать each. В частности, вы можете хранить произвольное состояние в аккумуляторе.

Например, я писал выше, что reduce уменьшает коллекцию до одного значения. Но, конечно, это «единственное значение» может быть сколь угодно сложным. Это может быть, например, сама коллекция. Или строка:

class Array
  def mystery_method(foo)
    drop(1).reduce("#{first}") {|s, el| s << foo.to_str << el.to_s }
  end
end

Это пример того, как далеко вы можете пойти, играя трюками с аккумулятором. Если вы попробуете это, вы, конечно, узнаете это как Array#join:

class Array
  def join(sep=$,)
    drop(1).reduce("#{first}") {|s, el| s << sep.to_str << el.to_s }
  end
end

Обратите внимание, что нигде в этом "цикле" я не должен следить за тем, нахожусь ли я на последнем или втором на последнем элементе. Также нет никаких условных в коде. Здесь нет потенциальных ошибок для забора. Если вы думаете о том, как реализовать это с помощью each, вам придется как-то отслеживать индекс и проверять, находитесь ли вы на последнем элементе, а затем иметь if, чтобы предотвратить испуская разделитель в конце.

Поскольку я писал выше, что вся итерация может быть сделана с помощью reduce, я с таким же успехом могу доказать это. Вот методы Ruby Enumerable, реализованные в терминах reduce вместо each, как обычно. (Обратите внимание, что я только начал и только достиг g .)

module Enumerable
  def all?
    reduce(true) {|res, el| res && yield(el) }
  end

  def any?
    reduce(false) {|res, el| res || yield(el) }
  end

  def collect
    reduce([]) {|res, el| res << yield(el) }
  end
  alias_method :map, :collect

  def count
    reduce(0) {|res, el| res + 1 if yield el }
  end

  def detect
    reduce(nil) {|res, el| if yield el then el end unless res }
  end
  alias_method :find, :detect

  def drop(n=1)
    reduce([]) {|res, el| res.tap {|res| res << el unless n -= 1 >= 0 }}
  end

  def drop_while
    reduce([]) {|res, el| res.tap {|res| res << el unless yield el }}
  end

  def each
    reduce(nil) {|_, el| yield el }
  end

  def each_with_index
    tap { reduce(-1) {|i, el| (i+1).tap {|i| yield el, i }}}
  end

  def find_all
    reduce([]) {|res, el| res.tap {|res| res << el if yield el }}
  end
  alias_method :select, :find_all

  def grep(pattern)
    reduce([]) {|res, el| res.tap {|res| res << yield(el) if pattern === el }}
  end

  def group_by
    reduce(Hash.new {|hsh, key| hsh[key] = [] }) {|res, el| res.tap {|res|
        res[yield el] = el
    }}
  end

  def include?(obj)
    reduce(false) {|res, el| break true if res || el == obj }
  end

  def reject
    reduce([]) {|res, el| res.tap {|res| res << el unless yield el }}
  end
end

[Примечание: для этого поста я сделал несколько упрощений. Например, согласно стандартному протоколу Ruby Enumerable, each должен возвращать self, поэтому вам придется добавить туда дополнительную строку; другие методы ведут себя немного по-разному, в зависимости от того, какого типа и сколько аргументов вы передаете и так далее. Я оставил их, потому что они отвлекают от того, что я пытаюсь сделать.]

5 голосов
/ 17 августа 2010

Они говорят о более специализированных методах, таких как map, filter или inject.Например, вместо этого:

even_numbers = []
numbers.each {|num| even_numbers << num if num.even?}

Вы должны сделать это:

even_numbers = numbers.select {|num| num.even?}

В нем говорится , что вы хотите сделать, но инкапсулирует все несущественные технические деталив методе select.(И, кстати, в Ruby 1.8.7 или более поздней версии вы можете просто написать even_numbers = numbers.select(&:even?), так что даже более кратко, если немного похоже на Perl.)

Обычно их не называют «итераторами высшего порядка».но тот, кто написал это, вероятно, просто имел небольшую путаницу.Это хороший принцип, независимо от используемой вами терминологии.

3 голосов
/ 17 августа 2010

Из обычного определения «высшего порядка» я бы сказал, что итератор высшего порядка - это итератор, который принимает итератор в качестве аргумента или возвращает итератор. Может быть, что-то вроде enum_for. Однако я не думаю, что это то, что имел в виду человек.

Я думаю, что человек имел в виду итераторы, такие как map или select, которые являются функциями высшего порядка, но не понимал, что each, конечно, также является функцией высшего порядка. Так что в основном это всего лишь случай терминологической путаницы.

Смысл плаката, по-видимому, заключался в том, что вы не должны использовать each в тех случаях, когда вместо него можно использовать map, select или inject. И чтобы подчеркнуть это, он использовал термин, который не имел смысла в этом контексте.

0 голосов
/ 05 октября 2012

Я только что написал блог, который очень относится к этому вопросу. Причина, по которой вы хотите использовать функции более высокого порядка, заключается в том, что это поднимает программиста до более высокого уровня абстракции, до такой степени, что проблема может быть выражена декларативно,проталкивая реализацию либо к стандартной библиотеке Ruby, либо к коду более низкого уровня.

http://www.railstutors.com/blog/declarative-thinking-with-higher-order-functions-and-blocks#.UG5x6fl26jJ

0 голосов
/ 27 января 2011

Я получил этот вопрос справедливо, поэтому я написал в блоге о наиболее часто используемых итераторах: выберите и отклоните. В посте есть примеры, где «каждый» используется неправильно и как исправить код, чтобы использовать «выбрать» или «отклонить». В любом случае, надеюсь, это поможет.

http://www.natontesting.com/2011/01/01/rubys-each-select-and-reject-methods/

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...