上次发表了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  装配脑袋  阅读(6941)  评论(21编辑  收藏  举报