Call a Closure, RECURSIVEly?

如果你需要递归调用一个函数,你会怎么做?这还不简单,直接在函数体内调用自己不就行了。

# rust
fn factorial(n: i32) -> i32 {
    if i <= 1 {
        1
    } else {
        n * factorial(n - 1)
    }
}
# lisp
> (define (fact n)
    (cond ((<= n 1) 1)
          (else (* n (fact (- n 1))))))
> (fact 5)
120

但是如果我们不使用 define ,只用闭包来定义这个函数,这种做法是否会出现问题呢?

(lambda (n)
  (cond ((<= n 1) 1)
        (else (* n (??? (- n 1))))))

当需要递归调用时,我们惊喜地发现,我们无法找到这个函数的名字,也就无从调用。而 Lambda 演算和图灵机是等价的,实际上是可以找到一种方法递归调用的(当然,实际的逻辑关系正好相反,正是因为 Lambda 演算拥有递归的能力才能够图灵等价)。

回调函数?

我们很容易想到一个最简单的方法:既然我们在闭包内无法获得这个闭包的名字,那么我们就给这个闭包添加一个参数,接受一个函数。只要这个参数的实参是闭包本身,就可以用这个参数的名字替代闭包,做到递归调用的效果。我们先从 Lisp 入手。

> ((lambda (n callback)
     (cond ((<= n 1) 1)
           (else (* n (callback (- n 1) callback)))))
   5
   (lambda (n callback)
     (cond ((<= n 1) 1)
           (else (* n (callback (- n 1) callback))))))
120

可以看到,为了递归调用,我们添加了 callback 参数,最后将闭包自身作为 callback 参数进行调用。调用这个函数需要使用类似这样的形式:(fact n fact) 。如果需要不止一个参数(阶乘只需要一个 n ),我们可以将 callback 的参数顺序调整到第一个,同时进行柯里化,就能得到这样的形式: ((fn fn) args ...)

> (((lambda (fact)
     (lambda (n)
       (cond ((<= n 1) 1)
             (else (* n ((fact fact) (- n 1)))))))
   (lambda (fact)
     (lambda (n)
       (cond ((<= n 1) 1)
             (else (* n ((fact fact) (- n 1))))))))
   5)
120

对于这种形式 (fn fn) ,我们可以使用 ω 组合子来简化一下。

# 虽然这里用了 define , 但是可以当作是宏的定义
# omega = λf . f f
> (define omega
    (lambda (f)
      (f f)))
> ((omega (lambda (fact) (lambda (n)
                           (cond ((<= n 1) 1)
                                 (else (* n ((omega fact) (- n 1))))))))
   5)
120

ω 组合子的作用就是将闭包本身作为闭包的第一个参数。这样我们就能在闭包中调用到闭包自己了。

具体来说,如果我们拥有一个变量 w: w -> a ,它接受与自己类型相同的变量作为第一个参数,我们将其传入 ω 组合子,我们就可以通过 (w w) 得到一个 a ,同时,整个 ω 组合子表达式的结果也是 a 。得益于 ω 组合子,我们获得了给闭包起名字的能力(通过第一个参数)。

对于 Rust ,我们似乎也可以用同样的方法来解决这个问题。

let fact = |fact: ???, n: i32| {
    if n <= 1 {
        1
    } else {
        n * fact(fact, n - 1)
    }
}

这里需要解决一个问题,闭包的参数中, fact 的实际类型是什么? fact 的实际类型是 Self ,其中 Self: Fn(Self, i32) -> i32 。我们可以发现,这实际上一个递归类型,这个递归类型让这个闭包可以无限递归,但是 Rust 无法接受这样一个可以无限递归的类型。因此,我们需要把这个闭包用 Struct 包裹起来,使用它提供的 Self 类型。

struct Wrapper<'a, I, O> {
    pub closure: &'a dyn Fn(&Self, I) -> O,
}

impl<'a, I, O> Wrapper<'a, I, O> {
    pub fn new(closure: &'a dyn Fn(&Self, I) -> O) -> Self {
        Self { closure }
    }

    pub fn call(&self, i: I) -> O {
        (self.closure)(self, i)
    }
}

fn main() {
    let fact = Wrapper::new(&|fact: &Wrapper<i32, i32>, n| {
        if n <= 1 {
            1
        } else {
            n * fact.call(n - 1)
        }
    });

    println!("fact(5) = {}", fact.call(5)); // fact(5) = 120
}

这里的 Wrapper::call 其实就是 ω 组合子,我们就不需要写 fact.closure(&fact, n) 了。

更简洁的形式

虽然我们已经实现了在闭包中递归调用,不过,每次调用都需要使用 ω 组合子或者把函数名写两遍,显得很累赘,形式不够优美(对于 Rust ,我们可以使用 Fn traits 来简化书写方式,以下不讨论)。为了简洁,我们需要将找到一个新的组合子,满足下面的条件:

((new-combinator f') n) = ((omega f) n)
其中
f: (f -> a -> b) -> a -> b
f': f'' -> a -> b
f'': a -> b

这个新的组合子与 ω 组合子的不同点在于, f'' 不再需要一个函数作为参数,我们可以直接将 a 作为它的参数来调用,(f'' a) = b

我们先写出这个新组合子的参数部分:

(lambda (f)
  (wrap f))

我们现在还不能知道如何实现 wrap 。但可以确定的一点是 (wrap ...) 的结果是一个 a -> b ,我们可以从 new-combinator 的签名得知。既然 (wrap ...) 的结果可以直接作为 f'' ,我们不妨对这一点加以利用,使用 ω 组合子,让这个结果在 wrap 内部可用。

(lambda (f)
  (omega (lambda (g)
    (f (g g)))))

基于 ω 组合子的性质,我们可以通过 (g g) 获取这个闭包的结果( g 实际上只有一个参数),也就是我们需要的 f''。不过,现在的组合子我们不能直接用在部分代码中——对于严格求值的语言,在调用函数前,需要先计算出 (g g) 的值,这是一个无限递归的过程。我们需要一个延迟计算的方式。在 Racket 中有两种方法:

对于第二种方式,实现如下。

(define Y
  (λ (f)
    (ω (λ (g)
         (f (λ (x) ((g g) x)))))))

这里用一个 a -> b 闭包包裹住了 (g g) ,这样,只有在传递一个 a 时才会触发 (g g) 的求值。这就是大名鼎鼎的 Y 组合子,它赋予了 Lambda 演算递归的能力,是一个非常强大的组合子。某些 Lisp 方言利用它实现了函数定义,比如这门 Heresy 方言

我们可以试试 Y 组合子的威力。

> (define ω
    (λ (f)
      (f f)))
> (define Y
    (λ (target)
      (ω (λ (f)
           (target (λ (x) ((ω f) x)))))))
> (define factorial
    (Y (lambda (factorial)
         (lambda (n)
           (cond ((<= n 1) 1)
                 (else (* n (factorial (- n 1)))))))))
> (factorial 5)
120 # TADAA!

Y 组合子的形式定义如下:

ω = λf . f f
Y = λ f . ω (λ g . f (ω g))
  = λ f . (λ g . f (g g)) (λ g . f (g g))