如何使用Clojure语言的子集在lambda演算中实现递归函数?

时间:2017-09-05 18:36:18

标签: recursion clojure lambda-calculus

我正在学习lambda演算与书#34;通过Lambda演算的功能编程简介"作者:Greg Michaelson。

我仅使用该语言的一个子集在Clojure中实现示例。我只允许:

  • 符号
  • one-arg lambda functions
  • 功能应用
  • 为方便起见,
  • var定义。

到目前为止,我已经完成了这些功能:

(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-combinator,避免了样板,但我得出了同样令人毛骨悚然的结论。

修改

正如@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轮β减少后,看起来它在一片parens森林里无限扩张。

那么制作Clojure"正常顺序"的诀窍是什么?而不是"申请顺序"没有宏。它甚至可能吗?它甚至是我的问题的解决方案吗?

这个问题非常接近这个问题:How to implement iteration of lambda calculus using scheme lisp?。除了我的是关于Clojure而不一定是关于Y-Combinator。

2 个答案:

答案 0 :(得分:5)

对于严格的语言,您需要Z combinator而不是Y组合子。这是相同的基本想法,但将(x x)替换为(fn [v] (x x) v),以便自引用包含在lambda中,这意味着只有在需要时才会对其进行评估。

您还需要修改布尔值的定义,以使它们以严格的语言运行:您不能只传递它关注的两个值并在它们之间进行选择。相反,你传递thunks来计算你关心的两个值,并使用伪参数调用适当的函数。也就是说,就像你通过eta扩展递归调用来修复Y组合子一样,你通过eta扩展if和eta的两个分支来修复布尔值 - 减少布尔本身(我不是100%肯定eta-reducing这是正确的术语。)

(def add2 (fn [f]
            (fn [a]
              (fn [b]
                ((((zero? b) (fn [_] a)) (fn [_] ((f (succ a)) (pred b)))) b)))))

请注意,if的两个分支现在都包含(fn [_] ...),if本身包含(... b),其中b是我任意传入的值;你可以改为通过zero,或者任何东西。

答案 1 :(得分:5)

我看到的问题是你的Clojure程序和你的Lambda微积分程序之间的耦合太强了

  1. 你使用Clojure lambdas代表LC lambdas
  2. 您正在使用Clojure变量/定义来表示LC变量/定义
  3. 你正在使用Clojure的应用机制(Clojure的评估员)作为LC的应用机制
  4. 所以你实际编写一个受clojure编译器/评估者影响的clojure程序(不是LC程序) - 这意味着严格评估 - 恒定空间方向递归。我们来看看:

    (def add2 (fn [f]
                (fn [a]
                  (fn [b]
                    (((zero? b) a) ((f (succ a)) (pred b)))))))
    

    作为Clojure计划,在严格评估的环境中,我们称之为add2的每个时间,我们评估

    1. (zero? b)value1
    2. (value1 a)value2
    3. (succ a)value3
    4. (f value2)value4
    5. (pred b)value5
    6. (value2 value4)value6
    7. (value6 value5)
    8. 我们现在可以看到调用add2 始终会导致调用递归机制f - 当然程序永远不会终止并且我们会出现堆栈溢出!< / p>

      您有几个选择

      1. 根据@ amalloy的建议,使用thunk来延迟评估某些表达式,然后在你准备继续计算时强制(运行)它们 - 我不认为这将教你很多

      2. 您可以使用Clojure的loop / recurtrampoline进行常量空间递归,以实现您的YZ组合器 - 这是一个因为你只是希望支持单参数lambdas,所以在一个不优化尾调用的严格评估器中这样做会很棘手(可能是不可能的)

        我在JS中做了很多这样的工作,因为大多数JS机器都遇到同样的问题;如果您有兴趣查看自制程序,请查看:How do I replace while loops with a functional programming alternative without tail call optimization?

      3. 编写一个实际的评估者 - 这意味着你可以将你的Lambda Calculus程序的表示与Clojure和Clojure的编译器/评估器的数据类型和行为分离 - 你可以选择那些东西因为你是编写评估者的那个人而工作

        我从来没有在Clojure中做过这个练习,但我在JavaScript中已经完成了几次 - 学习经验是变革性的。就在上周,我写了https://repl.it/Kluo,它使用的是评估的正常顺序替换模型。这里的评估器对于大型LC程序来说不是堆栈安全的,但是你可以看到第113行的Curry Y支持递归 - 它支持LC程序中与底层JS机器支持相同的递归深度。这是另一个使用memoisation和更熟悉的环境模型的评估者:https://repl.it/DHAT/2 - 也继承了底层JS机器的递归限制

        在JavaScript中使递归堆栈安全非常困难,正如我在上面链接的那样,并且(有时)需要在代码中进行大量的转换才能使其成为堆栈安全的。我花了两个多月的时间进行了许多不眠之夜,以使其适应堆栈安全,正常顺序,按需调用的评估器:https://repl.it/DIfs/2 - 这就像Haskell或Racket的{{} 1}}

        至于在Clojure中这样做,JavaScript代码可以很容易地进行调整,但是我不知道Clojure能够向你展示明智的评估程序可能是什么样子 - 在书中,{ {3}}, (第4章),作者向您展示了如何使用Scheme本身为Scheme(一个Lisp)编写一个求值程序。当然,这比原始的Lambda微积分复杂10倍,所以如果你能编写一个Scheme评估器,你也可以写一个LC。这可能对您更有帮助,因为代码示例看起来更像Clojure

      4. 起点

        我为你研究了一点Clojure并想出了这个 - 它只是一个严格的评估者的开始,但它应该让你知道如何尽可能少地工作以获得非常接近的工作解决方案。

        请注意,我们在评估#lang lazy时会使用fn,但此详细信息不会透露给该程序的用户。对于'lambda也是如此 - 即,env只是一个实现细节,不应该是用户的关注。

        为了击败死马,您可以看到替代评估者和基于环境的评估者都得到了相同输入程序的等效答案 - 我不能强调这些选择是如何由你< / em> - 在SICP中,作者甚至继续改变评估者,使用一个简单的基于寄存器的模型来绑定变量和调用procs。可能性是无穷无尽的因为我们选择控制评估;在Clojure中编写所有内容(正如您最初所做的那样)并没有给我们带来那种灵活性

        env

        *或者它可能会为未绑定的标识符y引发错误;你的选择