【发布时间】:2018-02-14 03:01:02
【问题描述】:
我正在阅读 Greg Michaelson 的《通过 Lambda 演算进行函数式编程简介》一书来学习 lambda 演算。
我在 Clojure 中仅使用该语言的一个子集来实现示例。我只允许:
- 符号
- 单参数 lambda 函数
- 功能应用
- 为方便起见定义变量。
到目前为止,我的这些功能都在工作:
(def identity (fn [x] x))
(def self-application (fn [s] (s s)))
(def select-first (fn [first] (fn [second] first)))
(def select-second (fn [first] (fn [second] second)))
(def make-pair (fn [first] (fn [second] (fn [func] ((func first) second))))) ;; def make-pair = λfirst.λsecond.λfunc.((func first) second)
(def cond make-pair)
(def True select-first)
(def False select-second)
(def zero identity)
(def succ (fn [n-1] (fn [s] ((s False) n-1))))
(def one (succ zero))
(def zero? (fn [n] (n select-first)))
(def pred (fn [n] (((zero? n) zero) (n select-second))))
但现在我被递归函数困住了。更准确地说是add 的实现。书中提到的第一次尝试就是这个:
(def add-1
(fn [a]
(fn [b]
(((cond a) ((add-1 (succ a)) (pred b))) (zero? b)))))
((add zero) zero)
Lambda 演算规则强制将内部调用替换为包含定义本身的实际定义 add-1... 无休止。
在 Clojure 中,这是一种应用程序命令语言,add-1 在任何类型的执行之前也会被急切地评估,我们得到了 StackOverflowError。
经过一番摸索,本书提出了一个装置,用于避免对前面示例的无限替换。
(def add2 (fn [f]
(fn [a]
(fn [b]
(((zero? b) a) (((f f) (succ a)) (pred b)))))))
(def add (add2 add2))
add 的定义扩展为
(def add (fn [a]
(fn [b]
(((zero? b) a) (((add2 add2) (succ a)) (pred b))))))
在我们尝试之前完全没问题!这就是 Clojure 要做的事情(引用透明性):
((add zero) zero)
;; ~=>
(((zero? zero) zero) (((add2 add2) (succ zero)) (pred zero)))
;; ~=>
((select-first zero) (((add2 add2) (succ zero)) (pred zero)))
;; ~=>
((fn [second] zero) ((add (succ zero)) (pred zero)))
在最后一行 (fn [second] zero) 是一个 lambda,它在应用时需要一个参数。这里的论点是((add (succ zero)) (pred zero))。
Clojure 是一种“应用顺序”语言,因此在函数应用之前对参数进行评估,即使在这种情况下根本不会使用该参数。在这里,我们在add 中重复出现,这将在add... 中重复出现,直到堆栈爆炸。
在像 Haskell 这样的语言中,我认为这很好,因为它是惰性的(正常顺序),但我使用的是 Clojure。
在那之后,这本书详细介绍了避免样板的美味 Y 组合器,但我得出了同样可怕的结论。
编辑
正如@amalloy 建议的那样,我定义了 Z 组合器:
(def YC (fn [f] ((fn [x] (f (fn [z] ((x x) z)))) (fn [x] (f (fn [z] ((x x) z)))))))
我这样定义add2:
(def add2 (fn [f]
(fn [a]
(fn [b]
(((zero? b) a) ((f (succ a)) (pred b)))))))
我是这样使用它的:
(((YC add2) zero) zero)
但我仍然得到一个 StackOverflow。
我尝试“手动”扩展该功能,但经过 5 轮 beta 缩减后,它看起来就像在一片森林中无限扩展。
那么在没有宏的情况下,使 Clojure 成为“正常顺序”而不是“应用顺序”的诀窍是什么。甚至可能吗?它甚至可以解决我的问题吗?
这个问题非常接近这个问题:How to implement iteration of lambda calculus using scheme lisp?。除了我的是关于 Clojure 的,而不一定是关于 Y-Combinator 的。
【问题讨论】:
-
我明天起床可以帮你;如果你还需要它
-
您必须使用蹦床或使用
recur以避免堆栈溢出。 -
@JohannesKuhn :我会在接下来的几天里朝这个方向发展。
-
@naomik 如果您有解决方案...欢迎任何帮助,谢谢 :)
-
@DemeterPurjon 我刚醒来;今天感觉很不舒服;别担心,我正在为你写一篇文章
标签: recursion clojure lambda-calculus