Это обсуждалось в группе Google Clojure; см., например, ветку семантики карты от февраля этого года. Я позволю себе повторно использовать некоторые моменты, которые я сделал в своем сообщении в этой ветке ниже, добавляя несколько новых.
Прежде чем я продолжу объяснять, почему я думаю, что дизайн с «раздельной последовательностью» является правильным, я хотел бы отметить, что это естественное решение для ситуаций, когда вы действительно хотите получить вывод, аналогичный вводу, но не являясь явным. об этом существует в виде функции fmap
из библиотеки contrib algo.generic. (Однако я не думаю, что это хорошая идея использовать его по умолчанию, по тем же причинам, по которым дизайн базовой библиотеки является хорошим.)
Обзор
Ключевым наблюдением, на мой взгляд, является то, что операции последовательности, такие как map
, filter
и т. д., концептуально делятся на три отдельные задачи:
некоторый способ повторения их ввода;
применение функции к каждому элементу ввода;
производя вывод.
Очевидно, что 2. не вызывает проблем, если мы можем иметь дело с 1. и 3. Итак, давайте посмотрим на них.
Итерация
Для 1. учтите, что самый простой и наиболее производительный способ перебора коллекции обычно не включает выделение промежуточных результатов того же абстрактного типа, что и коллекция. Сопоставление функции с фрагментированной последовательностью с вектором, вероятно, будет намного более эффективным, чем сопоставление функции с последовательностью, создающей «векторы просмотра» (с использованием subvec
) для каждого вызова next
; последний, однако, является лучшим, что мы можем сделать с точки зрения производительности для next
на векторах в стиле Clojure (даже при наличии деревья RRB, которые хороши, когда нам нужна правильная операция подвектора/векторного среза для реализации интересного алгоритма, но делают обходы ужасающе медленными, если мы использовали их для реализации next
).
В Clojure специализированные типы seq поддерживают состояние обхода и дополнительные функции, такие как (1) стек узлов для отсортированных карт и наборов (помимо лучшей производительности, это имеет лучшую сложность большого O, чем обходы с использованием dissoc
/ disj
!), (2) текущий индекс + логика для упаковки листовых массивов в куски для векторов, (3) "продолжение" обхода для хэш-карт. Обход коллекции через такой объект просто быстрее, чем любая попытка прохождения через subvec
/ dissoc
/ disj
.
Предположим, однако, что мы готовы смириться с падением производительности при отображении функции на вектор. Что ж, давайте попробуем фильтровать сейчас:
(->> some-vector (map f) (filter p?))
Здесь есть проблема — нет хорошего способа удалить элементы из вектора. (Опять же, RRB-деревья могли бы помочь в теории, но на практике все нарезки и объединения RRB, связанные с созданием «реального вектора» для операций фильтрации, абсолютно снижают производительность.)
Вот аналогичная проблема. Рассмотрим этот конвейер:
(->> some-sorted-set (filter p?) (map f) (take n))
Здесь мы выиграем от лени (или, скорее, от возможности прекратить фильтрацию и сопоставление раньше времени; здесь есть пункт, связанный с редьюсерами, см. ниже). Очевидно, что take
можно заменить на map
, но не на filter
.
Дело в том, что если filter
можно неявно преобразовывать в seq, то это нормально и для map
; и аналогичные аргументы могут быть сделаны для других функций последовательности. Как только мы привели аргументы для всех — или почти всех — из них, становится ясно, что для seq
также имеет смысл возвращать специализированные seq
объекты.
Между прочим, фильтрация или сопоставление функции с коллекцией без создания в результате аналогичной коллекции очень полезна. Например, часто нас волнует только результат сведения последовательности, производимой конвейером преобразований, к некоторому значению или вызов функции для побочного эффекта на каждом элементе. Для этих сценариев нет ничего, что можно было бы выиграть, поддерживая тип ввода, и довольно много можно потерять в производительности.
Создание вывода
Как отмечалось выше, мы не всегда хотим создавать вывод того же типа, что и ввод. Однако, когда мы это делаем, часто лучший способ сделать это — это сделать что-то вроде заливки последовательностью входных данных в пустую выходную коллекцию.
На самом деле нет абсолютно никакого способа сделать лучше карты и наборы. Основная причина заключается в том, что для наборов мощности больше 1 невозможно предсказать мощность вывода отображения функции на набор, поскольку функция может «склеивать» (давать одинаковые результаты для) произвольные входы.
Кроме того, для отсортированных карт и наборов нет гарантии, что компаратор входного набора сможет работать с выходными данными произвольной функции.
Таким образом, если во многих случаях нет способа, скажем, map
значительно лучше, чем выполнять seq
и into
по отдельности, и учитывая, что и seq
, и into
сами по себе создают полезные примитивы, Clojure делает выбор в том, чтобы раскрыть полезные примитивы и позволяя пользователям создавать их. Это позволяет нам map
и into
создавать набор из набора, оставляя нам свободу не переходить к этапу into
, когда создание набора (или другого набора) не принесет никакой пользы. типа, в зависимости от обстоятельств).
Не все последовательно; или, рассмотрим редукторы
Некоторые проблемы с использованием самих типов коллекций при сопоставлении, фильтрации и т. д. не возникают при использовании редюсеров.
Ключевое различие между редьюсерами и последовательностями заключается в том, что промежуточные объекты, созданные clojure.core.reducers/map
и его друзьями, производят только объекты-дескрипторы, которые содержат информацию о том, какие вычисления необходимо выполнить в случае, если редюсер действительно будет сокращен. Таким образом, отдельные этапы вычислений могут быть объединены.
Это позволяет нам делать такие вещи, как
(require '[clojure.core.reducers :as r])
(->> some-set (r/map f) (r/filter p?) (into #{}))
Конечно, нам по-прежнему нужно явно указать наш (into #{})
, но это всего лишь способ сказать: «Конвейер редюсеров здесь заканчивается; пожалуйста, представьте результат в виде набора». Мы также можем запросить другой тип коллекции (возможно, вектор результатов; обратите внимание, что сопоставление f
с набором вполне может привести к дублированию результатов, и в некоторых ситуациях мы можем захотеть их сохранить) или скалярное значение ((reduce + 0)
).
Резюме
Основные моменты таковы:
самый быстрый способ перебора коллекции обычно не требует получения промежуточных результатов, аналогичных входным данным;
seq
использует самый быстрый способ итерации;
лучший подход к преобразованию набора путем сопоставления или фильтрации включает использование операции в стиле seq
, потому что мы хотим очень быстро выполнять итерацию при накоплении вывода;
таким образом seq
делает большой примитив;
map
и filter
в своем выборе для работы с последовательностями, в зависимости от сценария, могут избежать штрафов за производительность без положительных сторон, выиграть от лени и т. д., но все же могут использоваться для получения результата сбора с into
;
таким образом, они тоже являются великими примитивами.
Некоторые из этих пунктов могут не относиться к языку со статической типизацией, но, конечно, Clojure является динамическим. Кроме того, когда мы хотим, чтобы возвращаемое значение соответствовало типу ввода, мы просто вынуждены указывать это явно, и это само по себе может рассматриваться как хорошая вещь.
person
Michał Marczyk
schedule
21.04.2014
filter
не нужно было бы работать с последовательностью. Типа подтверждает мою точку зрения. - person VitoshKa   schedule 21.04.2014