Всякий раз, когда вы видите картину:
- инициализировать аккумулятор (в вашем случае
output
)
- на каждой итерации какая-то коллекция модифицирует аккумулятор (в вашем случае прилагается к нему)
- вернуть аккумулятор
это fold
, или в Ruby terms a inject
.
Собственно, это немного тавтология. A fold
- универсальный метод итерации: все, которое может быть выражено путем итерации по элементам коллекции, также может быть представлено как fold
по коллекции. Другими словами: все методы на Enumerable
(включая each
!) Также можно определить в терминах inject
в качестве примитивного метода вместо each
.
Подумайте об этом так: коллекция может быть пустой или может быть текущий элемент. Нет третьего варианта, если вы покроете эти два случая, тогда вы все охватили. Ну, fold
принимает два аргумента: один, который сообщает ему, что делать, когда коллекция пуста, и тот, который сообщает ему, что делать с текущим элементом. Или добавьте еще один способ: вы можете увидеть коллекцию в виде серии инструкций, а fold
- это интерпретатор этих инструкций. Существует только два вида инструкций: инструкция END
и инструкция VALUE(el)
. И вы можете предоставить код интерпретатора для обеих этих инструкций для fold
.
В Ruby второй аргумент не является частью списка аргументов, это блок.
Итак, что это будет за fold
?
def to_s
@data.each_with_index.inject("[\n") do |acc, (row, i)|
acc + "Row #{i+1} : #{row.join(',')}\n"
end + ']'
end
Если вы хотите знать, может ли или нет each_with_index
заразить ваш код с некоторым нефункциональной примеси, будьте уверены, что вы можете так же легко избавиться от него, включая индекс в аккумуляторе:
def to_s
@data.inject(["[\n", 1]) do |(s, i), row|
[s + "Row #{i} : #{row.join(',')}\n", i+1]
end.first + ']'
end
отметить также, что в первом случае, сeach_with_index
, мы на самом деле не делать ничего «интересного» с аккумулятором, в отличие от второго случая, когда мы используем его, чтобы подсчет индекса , Фактически, первый случай на самом деле является ограниченной формой fold
, он не использует всю свою мощность. Это действительно просто map
:
def to_s
"[\n" + @data.map.with_index(1) do |row, i|
"Row #{i} : #{row.join(',')}\n"
end.join + ']'
end
По моему личному мнению, это было бы на самом деле совершенно нормально использовать (изменяемый) добавляемую строку здесь вместо конкатенации:
def to_s
"[\n" << @data.map.with_index(1) do |row, i|
"Row #{i} : #{row.join(',')}\n"
end.join << ']'
end
Это спасает нас от создания несколько ненужных строковых объектов, но что более важно: это более идиоматично. Реальная проблема: shared измененное состояние, но мы не делимся с нашей изменяемой строкой здесь: когда to_s
возвращает своего вызывающего абонента , получает доступ к строке, но to_s
сам вернулся и, следовательно, больше не имеет к нему доступа.
Если вы хотите, чтобы получить реальные фантазии, вы можете даже использовать интерполяцию строки:
def to_s
%Q<[\n#{@data.map.with_index(1) do |row, i|
"Row #{i} : #{row.join(',')}\n"
end.join}]>
end
К сожалению, это не только нарушает подсветку синтаксиса IRB, но и мой мозг ;-)
Хм, я серьезно подумывал предложить ваш последний пример ... Кстати, 'with_index' (из перечислителя) принимает параметр смещения. – steenslag
@steenslag: Не знал этого! Вы узнаете что-то новое каждый день, даже обучаясь. –
@ JörgWMittag Спасибо за подробный ответ :) Но я не понимаю, как мы можем знать порядок «параметров» между | .. | блока, когда мы связываем счетчики. – code4j