在 Clojure 中获得意想不到的序列实现

Getting unexpected sequence realization in Clojure

在尝试对复杂计算进行故障排除时发现了一些奇怪的事情。我遇到了一个奇怪的错误,所以我开始逐步构建计算,并且很早就发现意外地实现了一个非常非常长的序列。我有一个这样的列表组合序列:

(0 1 2 3)
(0 1 2 4)
(0 1 2 5)

for n-choose-k for n = 143 and k = 4 sometimes unoriginally named "combos".我计划将其作为 seq 进行操作以保持合理的内存消耗,但这失败了:

(def combos (combinations 4 143))
(def semantically-also-combos
  (filter nil? (map identity combos)))

;; this prints instantly and uses almost no memory, as expected
(println (first combos)) ; prints (0 1 2 3)

;; this takes minutes and runs the JVM out of memory
;; without printing anything
(println (first semantically-also-combos))

根据 type,它们都是 clojure.lang.LazySeq,但一个按预期工作,另一个使进程崩溃。为什么整个seq通过identity函数实现只是为了运行它并检查它是否为nil?

完整的重现代码

(ns my-project.core
  (:gen-class))

;;; Copied from rosetta code
(defn combinations
  "If m=1, generate a nested list of numbers [0,n)
   If m>1, for each x in [0,n), and for each list in the recursion on [x+1,n), cons the two"
  [m n]
  (letfn [(comb-aux
            [m start]
            (if (= 1 m)
              (for [x (range start n)]
                (list x))
              (for [x (range start n)
                    xs (comb-aux (dec m) (inc x))]
                (cons x xs))))]
    (comb-aux m 0)))

(println "Generating combinations...")
(def combos (combinations 4 143))

(def should-also-be-combos
  (filter nil? (map identity combos)))

(defn -main
  "Calculates combos"
  [& _args]
  (println (type combos))
  (println (type should-also-be-combos)))

我对示例做了如下修改:

(ns tst.demo.core
  (:use tupelo.core tupelo.test))

(def cnt (atom 0))

; Copied from rosetta code
(defn combinations
  "If m=1, generate a nested list of numbers [0,n)
   If m>1, for each x in [0,n), and for each list in the recursion on [x+1,n), cons the two"
  [m n]
  (let [comb-aux (fn comb-aux
                   [m start]
                   (swap! cnt inc)
                   (when (zero? (mod @cnt 100))
                     (print \.)
                     (flush))
                   (when (zero? (mod @cnt 10000))
                     (newline)
                     (print @cnt "  "))
                   (if (= 1 m)
                     (for [x (range start n)]
                       (list x))
                     (for [x  (range start n)
                           xs (comb-aux (dec m) (inc x))]
                       (cons x xs))))]
    (comb-aux m 0)))

(println "Generating combinations...")
(def combos (combinations 4 143))

(def should-also-be-combos
  (filter nil? (map identity combos)))

和调用:

(dotest
  (newline)
  (spyx (type combos))
  (spyx (type should-also-be-combos))
  (newline)
  (println :1)
  (println (first combos))
  (newline)
  (println :2)
  (println (first should-also-be-combos))
  (newline))

结果:

-------------------------------
   Clojure 1.10.2    Java 15
-------------------------------

Testing tst.demo.core

(type combos)                  => clojure.lang.LazySeq
(type should-also-be-combos)   => clojure.lang.LazySeq

:1
(0 1 2 3)

:2
....................................................................................................
10000   ....................................................................................................
20000   ....................................................................................................
30000   ....................................................................................................
40000   ....................................................................................................
50000   ....................................................................................................
60000   ....................................................................................................
70000   ....................................................................................................
80000   ....................................................................................................
90000   ....................................................................................................
100000   ....................................................................................................
110000   ....................................................................................................
120000   ....................................................................................................
130000   ....................................................................................................
140000   ....................................................................................................
150000   ....................................................................................................
160000   ....................................................................................................
170000   ....................................................................................................
180000   ....................................................................................................
190000   ....................................................................................................
200000   ....................................................................................................
210000   ....................................................................................................
220000   ....................................................................................................
230000   ....................................................................................................
240000   ....................................................................................................
250000   ....................................................................................................
260000   ....................................................................................................
270000   ....................................................................................................
280000   ....................................................................................................
290000   ....................................................................................................
300000   ....................................................................................................
310000   ....................................................................................................
320000   ....................................................................................................
330000   ....................................................................................................
340000   ....................................................................................................
350000   ....................................................................................................
360000   ....................................................................................................
370000   ....................................................................................................
380000   ....................................................................................................
390000   ....................................................................................................
400000   ....................................................................................................
410000   ....................................................................................................
420000   ....................................................................................................
430000   ....................................................................................................
440000   ....................................................................................................
450000   ....................................................................................................
460000   ....................................................................................................
470000   ....................................................................................................
480000   ..........................................................................nil

在我的台式电脑上计时(5 岁,8 核,32Gb 内存):

Passed all tests
Finished at 16:37:28.484 (run time: 5.354s)

因此您可以看到该函数被调用了大约 1/2 百万次,在我的计算机上需要 5.3 秒。

我相信您看到的内容与 Clojure Don'ts: Concat. It's recursive form also reminds me of the famous Ackerman Function 中描述的类似,这是一个看似无害的函数如何“爆炸”的示例,即使对于较小的输入值也是如此。

您为什么不检查 (filter nil? (map identity combos)) 实际上 returns 传递给 combinations 的更小参数所期望的结果?我做到了。这就是 combinations returns 与参数 2 和 5:

(combinations 2 5)
;; => ((0 1) (0 2) (0 3) (0 4) (1 2) (1 3) (1 4) (2 3) (2 4) (3 4))

这是我们通过示例的额外惰性序列操作得到的结果:

(filter nil? (map identity (combinations 2 5)))
;; => ()

(filter nil? ...)所做的是保留所有满足谓词的元素。而输入序列中元素的none是nil?,所以它会扫描整个输入序列,会找到none。也许您想使用 (remove nil? ...),它将删除满足谓词的元素?

(remove nil? (map identity (combinations 2 5)))
;; => ((0 1) (0 2) (0 3) (0 4) (1 2) (1 3) (1 4) (2 3) (2 4) (3 4))

回到最初的例子,这就是我使用 remove:

(first (combinations 4 143))
;; => (0 1 2 3)

(first (remove nil? (map identity (combinations 4 143))))
;; => (0 1 2 3)

我的一般建议是先用“较小”的数据(例如 2 和 5)测试您的函数,然后再使用“较大”的数据(例如 4 和 143)。