重新发明 Y 组合子:不用 λ演算那一套

重新发明 Y 组合子:不用 λ演算那一套

重新发明 Y 组合子:不用 λ演算那一套

目录

  • 1. 一类奇妙的函数

    • 1.1. 三个例子
    • 1.2. 构造方法
    • 1.3. 程序分解
  • 2. X 组合子
  • 3. Y 组合子
  • 4. 结语

1 一类奇妙的函数

1.1 三个例子

函数

(lambda ($)
  (lambda (n)
    (if (< n 2)
        1
        (* n (($ $) (- n 1))))))

以一个函数为参数,当这个参数是函数本身时,返回计算阶乘的函数

函数

(lambda ($)
  (lambda (l)
    (if (null? l)
        0
        (+ 1 (($ $) (cdr l))))))

以一个函数为参数,当这个参数是函数本身时,返回计算列表长度的函数

函数

(lambda ($)
  (lambda (n)
    (cond ((= n 0) 0)
          ((= n 1) 1)
          (else (+ (($ $) (- n 1))
                   (($ $) (- n 2)))))))

以一个函数为参数,当这个参数是函数本身时,返回计算 Fibonacci 数的函数

1.2 构造方法

上面三个函数的构造方法很机械。

以第一个为例。

常见的计算阶乘的函数

(define fact
  (lambda (n)
    (if (< n 2)
        1
        (* n (fact (- n 1))))))

该函数的名字是

fact

该函数的函数体是

(lambda (n)
  (if (< n 2)
      1
      (* n (fact (- n 1)))))

该函数的名字在该函数的函数体中的上下文是

(lambda (~)
  (lambda (n)
    (if (< n 2)
        1
        (* n (~ (- n 1))))))

以一个函数为参数,当参数是自身时返回计算阶乘函数的函数就是

(lambda ($)
  (lambda (n)
    (if (< n 2)
        1
        (* n (($ $) (- n 1))))))

1.3 程序分解

重复上一节的最后两个步骤。 我们是从【fact 函数的名字在 fact 函数的函数体中的上下文】

A = (lambda (~)
      (lambda (n)
        (if (< n 2)
            1
            (* n (~ (- n 1))))))

构造【以一个函数为参数,当参数是自身时返回计算阶乘函数的函数】

B = (lambda ($)
      (lambda (n)
        (if (< n 2)
            1
            (* n (($ $) (- n 1))))))

的。但是正如你看到的那是无理由的机械构造。

那么问题来了。B 真的能由 A 构造吗?

对比一下

;; 只有一点点不同
A = (lambda (~) (lambda (n) (if (< n 2) 1 (* n (~     (- n 1)))))) ;; 一个是 ~
B = (lambda ($) (lambda (n) (if (< n 2) 1 (* n (($ $) (- n 1)))))) ;; 一个是 ($ $)

很像数学

   a    * c
(a + b) * c = a * c + b * c

式子

(a + b) * c

确实可以由式子

a * c

来构造。

这是乘法的【结合律】。

程序也有【结合律】吗?

数学中乘法的【结合律】是用【因式分解】证明的。

程序里面怎么做【因式分解】?

用【上下文】。

分解谁?

分解

B = (lambda ($) (lambda (n) (if (< n 2) 1 (* n (($ $) (- n 1))))))

因为,我们想看看它是不是由

A = (lambda (~) (lambda (n) (if (< n 2) 1 (* n (~ (- n 1))))))

构造的。

怎么分解?

($ $)

的上下文,因为它是两者唯一的不同点。

怎么取它的上下文呀?包含它的表达式起码有

1. ($ $)
2. (($ $) (- n 1))
3. (if (< n 2) 1 (* n (($ $) (- n 1))))
4. (lambda (n) (if (< n 2) 1 (* n (($ $) (- n 1)))))
5. (lambda ($) (lambda (n) (if (< n 2) 1 (* n (($ $) (- n 1))))))

5 个。取它在哪个表达式中的上下文呀?

都取试试看

1. (lambda (~) ~)
2. (lambda (~) (~ (- n 1)))
3. (lambda (~) (if (< n 2) 1 (* n (~ (- n 1)))))
4. (lambda (~) (lambda (n) (if (< n 2) 1 (* n (~ (- n 1))))))
5. (lambda (~) (lambda ($) (lambda (n) (if (< n 2) 1 (* n (~ (- n 1)))))))

1 是废话,不要。

2 和 3 少了n 的信息,也不要。

5 多了无用的 dollar ,也不要。

4 提供的绑定不多不少,就要它了。

而且回过头来看 4 正好就是 A

4.  (lambda (~) (lambda (n) (if (< n 2) 1 (* n (~ (- n 1))))))
A = (lambda (~) (lambda (n) (if (< n 2) 1 (* n (~ (- n 1))))))

现在更加确定 A 能构造出 B ,即 B 能分解出 A 来。

把 4 装回 B 中去。【装逼去!】

A = (lambda (~) (lambda (n) (if (< n 2) 1 (* n (~ (- n 1))))))
B = (lambda ($) (lambda (n) (if (< n 2) 1 (* n (($ $) (- n 1))))))
4.  (lambda (~) (lambda (n) (if (< n 2) 1 (* n (~ (- n 1))))))
;; 装回去
B = (lambda ($) ((lambda (~) (lambda (n) (if (< n 2) 1 (* n (~ (- n 1))))))
                 ($ $)))

这样装 B 对不对呀?

不对。

在真 B 中 (dollar dollar) 在求值进入到里面那个函数之后,如果 n 小于 2 就不用求值,如果 n 不小于 2 才求值。

而装 B 中 (dollar dollar) 不用等到求值进入到里面的那个函数,求值才刚进入外面这个函数就必须求值了。

所以说装 B 不对。

怎样把装 B 修复成真 B ?

装 B 把真 B 中的一个求值提前了,那我们就把它延迟一下就行了。

B = (lambda ($) ((lambda (~) (lambda (n) (if (< n 2) 1 (* n (~ (- n 1))))))
                 (lambda (n) (($ $) n))))

现在 B 中分解出 A 来了,证实了能从 A 构造出 B 来,我们的程序分解结束了吗?

没有,A 插入 B 中太深,拔出一点点来才好。

B = ((lambda (^)
       (lambda ($)
         (^ (lambda (n) (($ $) n)))))
      (lambda (~) (lambda (n) (if (< n 2) 1 (* n (~ (- n 1)))))))
A =   (lambda (~) (lambda (n) (if (< n 2) 1 (* n (~ (- n 1))))))

这种深度最舒服了。

2 X 组合子

把剩下的两个例子也分解一下和上一节的放在一起

((lambda (^) (lambda ($) (^ (lambda (n) (($ $) n)))))
 (lambda (~) (lambda (n) (if (< n 2) 1 (* n (~ (- n 1)))))))

((lambda (^) (lambda ($) (^ (lambda (list) (($ $) list)))))
 (lambda (~) (lambda (list) (if (null? list) 0 (+ 1 (~ (cdr list)))))))

((lambda (^) (lambda ($) (^ (lambda (n) (($ $) n)))))
 (lambda (~) (lambda (n) (cond ((= n 0) 0) ((= n 1) 1) (+ (~ (- n 1)) (~ (- n 2)))))))

三个函数的第一行都是一样的,抽象出来吧

(lambda (^)
  (lambda ($)
    (^ (lambda a (apply ($ $) a)))))

取个什么名字好?

X 呀,因为之前又是 B 又是 插入太深的。

(define X (lambda (^) (lambda ($) (^ (lambda a (apply ($ $) a))))))

现在有了 X 了,用一句话说清楚 X 有什么用?

X 能将【有名字的递归函数的名字在函数体中的上下文】转换成【以一个函数为参数,当参数是它本身时,返回递归函数的函数】。

X!这么复杂的表述方式,但是老子还是听懂了。那有没有
能将【有名字的递归函数的名字在函数体中的上下文】转换成【递归函数】
的函数。

有,就是下一个字母 Y 。

3 Y 组合子

(define Y
  (lambda (^)
    ((X ^) (X ^))))

4 结语

老子总算懂 Y 组合子了。但这个过程中经历一次装B失败。

其实如果仔细研究最简单的

(define fact
  (lambda (n)
    (if (< n 2)
        1
        (* n (fact (- n 1))))))

这种类型的递归函数,就可能避免这次装B失败。

【递归函数总要使用 if 或者 cond 】

因为它们特殊的求值规则,能够延迟或者取消对递归部分的求值,避免无穷递归下去。

时间: 2024-08-04 14:05:33

重新发明 Y 组合子:不用 λ演算那一套的相关文章

Lambda演算 - 简述Y组合子的作用

Y组合子:\f.(\x.f(xx))(\x.f(xx)),接受一个函数,返回一个高阶函数 Y组合子用于生成匿名递归函数. 什么叫匿名递归函数,考虑以下C语言递归函数 int sum(int n) { return n == 0 ? 0 : n + sum(n-1); } 这个函数在内部递归调用了自身,调用自身需要函数本体的名字,这个函数叫sum,sum内部用名字sum,递归调用了自己 在lambda演算中,可以写成类似的表达式sum = \x. x == 0 ? 0 : sum x 但是对于一个

大到可以小说的Y组合子(一)

问:上回乱扯淡了一通,这回该讲正题了吧. 答:OK. 先来列举一些我参考过,并从中受到启发的文章. (1.)老赵的一篇文章:使用Lambda表达式编写递归函数 (2.)装配脑袋的两篇文章:VS2008亮点:用Lambda表达式进行函数式编程和用Lambda表达式进行函数式编程(续):用C#实现Y组合子 (3.)Y组合子的推导过程(用Scheme推导),这里的"推导"并不是数学意义上上的推导证明,而是说如何一步步引导构想出Y来的,值得一看.也许从某种程度反应出了当年Y是如何被发明的. [

Racket中使用Y组合子

关于Y组合子,网上已经介绍很多了,其作用主要是解决匿名lambda的递归调用自己. 首先我们来看直观的递归lambda定义, 假设要定义阶乘的lambda表达,C#中需要这么定义 Func<int, int> fact = null; fact = x => x <= 1 ? 1 : x * fact(x - 1); 这种方法非常简单直接,当然问题也存在,因为这里fact其实是一个委托对象,当这个对象改变后,可能就得不到阶乘的效果了. 在scala中则是这样, def F: Int

大到可以小说的Y组合子(二)

问:上一回,你在最后曾提到"抽象性不足",这话怎么说?  答:试想,如果现在需要实现一个其它的递归(比如:Fibonacci),就必须把之前的模式从头套一遍,然后通过fib_maker(fib_maker)来返回一个fib函数.可见,这个产生递归过程的"接口"让用户相当不舒服.  问:嗯,fib_maker(fib_maker)这种形式看起来的确不怎么舒服,那又如何对其进行抽象,以得到更好的接口呢?  答:这里,有两条路可以走.其一,就是对fact_maker(fa

大到可以小说的Y组合子(三)

答:关于Fix的问题你fix了吗? 问:慢着,让我想想,上次留下个什么问题来着?是说我们有了一个求不动点的函数Fix,但Fix却是显式递归的,是吧? 答:有劳你还记的这个问题. 问:Fix的参与背离了匿名递归的定义,所以-所以-我们被Fix给坑了? 答:当然不是.你还记的第(一)章我们讨论过什么吗? 问:记的,我们把一个显式递归的Fact变成了一个匿名递归的结构. 答:很好,让我们再造一次轮子. 问:哦!我明白了,是用与上次类似的方法,把Fix写成一个匿名递归的Lambda. 答:就是这个意思,

scheme实现匿名递归阶乘(Y组合子)

(((lambda ()         ((lambda (f)        (f f))      (lambda (x)        ((lambda (y)           (lambda (n)             (if (zero? n)                 1                 (* n (y (- n 1)))))) (lambda z                                        (apply (x x) 

利用F#编写、理解Y组合子函数

赵劼的博客<使用Lambda表达式编写递归函数>中用C#实现了为函数求出 Y 组合子的函数.C#代码生涩,难以阅读,原代码如下: static Func<T, TResult> Fix<T,TResult>(Func<Func<T, TResult>, Func<T, TResult>> f) { return x => f(Fix(f))(x); } static Func<T1, T2, TResult> Fix

Y组合子

// Y组合子 fact = n => n == 1 ? 1 : n * fact(n - 1) // 无法匿名调用 (n => n == 1 ? 1 : n * fact(n - 1))(5) // 参数可以命名, 消灭掉里面掉fact fact = (f, n) => n == 1 ? 1 : n * f(f, n - 1); // 只能有一个参数 fact = f => n => n == 1 ? 1 : n * f(f)(n - 1); // 将fact带入 amaz

解析器组合子

本文引自:http://www.ibm.com/developerworks/cn/java/j-lo-compose/ Ward Cunningham 曾经说过,干净的代码清晰地表达了代码编写者所想要表达的东西,而优美的代码则更进一步,优美的代码看起来就像是专门为了要解决的问题而存在的.在本文中,我们将展示一个组合式解析器的设计.实现过程,最终的代码是优美的,极具扩展性,就像是为了解析特定的语法而存在的.我们还会选取 H.248 协议中的一个例子,用上述的组合式解析器实现其语法解析器.读者在这