随笔- 31  文章- 0  评论- 290 
上次发表了VS2008亮点:用Lambda表达式进行函数式编程这篇文章后,有些人提出希望看C#版。其实我本来想等大家多尝试下能否自己实现的,可惜没有太多人实际思考这个问题,是不是觉得函数式编程离我们的日常生活太远……不管怎么说,这次我将公布强类型语言C#实现不动点组合子Y的方法,以及类型推导的全过程。不喜欢强类型思考的朋友看本文一定要做好头晕的准备……
首先复习一下不动点组合子,它就是这样的东西:(现在必须用VB语法,否则无法写出,见谅)
Dim Y = Function(f) _
            (
Function(h) Function(x) f(h(h))(x)) _
            (
Function(h) Function(x) f(h(h))(x))
对任意函数f,Y(f)就等价于f(Y(f)),具体可见前一篇文章。诸位可见,如果强类型地思考,Y的参数f的类型并不是任意的,它必须也是一个函数,能够接受Y(f)作为参数。下面需要很多类型推导的步骤才能将f的类型推出,所以首先引入一个形式化推导类型用的“签名分析法”。这种分析方法是从强类型函数式语言Standard ML中获得灵感的,在那种语言中函数签名是一种实际存在的语法结构。我们用sig f这种记法来表示函数f的签名。如果f不是函数(而是单纯变量),则sig f表示f的数据类型。我们还用<TA, TR>这种形式表示一般的一元函数,其参数类型为TA,返回值类型为TR。比方说Math.Sin函数接受double作为参数,返回类型也是double,我们就有:

sig Math.Sin == <double, double>

这就是我们后面要用的签名分析法表达式。这里特别使用双等号,为了和表示数值相等的普通等号区分开。首先我们看一个Y组合子应用的例子,还是那个求阶乘的函数:
Dim fact = Y(Function(self) Function(n) If(n = 01, n * self(n - 1)))

我们知道fact的签名和self的签名是一样的,因为它就是这样表示递归的。fact是计算阶乘的函数,所以有:

sig fact == <int, int>
sig Function(self) Function(n) If (...) == <<int, int>, <int, int>>

我们将问题一般化,认为Y的返回类型是一般性的<a, r>,其中a,r是任意类型,可以相同。因此我们就弄清楚了Y的参数类型与返回值类型:

sig Y == <YA, YR>
YA == <<a, r>, <a, r>>
YR == <a, r>

sig Y == <<<a, r>, <a, r>>, <a, r>>

最后一个Y的签名表达式就是Y的最终类型,为了便于理解我们引入YA, YR分别表示Y的参数类型和返回值类型。回到Y的定义,很明显我们知道f的签名就是Y的参数类型,而x的类型则是a,但h的类型非常不好判断。所以下面我们来推导h的类型。首先我们定义:
= Function(h) Function(x) f(h(h))(x)) 
也就是说,为Y中调用自身的函数起一个名字,很显然:

sig g(g) == YR == <a, r>

我们用gA, gR分别表示g的参数类型和返回值类型,则有

<gA, gR>(<gA, gR>) == <a, r>

观察这个签名表达式,可以得出

gA == <gA, gR> == <gA, <a, r>>
gR == <a, r>

我们看到gA存在递归定义!这就是Y实现递归的体现。gA必须具有<gA, <a, r>>的形式,也就是说gA应该能重新解释为包含自己的另一个类型。写出不包含gA的gA定义是不可能的,因此我们换用弱类型的思路。我们将gA定义为<?, <a, r>>,其中?我们不指定具体的类型,而到用的时候我们把它重新解释为<?, <a, r>>类型。很显然,在C#中object类型可以担此重任。现在我们清楚了,g的签名是这样的:

sig g == <<object, <a, r>>, <a, r>>

结合g的定义,我们现在就可以得出h,x的类型或签名了:

sig h == <object, <a, r>>
sig x == a

现在类型分析结束,我们尝试编写第一个C#版,为了简单起见,a, r这两个类型我们也全都用object来填充:
Func<Func<Func<objectobject>, Func<objectobject>>, Func<objectobject>> Y = f =>
    {
        Func
<Func<object, Func<objectobject>>, Func<objectobject>> g = h => x => f(h(h))(x);
        
return g(g);
    };
很有成就感吧,但是这个却不能执行!因为我们没有完成刚才gA递归解释的步骤,所以g不能作为g的参数直接传递。为什么呢,我们看看刚才g的签名分析,g自身的类型是<<object, <a, r>>, <a, r>>,而g的参数则是<object, <a, r>>型,我们需要把<object, <a, r>>重新解释为object类型才能工作,这就应了刚才gA分析中递归调用的结论。我们需要编写一个方法来进行这项变换:
static Func<object, Func<T2, T3>> Untype<T1, T2, T3>(Func<T1, Func<T2, T3>> src)
{
    
return o => o2 => src((T1)o)(o2);
}
光是这个Untype的定义,所用的Lambda表达式就已经超出相当多人写过Lambda表达式的难度了把。此处用到的技巧在编写很多函数式程序时都可能会用到。现在,我们得出了最终版的Y:
Func<Func<Func<objectobject>, Func<objectobject>>, Func<objectobject>> Y = f =>
    {
        Func
<Func<object, Func<objectobject>>, Func<objectobject>> g = h => x => f(h(h))(x);
        
return g(Untype(g));
    };
注意其中Untype所发挥的作用。最后我们也用C#的Y来实现匿名的递归阶乘函数,来验证一下我们刚刚复杂的分析是否正确:
Func<Func<objectobject>, Func<objectobject>> my_f = g => n => (int)n == 0 ? 1 : (int)n * (int)g((int)n - 1);

Func
<objectobject> fact = Y(my_f);

int result = (int)fact(8);

这个实现采用的是object来代替具体a, r的弱类型实现,如果想实现指定a, r具体类型的Y实现,需要将Y定义在泛型方法中。有兴趣(而且还没晕)的朋友可以继续尝试啦,呵呵。
 posted on 2007-11-26 17:14 装配脑袋 阅读(2491) 评论(19)  编辑 收藏

#1楼 [楼主]    回复  引用  查看    
 装配脑袋       | 2007-11-26 17:47
实际运行时,这个C#版比VB版慢10倍,具体原因我还不是特别明确。。。
#2楼     回复  引用  查看    
 木野狐(Neil Chen)       | 2007-11-26 17:59
看完了,叹为观止。回头慢慢研究。
#3楼     回复  引用  查看    
 木野狐(Neil Chen)       | 2007-11-26 18:00
泛型的 Untype 转换很有用
#4楼     回复  引用  查看    
 水如烟(LzmTW)       | 2007-11-26 18:03
终于又见着你的文了
#5楼     回复  引用  查看    
 随风流月       | 2007-11-26 18:10
厉害。。。慢慢看。
#7楼 [楼主]    回复  引用  查看    
 装配脑袋       | 2007-11-26 19:24
@ccBoy_ccBoy
呵呵,虽然他的Query写的很长,但是每个Lambda都很简单。Y在Lambda演算中复杂程度可以算是小儿科了,用C#实现都如此复杂,若是想实现邱奇(Church)整数之类的……
PS.那人用了Y的说,谁看出来了?
#8楼 [楼主]    回复  引用  查看    
 装配脑袋       | 2007-11-26 19:34
ccBoy引的文章里又引用了一篇实现Y的文章:
http://blogs.msdn.com/madst/archive/2007/05/11/recursive-lambda-expressions.aspx

它用真正递归定义的delegate才实现Y组合子的,很有意思。但是我觉得我的版本是最纯的匿名版,无需任何物理上有递归结构的东西作为辅助
#9楼     回复  引用    
 Lambda [未注册用户] | 2007-11-26 19:58
这里居然还有人知道Fix Point Operator,博客园上也不全是菜鸟嘛
#10楼     回复  引用  查看    
  Enzo       | 2007-11-26 19:59
@Lambda
晕.....

#11楼     回复  引用  查看    
 没剑       | 2007-11-26 22:09
偶没有装2008,所以眼花缭乱
等装了再实践
#12楼     回复  引用  查看    
 海东青       | 2007-11-27 09:44
看过一遍,懂点,回家再看
#13楼     回复  引用  查看    
 Lion       | 2007-11-27 10:26
好文,这些东西比较精彩~~~
脑袋有空多写一些,好久不见你的东东了
#14楼     回复  引用  查看    
 Lion       | 2007-11-27 11:18
前段时间比较忙,一直没有时间仔细地看.net3.0 and 3.5
看了msdn了解了Lambda和Func的用法才看懂...现在让我写估计还写不出来..

一直以前就觉得脑袋在这方面是个天才~~~
#15楼     回复  引用    
 路西菲尔 [未注册用户] | 2007-11-27 17:29
看了半天头大了,直接去看Lambda的基础概念.
#16楼     回复  引用  查看    
 yujiasw       | 2007-11-28 22:02

不知道这个怎么样,实现了递归,但是没有实现Y,看了几篇关于Y的文章,头大啊,有空继续研究.

            Func<MulticastDelegate, intint> F = (f, n) => n == 0 ? 1 : n * ((Func<MulticastDelegate, intint>)f)(f, n - 1);
            Func
<Func<MulticastDelegate, intint>, Func<intint>> P = f => x => f(f, x);
            Func
<intint> fac = P(F);
            Console.WriteLine(fac(
6));

#17楼 [楼主]    回复  引用  查看    
 装配脑袋       | 2007-11-29 14:00
MulticastDelegate 可以呀,但是还是要进行一下类型转换才能实现Y。
#18楼     回复  引用  查看    
 一瞬间       | 2008-03-11 16:51
脑袋大哥
为什么遗憾呢?我没这样做过,才觉得遗憾呢

还要多向脑袋大哥学习啊加个好友吧。msn:yiju_jha@hotmail.com

还有我回复你邮件的时候会发生错误。
#19楼     回复  引用  查看    
 Joyaspx       | 2008-05-08 00:17
看了确实有点晕,^_^

标题  
姓名  
主页
Email (只有博主才能看到) 
验证码 *  看不清,换一张 [登录][注册]
内容(请不要发表任何与政治相关的内容)  
  登录  使用高级评论  新用户注册  返回页首  恢复上次提交      
该文被作者在 2007-11-26 17:44 编辑过


相关链接:

历史上的今天:
2005-11-26 手工打造运算符重载过程