随笔- 31  文章- 0  评论- 290 
Visual Studio 2008正式发布了,我们.NET开发者也走进了新的.NET 3.5时代。相对于.NET 3.0中的Foundations,3.5的改进更注重编码体验了。新的Linq系列语法和Linq to Sql、Linq to Xml大大改变了我们访问数据的方式。但是纵观Linq系列语法,影响最大的要数Lambda表达式。虽然从渊源来讲Lambda表达式就是匿名函数的简化表示方法,但谁也想不到这一小小的语法给我们带来了整个Linq与众不同的查询式语法。
Linq这种类似于SQL的语言,含有很多通常我们传统语言所不具备的要素。比如我们可以用一条语句将规则作用于整个集合之上,而规则则是通过表达式来传递的。也就是说我们编写这样的语句时,查询中的表达式并不立即执行,而是之后对整个集合的每个元素进行分别调用。这里面一个重要的思想就是我们将运算规则传递给了查询语句,而不是表达式的值或结果。这种思想在函数式语言中尤为重要,我们通过函数为媒介,用组合与递归的方式将运算规则逐渐抽象为一个完整的应用程序。
当然,整个函数式编程范式与我们所熟悉的命令式语言都是不同的,这里我们仅讨论函数式编程的一些思想,希望能够启发思考,或作为命令式编程的一种补充来使用。其实我们在编写Linq代码中已经用到了函数式编程的思想:
int[] a;
a.Where(i 
=> i > 0).Select(i => i * 2);
这里,Where函数将我们用Lambda表达式传递的运算规则作用于a[]的每一个元素上,然后返回满足条件的所有元素。这就是一种筛选操作。而Select函数则将规则作用于集合的每一个元素后返回新值组成的集合。这是一种映射操作。C#简短的Lambda语法为我们编写类似的逻辑提供了很大的方便。不过呢,从下面开始,我将使用Visual Basic 2008的Lambda表达式继续讨论。有人可能奇怪为什么不继续使用C#了呢,下面我就会介绍VB2008编写函数式编程的独特之处。即使您用C#,我仍建议您往下看完,您会看到有些程序用VB来写更容易想清楚;而C#的对应做法下面也会有小小的挑战等着您
首先我们熟悉一下VB2008的Lambda表达式语法。它由两个部分组成:
Function(参数列表)表达式

Function(a) a + 1 '等于C#的 a => a + 1
Function(a As Integer) a + 1 '显式指定类型
Function(a, b) a + b '两个或多个参数

如您所见,和普通的函数声明相比,它只是没有函数的名字,以及不需要使用Return语句而已。注意VB的Lambda表达式只能使用单行表达式作为函数体,而且必须有返回值,C#的Lambda表达式是没有这些限制的。接下来就是重头戏,VB2008的Lambda表达式拥有一些C#3.0所不具备的突出特性,使得它成为我们学习函数式编程的首选:
1.匿名委托特性
VB2008支持一种称作匿名委托的特性。请不要和匿名函数的概念弄混,有些人喜欢把C#的匿名方法教成匿名委托,实际上两者并不相同。VB2008可以根据Lambda表达式的签名,自动生成委托的定义。也就是说在VB中使用Lambda表达式,不用事先声明接受它的委托类型。这使得VB的Lambda表达式更像变量,而不是函数。
Dim f = Function(i) i + 1

'无需声明f的类型,它自动从Lambda表达式推导
f(2'调用,结果为3

而C#则需要显式定义委托的类型,是不能用var f = (int i) => i + 1;的形式来定义Lambda表达式变量的。后面我们会看到Lambda表达式的类型令人惊奇地复杂,以至于要显式声明有时会过于困难。而且匿名委托带来的好处绝不仅仅只有这一点……。

2.声明同时调用
拜匿名委托所赐,VB的Lambda表达式在任何时候都具有可调用的语义,即使是刚刚声明出来:
Dim result = (Function(a, b) a + b)(12)
'result结果为3

这种即时调用的用法实现某些逻辑的时候提供了极大的方便。而C#的Lambda表达是不具备即时调用的能力,必须用一个已知委托类型的变量接受它,然后才能通过委托变量调用。

3.同签名委托间的类型转换
这是一个VB编译器帮忙实现的小特性,即如果两个委托类型的参数之间有互相兼容的特性(完全一致或可以进行转换),那么两个委托变量之间就可以直接进行类型转换。而且这个转换支持VB的隐式转换规则。后面我们会看到,这个用法对编写动态语义的函数非常有帮助。
Delegate Function A(x As Integer)As Integer

Dim x As New A(Function(i) i + 1)
Dim y As Func(Of IntegerInteger)

= x '类型转换达成!

C#并不支持这种类型转换,但C#实现相同的功能也极为容易,你知道怎样写吗?不妨考虑一下。

以上三点特性加上VB的动态语言特性(隐式类型转换,后期绑定),使得VB的Lambda语法非常接近正牌函数式语言,比如Lisp。当然与现代函数式语言相比,这些功能还显得有些匮乏。下面我们就开始一个有趣的例子:用函数表示数据。Lambda函数的语法能够从表达式所在的上下问中捕获变量,这种称之为“闭包(Closure)”的特性使得函数具有表示数据的能力。例如我们不用Structure或Class之类的语句,就能完全生成一种包含两个成员的对偶结构:
Dim MakePair = Function(u, v) Function(m) If(m = 0, u, v)

Dim pair = MakePair("Asdf""231")
Console.WriteLine(pair(
0)) '第一个字段
Console.WriteLine(pair(1)) '第二个字段

您可以看到,MakePair不仅仅自己是一个函数,它调用之后还会返回另一个函数,而后者则捕获了MakePair传入的参数,形成了一个保存数据的结构体。这种返回函数的函数称作高阶函数。高阶函数就是函数式语言与普通命令式语言中函数的最大不同,它将函数的抽象能力提高了一个档次。
下面我们来看一个更难,更神奇的例子:怎么用Lambda表达式实现递归。众所周知,Lambda表达式是匿名函数,因此他们不能通过引用自己名字的方式进行递归。有人可能想到,将承载Lambda表达式的委托变量捕获到Lambda的闭包中实现递归,但这样违背函数式编程的精神。到底能不能实现纯粹Lambda的递归呢?答案是肯定的,但是需要一个特殊的东西——不动点组合子。
不动点用一句通俗的话表示就是将它作用于任何函数f后,等同于将f再次调用到整个体系本身上的效果。假设用Y表示不动点组合子,那么Y(f)的效果就等同于f(Y(f))的效果。也就是说我们把调用的体系Y(f)又完整地作为参数传递给了f函数。聪明的你就会发现,这个Y组合子可以用来实现函数的递归。只要让f的内部逻辑将自己的参数视为下一次调用的自身函数即可实现。如此神奇的Y在VB中到底是什么样子呢?如果用无类型的写法,它类似于这样:
Dim Y = Function(f) _
            (
Function(h) Function(x) f(h(h))(x)) _
            (
Function(h) Function(x) f(h(h))(x))
这行语句体现了函数式编程精妙之所在,函数Y是一个函数,它接受了一个函数f;接下来Y在内部生成了一个函数,它又以函数h为参数,返回了接受x的第三个函数。在完成了f(h(h))(x)这一连串调用后,Y又将自己内部的函数传递给了自己,并将最终的结果返回。尽管有如此解释,理解不动点组合子Y的真实运算过程是极其费脑子的,如果不能理解,我们可以直接记住Y(f)等同于f(Y(f))这一恒等式。虽然我们写出了Y的定义,但遗憾的是这样并不能执行。因为编译器不能猜出f,h等参数的类型,因此不允许用函数调用的语法访问他们。为了解决这个问题,我们采用VB的后期绑定功能:
Dim Y1 = Function(f) _
            (
Function(h) Function(x) f.Invoke(h.Invoke(h)).Invoke(x)) _
            (
Function(h) Function(x) f.Invoke(h.Invoke(h)).Invoke(x))

这时Y1就是一个真实可以运行的不动点组合子。我们来看一个用Y实现递归的例子:
Dim fact = Y1(Function(self) Function(n) If(n = 01, n * self.Invoke(n - 1)))

注意,fact这个函数是一个真正的匿名函数,它的内部并没有引用自己的名字,但是它采用不动点算子实现了递归。我们知道Y1作用于任何函数数,都等于将后者再次作用于整个体系上的效果。所以上述代码进行这种规则运算后就等同于:
fact = Function(n) If(n = 01, n * fact(n - 1))

很简单,只是将原代码中self位置换成代表整个体系Y1(f)的fact,就可以得到这一结果。很显然,这就是计算阶乘的递归算法。调用fact函数,你就能真实得到运算结果(记住参数不要太大。。。)。马上试验一下吧!是不是很神奇。虽然不太搞得懂Y内部的奇妙逻辑,但程序真的跑起来了!
虽然程序跑起来的,但使用.Invoke的后期绑定语法效率不高,我们下一步要得到一个具有一定类型约束的版本。这需要很强的逻辑分析能力,各位有兴趣可以尝试一下,我们最后得到的结果如下:
Dim Y = Function(f As Func(Of Func(Of ObjectObject), Func(Of ObjectObject))) _
                    (
Function(h As Func(Of Object, Func(Of ObjectObject))) Function(x) f(h(h))(x)) _
                    (
Function(h As Func(Of Object, Func(Of ObjectObject))) Function(x) f(h(h))(x))

Dim fact = Y(Function(self As Func(Of ObjectObject)) Function(n) If(n = 01, n * self(n - 1)))

Dim result = fact(4'真的能工作哦

诸位一定被这里复杂的类型吓到了。这下可以理解我前面说VB的匿名委托与弱类型特性有多么重要了吧。用这个方法获得的递归算法大概要比普通的递归慢一倍左右,我们认为这样的结果已经很好了,至少他们是同一个数量级的。(当然,不是说真实程序也非得这么编,这里只是在学习它的奇妙特性)。现在,大家都已经做过实验了,有没有人有兴趣研究Y在C#中的实现呢?首先我必须提醒C#实现Y是比较困难的!需要脑子里有特别清楚的类型概念和逻辑思维能力才能正确写出来(然而真的能写出来!不要怀疑)。我把这个问题留给感兴趣的人来挑战一下吧。
看了以上的例子,我想以前对函数式编程不了解的读者也已经感受到函数式编程的奇妙之处了吧。而且更进一步地研究还有更神奇的思想在里面。我们平时享受Linq的便捷语法时可能不曾想过这么深层的函数式世界,所以偶尔思考一下可能就能激发出更多的灵感,从而写出更多前所未有的程序来。欢迎各位参与讨论。
 posted on 2007-11-23 21:15 装配脑袋 阅读(4482) 评论(45)  编辑 收藏

#1楼     回复  引用  查看    
 随风流月       | 2007-11-23 21:31
抢一个沙发。明天仔细看。
#2楼     回复  引用  查看    
 随风流月       | 2007-11-23 21:36
看完了前半篇文章。去睡觉了。明天起来再看。
#3楼     回复  引用  查看    
 冷火       | 2007-11-23 22:11
对VB不再感兴趣了,还是C#正道
#4楼 [楼主]    回复  引用  查看    
 装配脑袋       | 2007-11-23 22:17
@冷火
老兄不看看VB2008的新特性会很遗憾的哦,C#并不包含VS全体Team的新智慧。
#5楼     回复  引用  查看    
 Adrian H.       | 2007-11-23 22:30
有了Inteliisense everywhere等改进之后...感觉VB9的编码效率比C#高了不少啊...
#6楼     回复  引用  查看    
 Zhuang miao       | 2007-11-23 23:02
如果是C#我会很乐意认真阅读完!唉 vB看着就不舒服~~
#7楼 [楼主]    回复  引用  查看    
 装配脑袋       | 2007-11-23 23:22
@Zhuang miao
平时使用C#时,很少会用到这些代码的,不妨当成变换思路吧。
#8楼 [楼主]    回复  引用  查看    
 装配脑袋       | 2007-11-23 23:24
PS.我要强调的是,用VB而不用C#是因为用C#的Lambda语法想这些问题不仅更难,更乱,而且几乎没有多少人能够一下写对。因此大家如果平时用的是C#,就很少有机会想这些问题。VB在本文提到的特点能够简化问题,所以不是强迫你学习VB,而是帮助大家更容易地感受函数式编程的与众不同感。
#9楼     回复  引用  查看    
 没剑       | 2007-11-24 00:02
因为是VB我才会很乐意认真阅读完!唉,如果是C#的话看着就不舒服!
#10楼     回复  引用  查看    
 KKcat       | 2007-11-24 00:37
呵呵,因为是菜鸟说以不发表其它言论
#11楼     回复  引用  查看    
 ZeroCool       | 2007-11-24 00:58
这是C# 3.0的亮点,如果说成“VS2008的亮点”也说得过去,但感觉似乎很牵强。
#12楼     回复  引用  查看    
 木野狐(Neil Chen)       | 2007-11-24 03:12
几个感觉:
1. Y 算子很难懂。。
2. VB 9 很强大
3. C# 写了半天没写出来

为了理解 Y 算子,写了一个 python 版本的示意代码(故意写成了内嵌函数为便于理解):

# Y 算子
def Y(X):
    
def aa(f):
        
def b(arg):
            
return (f(f))(arg)
        
return X(b)
    
return aa(aa)

def cc(self):
    
def dd(n):
        
if n == 0:
            
return 1
        
else:
            
return n * self(n-1)
    
return dd

fact 
= Y(cc)
for i in range(16):
    
print fact(i)

#13楼     回复  引用  查看    
 命运有自己的梦!       | 2007-11-24 07:51
技术的进步给我们带来最好的好处的效率的提高,但是如果总是只出现些零散的东西进步也就没有多大意义了,就比如Ruby on Rails,如果能有这么一套完整的开发概念,确实为开发者解决烦琐的实际问题,那才会令人痴迷!
#14楼     回复  引用  查看    
 随风流月       | 2007-11-24 08:05
阿哈哈......
总算给我理解完了。刚开始还纳闷,为什么要有俩一模一样的 (Function(h) Function(x) f(h(h))(x))?后来一想,原来敢情是把自己再丢给自己运算了。至于f这个咚咚,可以想象成创建递归函数函数体的工厂。
#15楼     回复  引用  查看    
 怪怪       | 2007-11-24 08:15
看了几眼一抬头, 果然是LZ的文章 :)
#16楼     回复  引用  查看    
 Hafeyang       | 2007-11-24 08:35
int[] a;
a.Where(i => i > 0).Select(i => i * 2);

看着又点像Ruby
#17楼     回复  引用  查看    
 金色海洋(jyk)       | 2007-11-24 09:24
收藏
#18楼     回复  引用  查看    
 jjx       | 2007-11-24 11:41
vb lambda 因为只允许一行,所以实用性大打折扣

c# /vb的新特色得确能写出很多漂亮简洁的代码来,现在正在改造自己的框架来着
#19楼     回复  引用    
 broodwarfish [未注册用户] | 2007-11-24 11:44
很吸引人拉.可是用VB来诠释,我这个有点看不懂..好久都没有看VB了..
#20楼     回复  引用  查看    
 Allen Lee       | 2007-11-24 12:17
脑袋,你终于又路面了~~~
#21楼     回复  引用    
 Ray Zhang [未注册用户] | 2007-11-24 12:18
不错的短文,补充一下,
在这个版本VB与C#中对于lambda的支持有些不同:
VB对lambda表达式的支持短小精悍,但不支持多行也不支持声明语句,C#中这两点都支持但多行的可能会比较慢。
#22楼 [楼主]    回复  引用  查看    
 装配脑袋       | 2007-11-24 12:33
如果有人还觉得学习函数式思想时还是C#的Lambda更好,那就来试试实现Y算子哦:P
大部分具有Lambda的动态语言都能一下写出Y来的。C#的语法的确更短,而且支持多行语句,但其实函数式算法几乎都不需要多行语句。而真正函数式算法所需的及时执行和类型转换方面的功能C#确都不能支持,所以C#按照真正函数式语言那样写程序是难上加难的。
#23楼 [楼主]    回复  引用  查看    
 装配脑袋       | 2007-11-24 12:59
@木野狐(Neil Chen)
用嵌套函数的形式写出来,如果能够单步跟踪一下它的执行过程就能理解Y了,呵呵。

#24楼     回复  引用  查看    
 随风流月       | 2007-11-24 13:19
@装配脑袋
不如编译后拿 Reflector 把代码抓出来调试 :D
#25楼     回复  引用  查看    
 随风流月       | 2007-11-24 13:23
@ZeroCool
VB9/C#3 共有的新特性。
#26楼     回复  引用  查看    
  Enzo       | 2007-11-24 16:46
@Zhuang miao
主要是你用C#习惯,突然看VB有点不舒服,再看下就习惯了 !
#27楼     回复  引用  查看    
 jjx       | 2007-11-24 17:08
看你怎么使用lambda (或者说匿名方法)了,如果你将其作为一个上下文的解决方案,像ruby的block来使用,多行是必须的,我做数据库应用时,vb的单行确实不够用


#28楼 [楼主]    回复  引用  查看    
 装配脑袋       | 2007-11-24 20:15
@jjx
你还是把它当作命令式语言来使用的,所以根本没有进行函数式编程。那么本文提到的东西当然就不在你的考虑之列。我们整天用命令式程序写一大堆代码,把其他编程范式都给忽略了,不觉得这样缺乏驾驭计算机的感觉吗?大家都是实用派哈,除了木野狐和随风流月之外没有人关注问题本身的样子……
#29楼     回复  引用  查看    
 木野狐(Neil Chen)       | 2007-11-24 21:19
用 C# 写的时候遇到的主要问题,就是很难推断清楚 f(f) 这种调用的返回类型,好多地方没法定义,越想越乱。
看来 VB 9 的匿名委托、和后期绑定,是有莫大的好处的。

这篇文章很精彩,昨晚花了我很长时间学习,还在别的地方搜了一些 scheme 的代码才理解了一点。

#30楼     回复  引用  查看    
 jjx       | 2007-11-24 21:48
这个,哪有为函数式而函数式的,lambda的作用并不仅仅与此,另外,如果真正的要用函数式,有太多的更适合的语言可以选择,何必执著于此呢
#31楼 [楼主]    回复  引用  查看    
 装配脑袋       | 2007-11-24 22:02
@木野狐(Neil Chen)
难得你这么关注这个问题,真是不胜感激。我想看看有没有其他人尝试写出C#版,然后再公布答案。先要提示一下,要想实现C#版,必须稍微跳出强类型的框框,因为既然Y能够实现Y(f) = f(Y(f)),Y中某个类型必然具有递归结构,想绝对强类型是实现不了的。而且,这也相当考验C#中编写Lambda表达式的功力哦,呵呵。
#32楼     回复  引用  查看    
 木野狐(Neil Chen)       | 2007-11-24 22:23
@装配脑袋
谢谢你的提示,我找时间再试试:)
#33楼     回复  引用  查看    
 随风流月       | 2007-11-25 10:11
@装配脑袋
俺实在是没什么 C# 功力的,就算了。
Lambda 是非常好用的一种东西。
#34楼     回复  引用  查看    
 rex xiang       | 2007-11-27 11:30
@旻(http://www.cnblogs.com/renmin/)

请你不要在其他人的blogs回复中, 悄悄藏一个很小的播放器(小到无法控制播放停止,只有一个小黑点), 已经发现好几篇blog上都有. 不管我是否想不想听, 或者是否已经听过了, 都无休止的播放着, 看上去很讨厌.
为了大家的方便, 请你还是把播放器去掉吧!
#35楼 [楼主]    回复  引用  查看    
 装配脑袋       | 2007-11-27 15:05
--引用--------------------------------------------------
旻: 以后公司的面试题要是出现Y算子,或者Lamda表达式,这可怎么办呀。
--------------------------------------------------------

这是您的评论,播放器不好意思我删掉了,因为有人觉得影响了正常浏览
#36楼     回复  引用  查看    
 浪子       | 2007-11-27 16:21
上次脑袋就介绍过函数式编程思想,呵呵,当时没啥感觉。

今天特意看了好多文章,收益颇多。

我用VS2008 beta2,似乎在
Dim x As New A(Function(i) i + 1)
Dim y As Func(Of Integer, Integer)

y = x '类型转换达成!
的转换不成功。

Error 1 Value of type 'LambdaTest.Module1.A' cannot be converted to 'System.Func(Of Integer, Integer)'. D:\Cdy_DotNet\TestSolution\LambdaTest\LambdaTest\Module1.vb 46 13 LambdaTest

#37楼     回复  引用  查看    
 yujiasw       | 2007-11-27 16:57
//C#

delegate T SelfApplicable<T>(SelfApplicable<T> self);
private static void FixedPoint()
{
SelfApplicable<Func<Func<Func<int, int>, Func<int, int>>, Func<int, int>>>
Y = y => f => x => f(y(y)(f))(x);

Func<Func<Func<int, int>, Func<int, int>>, Func<int, int>> Fix = Y(Y);

Func<Func<int, int>, Func<int, int>>
F = fac => x => x == 0 ? 1 : x * fac(x - 1);

Func<int, int> factorial = Fix(F);

Console.WriteLine(factorial(5));

}
#38楼 [楼主]    回复  引用  查看    
 装配脑袋       | 2007-11-27 21:04
@yujiasw
这是Mads的实现,其实已经采用的递归定义的委托类型,作为Lambda的实现意义不如我的大。因为用递归可以轻而易举地实现不动点组合子。
#39楼 [楼主]    回复  引用  查看    
 装配脑袋       | 2007-11-27 21:06
@浪子
你该不是开了Option Strict On把,其实开了也应该可以转换成功才对呀
#40楼     回复  引用    
 Yashmak [未注册用户] | 2007-11-28 10:18
博主的题目取得太不靠谱了...
这是语言的功能,不是开发工具的功能. 没有Visual Studio 2008,用csc/vbc 照样可以编译包含Linq/Lamda的C#/VB.Net程序
#41楼     回复  引用  查看    
 随风流月       | 2007-11-29 13:02
@Yashmak
用 .net Framework 3.5 如何?
#42楼     回复  引用    
 vwxyzh [未注册用户] | 2007-12-28 17:16
关于Y的那段不错,稍微修改一下,性能可以接近Mads的实现,不过修改后不能计算较大的数(原来Object版本会自动转换类型,变成Decimal或Double,太强了)

Shared Function Foo(Of T)(func As Func(Of Func(Of T, T), Func(Of T, T))) As Func(Of T, T)
Dim Y = Function(f As Func(Of Func(Of T, T), Func(Of T, T))) _
(Function(h As Func(Of Object, Func(Of T, T))) Function(x) f(h(h))(x)) _
(Function(h As Func(Of Object, Func(Of T, T))) Function(x) f(h(h))(x))
Return Y(func)
End Function

Sub Bar()
Dim fact = Foo(Of Integer)(Function(self) Function(n) If(n = 0, 1, n * self(n - 1)))
Dim result = fact(4)
End Sub
#43楼     回复  引用  查看    
 逍遥海盗船       | 2008-02-27 09:37
VB好。
#44楼     回复  引用    
 EYG [未注册用户] | 2008-06-01 11:40
支持vb,收益很大

标题  
姓名  
主页
Email (博主才能看到) 
验证码 *  看不清,换一张 [登录][注册]
内容(请不要发表任何与政治相关的内容)  
  登录  使用高级评论  新用户注册  返回页首  恢复上次提交      
该文被作者在 2007-11-23 22:33 编辑过
"五向定位"职业成长路线公开课(上海、南京、大连)
Google站内搜索


相关链接: