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

答:关于Fix的问题你fix了吗?

问:慢着,让我想想,上次留下个什么问题来着?是说我们有了一个求不动点的函数Fix,但Fix却是显式递归的,是吧?

答:有劳你还记的这个问题。

问:Fix的参与背离了匿名递归的定义,所以…所以…我们被Fix给坑了?

答:当然不是。你还记的第(一)章我们讨论过什么吗?

问:记的,我们把一个显式递归的Fact变成了一个匿名递归的结构。

答:很好,让我们再造一次轮子。

问:哦!我明白了,是用与上次类似的方法,把Fix写成一个匿名递归的Lambda。

答:就是这个意思,如此便可用这个Lambda来获得不动点了。那就动手吧,让我们一路与Fact对比:

//C#
//Func3 Fix = f=>f(Fix(x=>f(x)))
//写成函数的形式为:
Func1 Fix(Func2 f) { return (Func1)(f(x =>Fix(f)(x))); }
//对比Fact
int Fact(int x) { return x == 0 ? 1 : x * Fact(x - 1); }
//C#
//为了把自己传给自己构造fix_maker
OuroborosFunc<Func3> fix_maker =
    self => f => f(x => self(self)(f)(x));
//对比fact_maker
OuroborosFunc<Func<int, int>> fact_maker =
    self => x => x == 0 ? 1 : x * self(self)(x - 1);
//C#
//自我调用产生Fix
Func3 Fix = ((Func<OuroborosFunc<Func3>, Func3>)(s => s(s)))
                            (self => f => f(x => self(self)(f)(x)));
//对比自我调用产生的Fact
Func<int, int> Fact =
    ((Func<OuroborosFunc<Func<int, int>>, Func<int, int>>)(s => s(s)))
                            (self => x => x == 0 ? 1 : x * self(self)(x - 1));

观察最后产生的Fix,发现我们已经摆脱了显式的递归调用,且得到了一个可复用的不动点“生成器”。这里插一句题外话,我之前按照上述思路得到了Fix,后来才发现,原来这就是阿兰?图灵当年发现的Θ组合子(当然,我得到这个组合子要容易得多,因为图灵的年代根本没有计算机,更别说程序语言了)。我相信这两个组合子是完全等价的,下一章我们将以非形式化的方法来理解这一点。

问:那么,是时候揭开Y组合子的面纱了吧。

答:让子弹再飞一会儿。在此之前,“插播”一个小问题:如果递归函数的输入参数不止一个,该怎么办呢?例如用辗转相除法求最大公约数:

//C#
//GCD的显式递归
int GCD(int m, int n) { return n == 0 ? m : GCD(n, m % n); }

问:让我想想…嗯…那就要对之前定义的Y<T,TResult>做一些改变,写成这样:

//C#
class Y<T1, T2, TResult>
{
        public delegate TResult Func1(T1 p1, T2 p2);
        public delegate Func1 Func2(Func1 f1);
        public delegate Func1 Func3(Func2 f2);
        ... ...
}

然后,对之前的Fix改写成这样:

//C#
Func3 Fix = ((Func<OuroborosFunc<Func3>, Func3>)(s => s(s)))
                            (self => f => f((x, y) => self(self)(f)(x, y)));

如此便可用这个Fix得到gcd_seed的不动点,即GCD递归函数:

//C#
Y<int, int, int>.Func2 gcd_seed = gcd => (m, n) => n == 0 ? m : gcd(n, m % n);
Console.WriteLine(Y<int, int, int>.Fix(gcd_seed)(24, 15));    //3

答:很好,这是一个方案,但不是一个通用方案。如果有三个输入参数的递归呢?四个呢?…

问:难道还有更好的方法吗?

答:是的,答案就是Currying(柯里化)。所谓柯里化,就是把接受多个参数的函数变换成接受单一参数的函数,并且返回以柯里化的形式接受剩余参数,最终返回结果的一种技术。(注:我是在独立想到解决方案之后,才知道Currying的存在的哦!)

问:这…讲得有点抽象…

答:那就直接上例子吧。在上述最大公约数的例子中,把GCD柯里化成如下形式:

//C#
Func<int, int> GCD(int m) { return n => n == 0 ? m : GCD(n)(m % n); }

即:高阶函数GCD以m为参数,返回一个能与m求最大公约数的函数,这个函数再受一个参数n,便可返回(m,n)的最大公约数。同时,Y<T, TResult>和Fix无需任何改变,即可完成有两个输入参数的递归:

//C#
Y<int, Func<int, int>>.Func2 gcd_seed =
        gcd => m => n => n == 0 ? m : gcd(n)(m % n);
Console.WriteLine(Y<int, Func<int, int>>.Fix(gcd_seed)(24)(15));    //3

依此看来,两个参数的Y<T, TResult>就足以满足任何递归的需求了,美哉!

问:然是美哉!

答:就在下章,静候Y组合子的“粉墨登场”。待续…

时间: 2024-12-21 20:36:31

大到可以小说的Y组合子(三)的相关文章

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

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

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

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

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 组合子:不用 λ演算那一套

重新发明 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 (

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

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 协议中的一个例子,用上述的组合式解析器实现其语法解析器.读者在这