我将总结我解决这个问题的故事,使用旧的创建示例运行,并以一些观察和扩展建议作为结尾。
解决方案 0
起初,我改进/概括了我为解决旧问题所采取的方法。在这里,我编写了我自己的版本concatandmap主要是为了更好地展示,在 的情况下concat,为了增加一些懒惰。当然,我们也可以使用 Clojure 的版本或mapcat代替。
(defn fproduct [f]
(fn [s]
(lazy-seq
(if (and (seq f) (seq s))
(cons
((first f) (first s))
((fproduct (rest f)) (rest s)))))))
(defn concat' [s]
(lazy-seq
(if (seq s)
(if-let [x (seq (first s))]
(cons (first x) (concat' (cons (rest x) (rest s))))
(concat' (rest s))))))
(defn map' [f]
(fn rec [s]
(lazy-seq
(if (seq s)
(cons (f (first s)) (rec (rest s)))))))
(defn nunfoldr [f e]
(fn rec [n]
(fn [x]
(if (zero? n)
(if (= e x) (list ()) ())
((comp
concat'
(map' (comp
(partial apply map)
(fproduct (list
(partial partial cons)
(rec (dec n))))))
f)
x)))))
为了获得内在的懒惰,我们可以(partial partial cons)用类似的东西来代替(comp (partial partial concat) list)。尽管通过这种方式我们得到了 inner LazySeqs,但我们并没有获得任何有效的惰性,因为在consing 之前,已经发生了完全实现该rest部分所需的大部分计算,这在这种通用方法中似乎是不可避免的。基于更长的版本nfoldr,我们还可以定义如下更快的版本。
(defn nunfoldr [f e]
(fn [n]
(fn [x]
(if (zero? n)
(if (= e x) (list ()) ())
(((fn rec [n]
(fn [x] (println \< x \>)
(if (= 1 n)
(list (list x))
((comp
concat'
(map' (comp
(partial apply map)
(fproduct (list
(partial partial cons)
(rec (dec n))))))
f)
x))))
n)
x)))))
在这里,我println在主递归函数中添加了一个调用,以获得一些渴望的可视化。因此,让我们展示外在的懒惰和内在的渴望。
user=> (first ((npt 5) (range 3)))
< (0 1 2) >
< (0 1 2) >
< (0 1 2) >
< (0 1 2) >
< (0 1 2) >
(() () () () (0 1 2))
user=> (ffirst ((npt 5) (range 3)))
< (0 1 2) >
< (0 1 2) >
< (0 1 2) >
< (0 1 2) >
< (0 1 2) >
()
解决方案 1
然后我想到了一个更有前途的方法,使用该函数:
(defn transpose [s]
(lazy-seq
(if (every? seq s)
(cons
(map first s)
(transpose (map rest s))))))
为了获得新的解决方案,我们将map'调用中的前一个参数替换为:
(comp
(partial map (partial apply cons))
transpose
(fproduct (list
repeat
(rec (dec n)))))
试图获得我们可以替换(partial apply cons)的内在懒惰,#(cons (first %) (lazy-seq (second %)))但这还不够。问题在于(every? seq s)内部的测试transpose,其中检查延迟分解序列是否为空(作为停止条件)导致实现它。
解决方案 2
解决我想到的前一个问题的第一种方法是使用一些关于n元素的 -ary 因式分解数量的额外知识。这样我们可以repeat多次使用这个序列作为 的停止条件transpose。所以我们将里面的测试替换为transpose,添加(seq (first s))一个输入并将调用中的参数替换为:countnunfoldrmap'
(comp
(partial map #(cons (first %) (lazy-seq (second %))))
transpose
(fproduct (list
(partial apply repeat)
(rec (dec n))))
(fn [[x y]] (list (list ((count (dec n)) y) x) y)))
让我们转向分区问题并定义:
(defn npt-count [n]
(comp
(partial apply *)
#(map % (range 1 n))
(partial comp inc)
(partial partial /)
count))
(def npt (nunfoldr pt2 () npt-count))
现在我们可以证明外在和内在的懒惰。
user=> (first ((npt 5) (range 3)))
< (0 1 2) >
(< (0 1 2) >
() < (0 1 2) >
() < (0 1 2) >
() < (0 1 2) >
() (0 1 2))
user=> (ffirst ((npt 5) (range 3)))
< (0 1 2) >
()
然而,对额外知识的依赖和额外的计算成本使得这种解决方案无法接受。
解决方案 3
最后,我认为在一些关键的地方我应该使用一种“带有非惰性结束”的惰性序列,以便能够在不知不觉中检查空虚。一个这样的空序列只是一个非惰性空列表,总体而言,它们的行为有点像lazy-consClojure 早期的 s。使用下面给出的定义,我们可以得到一个可接受的解决方案,该解决方案是在以下假设下工作concat'的因式分解,我们使用的是较长版本的nunfoldr.
(def lazy? (partial instance? clojure.lang.IPending))
(defn empty-eager? [x] (and (not (lazy? x)) (empty? x)))
(defn transpose [s]
(lazy-seq
(if-not (some empty-eager? s)
(cons
(map first s)
(transpose (map rest s))))))
(defn concat' [s]
(if-not (empty-eager? s)
(lazy-seq
(if-let [x (seq (first s))]
(cons (first x) (concat' (cons (rest x) (rest s))))
(concat' (rest s))))
()))
(defn map' [f]
(fn rec [s]
(if-not (empty-eager? s)
(lazy-seq (cons (f (first s)) (rec (rest s))))
())))
请注意,在这种方法中,输入函数f应该生成新类型的惰性序列,并且生成的n-ary 分解器也将生成这样的序列。为了处理新的输入需求,对于分区问题,我们定义:
(defn pt2 [s]
(lazy-seq
(let [start (list () s)]
(cons
start
((fn rec [[x y]]
(if (seq y)
(lazy-seq
(let [step (list (concat x (list (first y))) (rest y))]
(cons step (rec step))))
()))
start)))))
再一次,让我们展示外在和内在的懒惰。
user=> (first ((npt 5) (range 3)))
< (0 1 2) >
< (0 1 2) >
(< (0 1 2) >
() < (0 1 2) >
() < (0 1 2) >
() () (0 1 2))
user=> (ffirst ((npt 5) (range 3)))
< (0 1 2) >
< (0 1 2) >
()
为了使输入和输出使用标准的惰性序列(牺牲一点惰性),我们可以添加:
(defn lazy-end->eager-end [s]
(if (seq s)
(lazy-seq (cons (first s) (lazy-end->eager-end (rest s))))
()))
(defn eager-end->lazy-end [s]
(lazy-seq
(if-not (empty-eager? s)
(cons (first s) (eager-end->lazy-end (rest s))))))
(def nunfoldr
(comp
(partial comp (partial comp eager-end->lazy-end))
(partial apply nunfoldr)
(fproduct (list
(partial comp lazy-end->eager-end)
identity))
list))
观察和扩展
在创建解决方案 3 时,我观察到 Clojure 中用于惰性序列的旧机制可能不一定不如当前机制。随着过渡,我们获得了创建惰性序列而无需进行任何大量计算的能力,但失去了检查空虚的能力,而无需进行获取更多元素所需的计算。因为这两种能力在某些情况下都很重要,如果引入一种新机制会很好,它可以结合以前的优点。这样的机制可以再次使用外部LazySeqthunk,当被强制时将返回一个空列表或一个Cons或另一个LazySeq或一个新的LazyConsthunk。这个新的 thunk 在强制时会返回 aCons或者可能是 another LazyCons。现在empty?只会强制LazySeqthunks whilefirst并且rest还会强制LazyCons. 在此设置中map可能如下所示:
(defn map [f s]
(lazy-seq
(if (empty? s) ()
(lazy-cons
(cons (f (first s)) (map f (rest s)))))))
我还注意到,从解决方案 1 开始采用的方法有助于进一步推广。如果在调用的参数内部,我们用、笛卡尔积的一些实现和另一个递归调用替换map'更长的时间,那么我们可以创建“在不同位置拆分”的版本。例如,使用以下作为参数,我们可以定义一个“在中间分裂”并对应于一个易于想象的函数。请注意,所有“拆分策略”在关联时都是等效的。nunfoldrconsconcattransposerepeatnunfoldmnfoldmf
(comp
(partial map (partial apply concat))
cproduct
(fproduct (let [n-half (quot n 2)]
(list (rec n-half) (rec (- n n-half))))))
另一种自然修改将允许无限分解。为了实现这一点,如果f生成了无限分解,nunfoldr(f , e)(n)应该以 ω 类型的顺序生成分解,以便它们中的每一个都可以在有限时间内生成。
其他可能的扩展包括删除n参数、创建关系折叠(与我们在这里考虑的关系展开相对应)和一般处理除序列以外的代数数据结构作为输入/输出。这本书,我刚刚发现,似乎包含有价值的相关信息,以分类/关系语言给出。
最后,为了能够更方便地进行这种编程,我们可以将其转换为无点代数设置。这将需要构建相当多的“额外机器”,实际上几乎是制造一种新语言。本文演示了这种语言。