在Scheme中,如何使用lambda创建递归函数?

27

我正在学习Scheme课程,想尝试不使用define来编写递归函数。显然的问题是如果函数没有名称,你无法在函数内部调用它本身。

我找到了这个例子:使用lambda编写的阶乘生成器。

((lambda (x) (x x))
 (lambda (fact-gen)
   (lambda (n)
     (if (zero? n)
         1
         (* n ((fact-gen fact-gen) (sub1 n)))))))

但我甚至无法理解第一个调用 (lambda (x) (x x)) 的意义:它到底是干什么的?你在哪里输入要得到阶乘的值?

这不是为课堂准备的,只是出于好奇。

9个回答

17

(lambda (x) (x x)) 是一个函数,它在自身上调用一个参数 x

你贴出的整段代码结果是一个带有一个参数的函数。你可以像这样调用它:

(((lambda (x) (x x))
  (lambda (fact-gen)
    (lambda (n)
      (if (zero? n)
          1
          (* n ((fact-gen fact-gen) (sub1 n)))))))
 5)

那个调用它并返回120的参数是5。

高层次上最容易理解的方法是,第一个函数 (lambda (x) (x x)) 给了 x 一个指向自身的引用,因此现在 x 可以引用自己,从而进行递归。


11

表达式 (lambda (x) (x x)) 创建一个函数,当用一个参数(必须是函数)求值时,将该函数作为参数应用于自身。

你提供的表达式计算出一个函数,它接受一个数值参数并返回该参数的阶乘。尝试一下:

(let ((factorial ((lambda (x) (x x))
                  (lambda (fact-gen)
                    (lambda (n)
                      (if (zero? n)
                          1
                          (* n ((fact-gen fact-gen) (sub1 n)))))))))
  (display (factorial 5)))

在您的示例中有几个层,值得一步步进行并仔细检查每个层所做的事情。


6
基本上你拥有的是类似于Y组合器的表单。如果你重构阶乘特定代码,以便任何递归函数都可以实现,那么剩余的代码将是Y组合器。
我自己也经过这些步骤以获得更好的理解。
https://gist.github.com/z5h/238891
如果您不喜欢我写的内容,请搜索Y组合器(函数)。

5
你可以像这样定义它:

你可以像这样定义它:

(let ((fact #f)) 
  (set! fact 
        (lambda (n) (if (< n 2) 1 
                               (* n (fact (- n 1)))))) 
  (fact 5))

这就是letrec的真正工作原理,可以参考Christian Queinnec 的 LiSP


在你提问的例子中,自应用组合器被称为"U组合器"

(let ((U  (lambda (x) (x x)))
      (h  (lambda (g)
            (lambda (n)
              (if (zero? n)
                  1
                  (* n ((g g) (sub1 n))))))))
  ((U h) 5))

这里的微妙之处在于,由于 let 的作用域规则,lambda 表达式不能引用正在定义的名称。
当调用 ((U h) 5) 时,它被简化为 (h h) 5 应用程序,在 let 表单创建的环境框架内部。
现在,将 h 应用于 h 会创建一个新的环境框架,在该框架中,g 指向它上面的环境中的 h。
(let ((U  (lambda (x) (x x)))
      (h  (lambda (g)
            (lambda (n)
              (if (zero? n)
                  1
                  (* n ((g g) (sub1 n))))))))
  ( (let ((g h))
      (lambda (n)
              (if (zero? n)
                  1
                  (* n ((g g) (sub1 n)))))) 
    5))

在这里,(lambda (n) ...)表达式是从内部环境框架中返回的,该框架中g指向其上方的h - 作为一个闭包对象。也就是说,它是一个带有一个参数n的函数,同时还记住了ghU的绑定。

因此,当调用这个闭包时,n被赋值为5,然后进入if表单:

(let ((U  (lambda (x) (x x)))
      (h  (lambda (g)
            (lambda (n)
              (if (zero? n)
                  1
                  (* n ((g g) (sub1 n))))))))
    (let ((g h))
      (let ((n 5))
              (if (zero? n)
                  1
                  (* n ((g g) (sub1 n)))))))

应用程序 (g g) 会被简化为 (h h) 应用程序,因为 g 指向在环境框架中定义的 h,该环境框架位于创建闭包对象的环境之上。也就是说,在顶部的 let 表单中。但我们已经看到了 (h h) 调用的简化,它创建了闭包,即一个参数为 n 的函数,作为我们的 factorial 函数,下一次迭代将使用 4,然后是 3 等。 是否将使用新的闭包对象或重用同一闭包对象取决于编译器。 这可能会对性能产生影响,但不会影响递归的语义。

5

(lambda (x) (x x))接受一个函数对象,然后使用一个参数——函数对象本身来调用该对象。

然后,该对象由另一个函数调用,并将其放在参数名为fact-gen下。它返回一个lambda,以实际参数n作为输入。这就是((fact-gen fact-gen) (sub1 n))的工作原理。

如果您能够理解,建议阅读《The Little Schemer》的样例章节(第9章)。它讨论了如何构建这种类型的函数,最终将此模式提取到Y combinator中(可以用于提供一般的递归)。


4

我很喜欢这个问题。《Scheme编程语言》是一本好书。我的想法来自该书第二章。

首先,我们知道:

(letrec ((fact (lambda (n) (if (= n 1) 1 (* (fact (- n 1)) n))))) (fact 5))

使用letrec,我们可以使函数递归。当我们调用(fact 5)时,fact已经绑定到一个函数上了。如果我们有另一个函数,我们可以这样调用它:(another fact 5),现在another被称为二元函数(我的英语不好,抱歉)。我们可以这样定义another

(let ((another (lambda (f x) .... (f x) ...))) (another fact 5))

为什么我们不这样定义fact呢?
(let ((fact (lambda (f n) (if (= n 1) 1 (* n (f f (- n 1))))))) (fact fact 5))

如果fact是一个二元函数,那么可以使用一个函数f和一个整数n来调用它,此时函数f恰好为fact本身。
如果您已经理解了上述内容,现在可以写出Y组合器,将let替换为lambda

1
实际上,最后一个变量正是问题中的代码,uncurried。也就是说,OP代码是这里最后一个变量的柯里化版本,将一个二元函数转换为两个嵌套的单参数lambda,写成((f f) x)而不是(f f x) - Will Ness

3

用单个lambda是不可能的。但是使用两个或更多的lambda是可以的。因为所有其他解决方案都使用了三个lambda或let/letrec,我将使用两个lambda来说明方法:

((lambda (f x)
   (f f x))
 (lambda (self n)
   (if (= n 0)
       1
       (* n (self self (- n 1)))))
 5)

输出结果是120。

这里,

  1. (lambda (f x) (f f x)) 产生一个 lambda 函数,接受两个参数,第一个是lambda函数(称其为 f),第二个是参数(称其为 x)。请注意,在函数体中,它使用提供的 lambda fx 调用了自己。
  2. 现在,lambda 函数 f(来自第1点)即 self 是我们想要递归的。请注意,当递归调用 self 时,我们还将 self 作为第一个参数和 (- n 1) 作为第二个参数传递。

2

我想尝试写一段不使用define的递归函数。

当然,主要问题是如果函数没有名称,就无法在函数内部调用自身。

有点偏题了,但看到上面的语句,我只想告诉你“不使用define”并不意味着“没有名称”。在Scheme中,可以给某个东西命名,并且在不使用define的情况下进行递归调用。

(letrec
  ((fact
     (lambda (n)
       (if (zero? n)
         1
         (* n (fact (sub1 n)))))))
  (fact 5))

如果您的问题明确指出“匿名递归”,那么会更加清晰明了。

是的,但由于 let 本身就是 lambda 表达式的别名,因此能够为 lambda 表达式在其内部使用的局部名称赋值仍然具有意义。我相信这就是 OP 想要的,但我不知道在 Scheme 中如何实现。 - Hibou57
@Hibou57:虽然let很容易转换为lambda表达式应用,但是letrec稍微难一些。 - user102008
@Hibou57 (letrec ((name (lambda-expression))) (body)) = (let ((name (Y (lambda (name) (lambda-expression))))) (body)). 或者其他方式 -- 另一种方式是使用 set!(let ((name #f)) (set! name (lambda-expression)) (body)) - Will Ness

1
我发现这个问题是因为我需要在宏中使用递归辅助函数,而无法使用define。
人们想要理解(lambda (x) (x x))和Y组合器,但命名let能够完成任务而不会吓跑游客。
 ((lambda (n)
   (let sub ((i n) (z 1))
     (if (zero? i)
         z
         (sub (- i 1) (* z i)) )))
 5 )

如果像这样的代码足够使用,那么也可以推迟理解 (lambda (x) (x x)) 和 Y 组合器。Scheme、Haskell 和银河系一样,在其中心隐藏着一个巨大的黑洞。许多曾经富有成效的程序员被这些黑洞的数学之美所迷住,再也没有出现过。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接