Mapcat ломает ленивость

У меня есть функция, которая создает ленивые последовательности, называемые a-функцией.

Если я запустил код:

(map a-function a-sequence-of-values) 

он возвращает ленивую последовательность, как ожидалось.

Но когда я запускаю код:

(mapcat a-function a-sequence-of-values) 

это ломает ленивость моей функции. На самом деле он превращает этот код в

(apply concat (map a-function a-sequence-of-values)) 

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

Мне нужна функция, которая связывает результат функции карты по требованию, не предварительно запустив всю карту.

Я могу взломать функцию для этого:

(defn my-mapcat
  [f coll]
  (lazy-seq
   (if (not-empty coll)
     (concat
      (f (first coll))
      (my-mapcat f (rest coll))))))

Но я не могу поверить, что clojure уже ничего не сделал. Знаете ли вы, имеет ли clojure такую ​​функцию? Лишь у нескольких человек и у меня такая же проблема?

Я также нашел блог, который занимается одной и той же проблемой: http://clojurian.blogspot.com.br/2012/11/beware-of-mapcat.html

Ответ 1

Производство и потребление ленивой последовательности отличается от ленивой оценки.

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

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

Ваша функция my-mapcat накладывает дополнительную лень на оценку своих аргументов, обертывая их в thunks (другие lazy-seqs). Это может быть полезно, когда ожидаются значительные побочные эффекты - IO, значительное потребление памяти, состояние обновлений. Тем не менее, предупреждающие колокола, вероятно, должны быть у вас в голове, если ваша функция выполняет побочные эффекты и создает последовательность, которая будет конкатенирована, что ваш код, вероятно, нуждается в рефакторинге.

Здесь похоже algo.monads

(defn- flatten*
  "Like #(apply concat %), but fully lazy: it evaluates each sublist
   only when it is needed."
  [ss]
  (lazy-seq
    (when-let [s (seq ss)]
      (concat (first s) (flatten* (rest s))))))

Другой способ написать my-mapcat:

(defn my-mapcat [f coll] (for [x coll, fx (f x)] fx))

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

Рассмотрим эту функцию для подсчета реализованной части последовательности

(defn count-realized [s] 
  (loop [s s, n 0] 
    (if (instance? clojure.lang.IPending s)
      (if (and (realized? s) (seq s))
        (recur (rest s) (inc n))
        n)
      (if (seq s)
        (recur (rest s) (inc n))
        n))))

Теперь посмотрим, что реализуется

(let [seq-of-seqs (map range (list 1 2 3 4 5 6))
      concat-seq (apply concat seq-of-seqs)]
  (println "seq-of-seqs: " (count-realized seq-of-seqs))
  (println "concat-seq: " (count-realized concat-seq))
  (println "seqs-in-seq: " (mapv count-realized seq-of-seqs)))          

 ;=> seq-of-seqs:  4
 ;   concat-seq:  0
 ;   seqs-in-seq:  [0 0 0 0 0 0]

Итак, реализовано 4 элемента seq-seqs, но ни одна из его компонентных последовательностей не реализована и не была реализована в конкатенированной последовательности.

Почему 4? Поскольку применимая перезагруженная версия concat имеет 4 аргумента [x y & xs] (подсчитывает &).

Сравните с

(let [seq-of-seqs (map range (list 1 2 3 4 5 6))
      foo-seq (apply (fn foo [& more] more) seq-of-seqs)]
  (println "seq-of-seqs: " (count-realized seq-of-seqs))
  (println "seqs-in-seq: " (mapv count-realized seq-of-seqs)))

;=> seq-of-seqs:  2
;   seqs-in-seq:  [0 0 0 0 0 0]

(let [seq-of-seqs (map range (list 1 2 3 4 5 6))
      foo-seq (apply (fn foo [a b c & more] more) seq-of-seqs)]
  (println "seq-of-seqs: " (count-realized seq-of-seqs))
  (println "seqs-in-seq: " (mapv count-realized seq-of-seqs)))

;=> seq-of-seqs:  5
;   seqs-in-seq:  [0 0 0 0 0 0]

Clojure имеет два решения для оценки ленивых аргументов.

Один из них - макросы. В отличие от функций макросы не оценивают свои аргументы.

Здесь функция с побочным эффектом

(defn f [n] (println "foo!") (repeat n n))

Побочные эффекты возникают, даже если последовательность не реализована

user=> (def x (concat (f 1) (f 2)))
foo!
foo!
#'user/x
user=> (count-realized x)
0

Clojure имеет макрос lazy-cat для предотвращения этого

user=> (def y (lazy-cat (f 1) (f 2)))
#'user/y
user=> (count-realized y)
0
user=> (dorun y)
foo!
foo!
nil
user=> (count-realized y)
3
user=> y
(1 2 2)

К сожалению, макрос не может apply.

Другим решением для задержки оценки является обертывание в thunks, что именно то, что вы сделали.

Ответ 2

Ваша предпосылка неверна. Concat - ленивый, применить ленивый, если его первый аргумент, а mapcat - ленивый.

user> (class (mapcat (fn [x y] (println x y) (list x y)) (range) (range)))
0 0
1 1
2 2
3 3
clojure.lang.LazySeq

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

В блоге, на котором вы ссылаетесь, речь идет об опасности рекурсивно использовать mapcat на ленивом дереве, потому что он жаждет первых элементов (которые могут складываться в рекурсивном приложении).