Каждая коллекция в clojure называется «секверируемой», но на самом деле секвенсами являются только список и минусы:

user> (seq? {:a 1 :b 2})
false
user> (seq? [1 2 3])
false    

Все остальные функции seq сначала преобразуют коллекцию в последовательность, а только затем работают с ней.

user> (class (rest {:a 1 :b 2}))
clojure.lang.PersistentArrayMap$Seq

Я не умею:

user> (:b (rest {:a 1 :b 2}))
nil
user> (:b (filter #(-> % val (= 1)) {:a 1 :b 1 :c 2}))
nil

И должны вернуться к конкретному типу данных. Мне это кажется плохим дизайном, но, скорее всего, я его еще не понял.

Итак, почему коллекции clojure не реализуют интерфейс ISeq напрямую, а все функции seq не возвращают объект того же класса, что и входной объект?

20
VitoshKa 21 Апр 2014 в 04:15

4 ответа

Лучший ответ

Это обсуждалось в группе Google Clojure; см., например, ветку семантику карты от февраля этого года. Я возьму на себя смелость повторно использовать некоторые моменты, которые я высказал в своем сообщении в этой ветке ниже, добавляя несколько новых.

Прежде чем я продолжу объяснять, почему я считаю, что дизайн «отдельной последовательности» является правильным, я хотел бы указать, что это естественное решение для ситуаций, когда вы действительно хотите, чтобы результат был аналогичен входному, но не был явным. об этом существует в виде функции fmap из библиотеки contrib algo.generic . (Я не думаю, что использовать его по умолчанию, однако по тем же причинам, по которым дизайн основной библиотеки хорош.)

Обзор

Я считаю, что ключевое наблюдение состоит в том, что операции с последовательностью, такие как map, filter и т. Д., Концептуально разделяются на три отдельных аспекта:

  1. какой-то способ перебора их ввода;

  2. применение функции к каждому элементу ввода;

  3. производя выход.

Ясно, что 2. не составит проблем, если мы сможем разобраться с 1. и 3. Итак, давайте посмотрим на них.

Итерация

Для 1. учтите, что самый простой и наиболее производительный способ итерации по коллекции обычно не включает в себя выделение промежуточных результатов того же абстрактного типа, что и коллекция. Отображение функции на фрагментированную последовательность по вектору, вероятно, будет намного более производительным, чем отображение функции на последовательность, производящую «векторы представления» (с использованием subvec) для каждого вызова next; последний, однако, является лучшим, что мы можем сделать с точки зрения производительности для next на векторах в стиле Clojure (даже при наличии RRB-деревья, которые отлично подходят, когда нам нужна правильная операция субвектора / векторного среза для реализации интересного алгоритма, но делают обходы ужасно медленными, если мы использовали их для реализации next ).

В Clojure специализированные типы seq поддерживают состояние обхода и дополнительные функции, такие как (1) стек узлов для отсортированных карт и наборов (помимо лучшей производительности, это имеет лучшую сложность big-O, чем обходы с использованием disoc / disj !), (2) текущий индекс + логика для упаковки листовых массивов в блоки для векторов, (3) «продолжение» обхода для хэш-карт. Прохождение коллекции через такой объект просто быстрее, чем любая попытка прохождения через subvec / disoc / 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)).

Резюме

Основные моменты это:

  1. самый быстрый способ перебора коллекции обычно не приводит к получению промежуточных результатов, подобных входным;

  2. seq использует самый быстрый способ итерации;

  3. лучший подход к преобразованию набора путем сопоставления или фильтрации включает использование операции в стиле seq, потому что мы хотим выполнять итерацию очень быстро при накоплении вывода;

  4. таким образом, seq создает отличный примитив;

  5. map и filter, по своему выбору для работы с последовательностями, в зависимости от сценария могут избежать снижения производительности без преимуществ, выгоды от лени и т. Д., Но все же могут использоваться для получения результата сбора с помощью into;

  6. таким образом, они тоже создают великих примитивов.

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

17
Michał Marczyk 21 Апр 2014 в 07:30

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

Функции последовательности (карта, фильтр и т. Д.) Берут «seqable» вещь (что-то, что может создавать последовательность), вызывают на ней seq для создания последовательности, а затем работают с этой последовательностью, возвращая новую последовательность. Вам решать, нужно ли вам это и как собрать эту последовательность обратно в конкретную коллекцию. В то время как векторы и списки упорядочены, наборы и карты - нет, и, следовательно, последовательности этих структур данных должны вычислять и сохранять порядок вне коллекции.

Специализированные функции, такие как mapv, filterv, reduce-kv, позволяют вам оставаться «в коллекции», когда вы знаете, что хотите, чтобы операция возвращала коллекцию в конце, а не последовательность.

9
Alex Miller 21 Апр 2014 в 02:42

Seqs - это упорядоченные структуры, тогда как карты и наборы неупорядочены. Две карты равного значения могут иметь различный внутренний порядок. Например:

user=> (seq (array-map :a 1 :b 2))
([:a 1] [:b 2])
user=> (seq (array-map :b 2 :a 1))
([:b 2] [:a 1])

Нет смысла запрашивать rest карты, потому что это не последовательная структура. То же самое и с набором.

Так что насчет векторов? Они упорядочены последовательно, поэтому мы потенциально можем сопоставить вектор, и действительно существует такая функция: mapv.

Вы вполне можете спросить: почему это не подразумевается? Если я передаю вектор в map, почему он не возвращает вектор?

Ну, во-первых, это означало бы сделать исключение для упорядоченных структур, таких как векторы, а Clojure не умеет делать исключения.

Но что еще более важно, вы потеряете одно из самых полезных свойств seqs: лень. Объединение в цепочку функций seq, таких как map и filter, является очень распространенной операцией, и без лени это было бы гораздо менее производительно и гораздо более интенсивно потребляющим память.

2
weavejester 21 Апр 2014 в 02:33

Классы коллекции следуют шаблону фабрики, т.е. вместо реализации ISeq они реализуют Sequable, т.е. вы можете создать ISeq из коллекции, но сама коллекция не является ISeq.

Теперь, даже если бы эти коллекции реализовали ISeq напрямую, я не уверен, как это решило бы вашу проблему наличия функций последовательности общего назначения, которые возвращали бы исходный объект, поскольку это вообще не имело бы смысла, поскольку эти функции общего назначения предполагаются чтобы работать над ISeq, они понятия не имеют, какой объект дал им это ISeq

Пример на java:

interface ISeq {
    ....
}

class A implements ISeq {

}

class B implements ISeq {

}

static class Helpers {
    /*
        Filter can only work with ISeq, that's what makes it general purpose.
        There is no way it could return A or B objects.
    */
    public static ISeq filter(ISeq coll, ...) { } 
    ...
}
0
Ankur 21 Апр 2014 в 04:49