Идиоматический способ повторения через коллекции в Clojure - PullRequest
12 голосов
/ 10 февраля 2012

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

Я мог бы написать следующее для подсчета элементов в плоской коллекции (игнорируя тот факт, что это не хвостовая рекурсия):

(defn length
  ([xs]
     (if (nil? (seq xs))
       0
       (+ 1 (length (rest xs))))))

Теперь в Scheme или CL все примеры делают это только над списками, поэтому идиоматический тест базового варианта в этих языках будет (nil? xs). В Clojure мы хотели бы, чтобы эта функция работала на всех типах коллекций, поэтому идиоматический тест (nil? (seq xs)) или, может быть, (empty? xs), или что-то совершенно другое?

Другой случай, который я хотел бы рассмотреть, - это обход дерева, то есть обход списка или вектора, представляющего дерево, например [1 2 [3 4].

Например, подсчет узлов в дереве:

(defn node-count [tree]
  (cond (not (coll? tree)) 1
        (nil? (seq tree)) 0
        :else (+ (node-count (first tree)) (node-count (rest tree)))))

Здесь мы используем (not (coll? tree)) для проверки на атомы, тогда как в Схеме / CL мы будем использовать atom?. Мы также используем (nil? (seq tree)) для проверки пустой коллекции. И, наконец, мы используем first и rest для деструктурирования текущего дерева до левой ветви и остальной части дерева.

Итак, подведем итог, следующие идиоматические формы в Clojure:

  • (nil? (seq xs)) для проверки пустой коллекции
  • (first xs) и (rest xs) копать в коллекцию
  • (not (coll? xs)) для проверки на атомы

Ответы [ 2 ]

10 голосов
/ 10 февраля 2012

Идиоматический тест для непустого seqable: (seq coll):

(if (seq coll)
  ...
  )

nil? не является необходимым, поскольку не nil возвращаемое значение из seq гарантированно будет последовательным и, следовательно, ни nil, ни false и, следовательно, истинно.

Если вы хотите сначала разобраться с делом nil, вы можете изменить if на if-not или seq на empty?; последний реализован в виде композиции seq с not (вот почему нет смысла писать (not (empty? xs)), см. строку документации empty?).

Что касается first / rest - полезно помнить о строгом варианте rest, next, использование которого более идиоматично, чем упаковка rest в seq.

Наконец, coll? проверяет, является ли его аргумент персистентной коллекцией Clojure (экземпляр clojure.lang.IPersistentCollection). Является ли это подходящей проверкой для «неатомов», зависит от того, должен ли код обрабатывать структуры данных Java как неатомы (посредством взаимодействия): например, (coll? (java.util.HashSet.)) равно false, как и (coll? (into-array [])), но вы можете позвонить seq на обоих. В новом модульном контенте есть функция с именем seqable? в core.incubator, которая обещает определить, будет ли (seq x) успешным для данного x.

8 голосов
/ 14 февраля 2012

Мне лично нравится следующий подход к повторению через коллекцию:

(defn length
  "Calculate the length of a collection or sequence"
  ([coll]
     (if-let [[x & xs] (seq coll)]
       (+ 1 (length xs))
       0)))

Особенности:

  • (seq coll) идиоматичен для проверки, является ли коллекция пустой (согласно великому ответу Михала)
  • if-let с (seq coll) автоматически обрабатывает как нулевой, так и пустой случай сбора
  • Вы можете использовать деструктурирование, чтобы называть первый и следующий элементы так, как вам нравится для использования в вашем теле функции

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

(defn length
  "Calculate the length of a collection or sequence"
  ([coll]
    (length coll 0))
  ([coll accumulator]
    (if-let [[x & xs] (seq coll)]
      (recur xs (inc accumulator))
      accumulator)))

(length (range 1000000))
=> 1000000
...