在完成SICP 4.21时接触到Y运算符,感觉很巧妙,这里加以总结,大部分内容来自于第二份讲义,这里只做了翻译以及总结的工作。

参考资料:

https://liujiacai.net/blog/2016/02/22/recursion-without-name/

https://mvanier.livejournal.com/2897.html

问题的引入

问题来自于sicp习题4.21,基本问题是,如何不使用递归来定义递归函数?答案是,使用Y运算符。后续会一步步引入Y运算符,为了叙述方便,这里以阶乘函数为例:

(define factorial
  (lambda (n)
    (if (= n 0)
        1
        (* n (factorial (- n 1))))))

简单的想法

为了消除递归定义,只要将函数内factorial替换即可:

(define sort-of-factorial
  (lambda (n)
    (if (= n 0)
        1
        (* n (<???> (- n 1))))))

那么???处应该也是个函数,我们将其设为f,作为参数传入:

(define almost-factorial
	(lambda (f)
		(lambda (n)
			(if (= n 0)
			1
			(* n (f (- n 1)))))))

注意almost-factorial并不是阶乘函数,事实上,它是一个高阶函数,需要传入f,后续的问题是,如何找到f,使得

(almost-factorial f)

为阶乘函数。

假设我们现在已有一个阶乘函数factorialA(不管其是否由递归定义),那么通过定义不难验证通过almost-factorial定义的factorialB也是阶乘函数

(define factorialB (almost-factorial factorialA))

但是问题是如何找到factorialA?一个很巧妙的想法是考虑

(define factorialA (almost-factorial factorialA))

如果程序语言支持惰性求值,那么上述定义是没有问题的,反之则会报错(循环定义),后续的目标是通过一个方式绕开这点,使之对不支持惰性求值的程序语言依然有效。

不动点

不动点的定义

假设$f:\mathbb R\to \mathbb R$,那么$x$为函数$f$的不动点,如果其满足

后续会使用不动点的概念。

引入不动点

考虑函数:

(define identity (lambda (x) x))
(define factorial0 (almost-factorial identity))

那么

(factorial0 0)
==> ((almost-factorial identity) 0)
==> 1

(factorial0 1)
==> ((almost-factorial identity) 1)
==> (* 1 (identity 0))
==> 0

接着定义

(define factorial1 (almost-factorial factorial0))

那么

(factorial1 0)
==> 1

(factorial1 1)
==> ((almost-factorial factorial0) 0)
==> (* 1 (factorial0 0))
==> 1

(factorial1 2)
==> ((almost-factorial factorial0) 1)
==> (* 2 (factorial0 1))
==> 0

一般的,定义

(define factorial{k} (almost-factorial factorial{k-1}))

不难验证,factorialk对于$n < k$均满足阶乘函数的定义,所以,要得到正确的阶乘函数,我们应该令$k\to \infty$,即定义

(define factorial-infinity
  (almost-factorial
   (almost-factorial
    (almost-factorial
     ...))))

但是显然不可能用上述方式定义。

现在换一个角度考虑,不难发现factorial-infinity满足

factorial-infinity = (almost-factorial factorial-infinity)

这个形式是很熟悉的,即factorial-infinity为高阶函数almost-factorial的不动点。

Y运算符

引入Y运算符

现在引入Y运算符:

(Y f) = fixpoint-of-f

其中fixpoint-of-f满足

(f fixpoint-of-f) = fixpoint-of-f

那么

factorial-infinity = (Y almost-factorial)

更一般的,对于递归函数f,如果我们可以找到f对应的almost-f(这部分还需要查阅相关资料来补充almost-f的存在性),那么

f = (Y almost-f)

现在的问题是如何定义Y?

定义(惰性求值)

根据定义可得

(Y f) = fixpoint-of-f = (f fixpoint-of-f) = (f (Y f))

因此可以定义

(define (Y f) (f (Y f)))

; lambda表达式
(define Y
  (lambda (f)
    (f (Y f))))

注意到

(Y f) = (lambda (x) ((Y f) x))

那么Y表达式可以修改为

(define Y
  (lambda (f)
    (f (lambda (x) ((Y f) x)))))

根据该定义,不难给出阶乘函数的定义

(define almost-factorial
  (lambda (f)
    (lambda (n)
      (if (= n 0)
          1
          (* n (f (- n 1)))))))

(define factorial (Y almost-factorial))

(define factorial
  ((lambda (f) (f (lambda (x) ((Y f) x))))
   almost-factorial))

==>

(define factorial
  (almost-factorial (lambda (x) ((Y almost-factorial) x))))

==>

(define factorial
  (lambda (n)
    (if (= n 0)
        1
        (* n ((lambda (x) ((Y almost-factorial) x)) (- n 1))))))

和之前的讨论类似,该定义只有在惰性求值下才有意义,我们希望给出一个在非惰性求值依然适用的情形。

定义(非惰性求值)

引入Y运算符

回顾在惰性求值下,我们可以定义

(define (Y f) (f (Y f)))

我们的目标是不使用递归的方式定义Y。

依然回到最开始的例子:

(define (factorial n)
  (if (= n 0)
      1
      (* n (factorial (- n 1)))))

要使得该函数不是递归定义,可以使用类似之前的方法引入参数self,注意这里self出现的位置不同:

(define (part-factorial self n)
  (if (= n 0)
      1
      (* n (self (- n 1)))))

注意该定义是有问题的,因为如下调用会报错

(part-factorial part-factorial n)
; 参数数量不匹配
(* n (part-factorial (- n 1)))

为了解决这点,对函数内部做修改即可

(define (part-factorial self n)
  (if (= n 0)
      1
      (* n (self self (- n 1)))))

不难验证

(define factorial (part-factorial part-factorial))
(factorial n) = n!

为了使得part-factorial的定义和最初版本类似,做如下变换

(define (part-factorial self)
  (lambda (n)
    (if (= n 0)
        1
        (* n ((self self) (- n 1))))))

注意到这里我们没有使用递归来定义阶乘函数。

为了使part-factorial向almost形式靠齐,做如下变换

(define (part-factorial self)
  (let ((f (self self)))
    (lambda (n)
      (if (= n 0)
          1
          (* n (f (- n 1)))))))

注意到

(let ((x <expr1>)) <expr2>)
==> ((lambda (x) <expr2>) <expr1>)

那么上述形式也等价于

(define (part-factorial self)
  ((lambda (f)
     (lambda (n)
       (if (= n 0)
           1
           (* n (f (- n 1))))))
   (self self)))

回顾almost-factorial的定义,

(define almost-factorial
  (lambda (f)
    (lambda (n)
      (if (= n 0)
          1
          (* n (f (- n 1)))))))

不难发现part-factorial可以使用如下方式定义

(define (part-factorial self)
  (almost-factorial
   (self self)))

那么

(define factorial (part-factorial part-factorial))

为了做进一步的变换,使用lambda表达式重写part-factorial

(define part-factorial
  (lambda (self)
    (almost-factorial
     (self self))))

代入到factorial的定义中可得

(define factorial
  (let ((part-factorial (lambda (self) 
                          (almost-factorial 
                           (self self)))))
    (part-factorial part-factorial)))

; 用x替换part-factorial
(define factorial
  (let ((x (lambda (self) 
             (almost-factorial (self self)))))
    (x x)))

消除let可得

(define factorial
  ((lambda (x) (x x))
   (lambda (self) 
     (almost-factorial (self self)))))

; 用x替换self
(define factorial
  ((lambda (x) (x x))
   (lambda (x) 
     (almost-factorial (x x)))))

提取核心部分,定义

(define (make-recursive f)
  ((lambda (x) (x x))
   (lambda (x) (f (x x)))))

那么

(define factorial (make-recursive almost-factorial))

事实上,make-recursive就是Y运算符:

(define (Y f)
  ((lambda (x) (x x))
   (lambda (x) (f (x x)))))

等价定义

使用lambda表达式进行改写得到

(define Y
  (lambda (f)
    (f (Y f))))

注意到

(Y f) = (lambda (x) ((Y f) x))

那么Y表达式可以修改为

; (define (Y f) (f (Y f)))
(define Y
  (lambda (f)
    (f (lambda (x) ((Y f) x)))))

; (define (Y f)
;   ((lambda (x) (x x))
;    (lambda (x) (f (x x)))))
(define Y 
  (lambda (f)
    ((lambda (x) (x x))
     (lambda (x) (f (x x))))))

现在考虑如下事实:

((lambda (x) (x x))
 (lambda (x) (f (x x))))

==>
(let (y (lambda (x) (f (x x))))
  (y y))

==>
((lambda (x) (f (x x))
 (lambda (x) (f (x x)))

所以Y的等价定义为

(define Y 
  (lambda (f)
    ((lambda (x) (f (x x)))
     (lambda (x) (f (x x))))))

验证不动点性质

最后我们来验证Y满足

(Y f) = (f (Y f))

注意到

(Y f)
= ((lambda (x) (f (x x)))
   (lambda (x) (f (x x))))
= (let (y (lambda (x) (f (x x))))
		(f y y))
= (f ((lambda (x) (f (x x)))
      (lambda (x) (f (x x)))))
= (f (Y f))

小结

对于递归函数

(define f (F1 f))

假设我们可以找到一个函数F2,使其满足

f = (F2 f)

那么利用Y算子即可得到y:

(define f (Y F2))

其中Y算子的定义如下:

(define Y
  (lambda (f)
    (f (lambda (x) ((Y f) x)))))

(define Y 
  (lambda (f)
    ((lambda (x) (x x))
     (lambda (x) (f (x x))))))

(define Y 
  (lambda (f)
    ((lambda (x) (f (x x)))
     (lambda (x) (f (x x))))))

后续问题,如何找到递归函数F1对应的F2,此问题似乎比较难,目前只找到如下资料:

之后学习相关资料后再加以补充。