文/allenlooplee 出处/博客园
缘起 当你看到这篇文章的标题时,你有什么感觉?是不是很想脱口而出:"到底搞什么飞机啊,我C#还没来得及用好,现在又搞个F#,还让不让人活啊?"《程序员修炼之道》曾经建议我们"learn at least one new language every year",但
Gustavo Duarte却对这种建议提出质疑,并宣称"learning new programming languages is often a waste of time for professional programmers"。面对这种争论,你可能会显示出某种理性:除非我有需要(学习新的语言),否则我认为够用就可以了。那么,你什么时候会有需要?回想一下你的项目经历,是否发现,有权提出这种需要的往往不是你,而是你的项目,只要项目有需要,即使是老掉牙的语言你也得学。根据马斯洛的需要层次理论,如果你的项目已经让你忙得一塌糊涂了,那么你根本不会有闲情和兴致学习新的语言,而在现今这个讲求快速见效的社会里,或许只有专门研究语言的人才支付得起学习新的语言的代价了,但我并非专门研究语言的人,至少现在不是,那么,我为何要学习新的语言呢?
我曾经在杰拉尔德·温伯格(Gerald M. Weinberg)的《咨询的奥秘——成功提出和获得建议的指南》里读到一个有趣的"锤子法则"(The Law of The Hammer):
在圣诞节收到锤子做礼物的孩子会发现每样东西都需要敲打。 读完上面这句话之后,你的脑子里想着什么?或许你已经猜到我想说什么了,工具在为使用者带来便利的同时也会约束使用者解决问题的思路和方法,编程语言直接体现了对问题的抽象和表达,不同范式的编程语言则协助程序员从不同的角度把握问题,这也正是我学习新的语言的主要原因。要有持久的学习行为,学习动机应该是指向内部的,其道理和
《七个心理寓言》的"动机的寓言:孩子在为谁而玩"所说的是一样的。那么,我又为何选择F#呢?其实,这完全是因为C# 3.0,我们知道C# 3.0向函数式编程借鉴了不少,所以在学习C# 3.0的时候,我突然萌生了想了解函数式编程语言的念头,后来,在一次偶遇中,我邂逅了F#。在学习F#的过程中,我发现许多C# 3.0的新功能的"影子"(有人说C# 3.0的新功能是从F#那里借鉴过来的,是真的吗?),于是萌生了写下这篇文章的念头。
如何创建类型? 人们在接触新事物时通常不会抛开现有的积累,换句话说,你的知识和经验会影响你如何接受新事物。如果你是一个有使用面向对象编程语言经验的程序员,那么你第一个想问的问题很可能就是:"我如何创建类型?"
F#支持一种叫做Record Type的类型,它和C#里使用自动属性定义的类有点像:

附件:
您所在的用户组无法下载或查看附件 代码 1 而Book的实例化也是非常直观的:

附件:
您所在的用户组无法下载或查看附件 代码 2 F#会根据给出的属性名字以及值的类型推断出你要实例化的类型是Book。读到这里,你可能会问:"如果有两个不同的类型定义了相同的属性呢?"虽然出现这种情况的概率不大,但若真的让你碰上了,你可以使用显式语法来实例化它:

附件:
您所在的用户组无法下载或查看附件 代码 3 面向对象编程的一个特征是封装,狭义的封装是指封装对象的内部状态(广义的封装则是指封装系统的变化因素),而对象的内部状态在对象的生命周期里发生改变是很常见的,但当我们试图在F# Interactive(类似于Python的交互式控制台)里修改Price属性时却报告错误("<-"用于赋值,相当于C#的"="):

附件:
您所在的用户组无法下载或查看附件 图 1 为什么会这样呢?原来,在F#里,对象默认是不可变的(immutable),就像.NET的字符串那样。修改Price属性可以看作创建一个新对象,把原对象的Title、Authors和Tags属性的值复制到新对象对应的属性,并为新对象的Price属性设置新的值:

附件:
您所在的用户组无法下载或查看附件 代码 4 读到这里,你可能在想:虽然现在内存很便宜了,但也不至于要用这种方法来耗啊?在面向对象编程里,拥有和维护可变的内部状态是对象的一个很重要的特征,正因为这样我们得以完成许多复杂的操作,但也正是可变的内部状态提高了并发操作的复杂程度和处理代价。泛泛而谈可变对象和不可变对象孰优孰劣是没有意义的,对于一个给定的系统,一些对象适合设计成可变的,另一些则应该考虑设计成不可变的,从而使两者达到一定的平衡。
在C#里,对象默认是可变的,但你可以通过readonly关键字使某个(些)数据"固定"下来;F#刚好相反,对象默认是不可变的,但你可以通过mutable关键字使某个(些)数据"活动"起来。如果我把Book重新定义为:

附件:
您所在的用户组无法下载或查看附件 代码 5 那么修改Price属性就不会报错了。如果你决定使对象可变,那么你就应该做好并发处理的工作。什么?你的程序是单机单核单线程的?那你可以掷硬币决定对象是可变的还是不可变的。
除了Record Type,F#还支持Discriminated Union、Tuple和Constructed Class Type,有兴趣的话不妨到
F# Home看看。
如何初始化对象? C# 3.0引入了对象初始化器和集合初始化器,F#也提供了类似的功能。举个例子,假设我想初始化System.Windows.Forms.ListViewItem,并且设置它的Text、Selected和ToolTipText属性,我可以这样:

附件:
您所在的用户组无法下载或查看附件 代码 6 F#把这个功能叫做初始属性设置(initial property settings)或者可选属性设置(optional property settings)。上面代码等效于:

附件:
您所在的用户组无法下载或查看附件 代码 7 从这里可以看出,使用这个功能的前提条件是要初始化的属性必须具有set访问器。读到这里,你可能会问:"如果我要调用的构造函数是有参数的呢?"那也没问题,举个简单的例子,假设你要调用接受一个字符串作为参数的那个构造函数,你可以这样:

附件:
您所在的用户组无法下载或查看附件 代码 8 或者这样:

附件:
您所在的用户组无法下载或查看附件代码 9 第一种方法就是简单地把你要初始化的属性追加到构造函数的参数后面;而第二种方法则使用了F#的命名参数(Named Argument)功能。命名参数可以放在任何位置,例如Selected和ToolTipText之间,但匿名参数就必须按顺序放在要初始化的属性前面。读到这里,你可能会问:"如果我要调用的构造函数的参数和我要初始化的属性重名了呢?"你在考验F#的忍耐力吗(笑)?当然,这种情况是有可能出现,首先,F# 不允许同一个名字出现两次,不管它是构造函数的参数的名字还是属性的名字,所以你不可能鱼与熊掌兼得(即构造函数的参数和属性同时初始化);其次,一旦出现这种情况了,F#会优先考虑构造函数的参数。
我们探讨了如何初始化一个对象,那么初始化一组对象又是怎样的呢?在F#里,说到集合类型就不得不提Microsoft.FSharp.Collections.List<'a>了("'a"是F#的类型参数表示法)。假设我要实例化一组Book对象(Book的定义参见代码1),并把它们储存在List<'a>里,我可以这样:

附件:
您所在的用户组无法下载或查看附件 代码 10 列表里的每个元素通过";"分割。F#能够结合元素的类型推断出列表的完整类型,在这里是List<Book>(也可以表示为Book list)。F#的List<'a>通常只在F#里使用,如果要访问.NET的类库或者和其他语言交互,那么你通常会考虑使用数组(F#的List<'a>和数组的语法非常接近,能看出其中的区别吗?):

附件:
您所在的用户组无法下载或查看附件 代码 11 而对于整数列表,F#还支持区间表达式,你可以指定起始值和终止值:

附件:
您所在的用户组无法下载或查看附件 图 2 甚至指定递增值(步长):

附件:
您所在的用户组无法下载或查看附件 图 3 如果你有兴趣进一步了解F#的List<'a>,可以阅读
Dustin Campbell的
《Why I Love F#: Lists - The Basics》和
Chris Smith的
《Mastering F# Lists》。
如何外包逻辑? 有一次,我和两个朋友到东方既白吃饭,选餐的时候,其中一个考虑了很久,终于发话了:"椰香咖喱牛肉饭可不可以不要椰香?"服务员看着我的朋友,非常不好意思地说:"这是不可以的。"看到服务员的表情,我猜她应该是苦于不知如何向一个12岁的小朋友解释"烧饭的工作遵循了一套标准化的流程,这个流程是不能随意更改的"。此时,我的朋友大概在想:同样的钱,不能加东西可以理解,为什么连减东西也不可以呢?虽然他最后还是选了椰香咖喱牛肉饭,但我猜他心里肯定觉得东方既白做得太呆板了。试想一下,如果你打算使用我提供的Sort方法排序books2数组(参见代码11),却发现这个方法只接受一个数组作为参数,你肯定会问:"我如何告诉这个方法我要根据价格进行排序?"接着,我告诉你:"不好意思,这是不可以的,这个方法会自行选择合适的排序依据。"此时,你会有什么感觉?
无可否认,我们已经进入了一个个性化的时代,用户不再像从前那样满足于你所提供的普遍适用的标准化软件,他们希望你的软件是可配置的,必要时还能够扩展,也就是可以满足他们的个性化需求。然而,把逻辑外包出去并不只是为了满足用户的个性化需求,为什么这样说?试想一下,你可不可以写出这样一个Sort方法,每次调用时都能"猜中"用户的排序依据?很明显,当我把books2数组传给Sort方法时,如果我不说,它不可能知道我想按书名排序还是按价格排序,是升序还是降序。换句话说,把逻辑外包出去其实就是把这种不稳定的因素封装起来,再转嫁给用户,然后美其名曰"用户参与",当然,由于用户认为你不是把麻烦抛给他,而是为他带来灵活性,于是造就了"双赢"。
考察System.Array.Sort方法的众多重载版本,不难发现.NET外包逻辑的两种主要方式是:委托和接口。在F#里,我们可以通过Lambda表达式向接受委托作为参数的重载版本注入逻辑(
compare函数是F#提供的通用比较函数):

附件:
您所在的用户组无法下载或查看附件代码 12 当然,使用命名函数注入逻辑也是可以的:

附件:
您所在的用户组无法下载或查看附件 代码 13 代码13除了向我们示范如何在F#里定义函数,还向我们展示了一个有趣的东西,留意comparePrice函数的定义,我并没有为x和y这两个参数指定类型,但F#却从函数体以及上下文推断出它们的类型是Book!
另外,F#并没有刻意区分命名函数和Lambda表达式,代码13的comparePrice函数也可以这样定义:

附件:
您所在的用户组无法下载或查看附件 代码 14 代码13和代码14定义的两个comparePrice函数是等效的,使用上也没有区别,从代码14可以看出,在F#里,函数其实就是值,而我们在代码12里使用的Lambda表达式只不过是代码14定义的comparePrice函数的函数体。
Lambda表达式使你能够以一种紧凑的方式注入逻辑,但如果别人外包逻辑的方式是接口而不是委托呢?这个时候就轮到
对象表达式(Object Expression)出场了:

附件:
您所在的用户组无法下载或查看附件 代码 15 在这里,我通过"_"告诉F#我希望它帮我推断IComparer<'a>的类型变量,而F#也不负所托,成功推断出它的类型是Book。F#的对象表达式也算是一种匿名类型,但它和C# 3.0的匿名类型是不同的。在F#的对象表达式里,你可以实现接口的成员或者重写基类的成员,但不能添加任何新的成员;而C# 3.0的匿名类型则只允许属性的存在。
如何扩展类型? 假设我要把一组Book对象添加到System.Windows.Forms.ListView上,我应该怎样?对于习惯运用命令式编程方式思考问题的人,他可能会首先想到创建一个ToListViewItem函数:

附件:
您所在的用户组无法下载或查看附件 代码 16 然后"foreach"那组Book对象,对每个Book对象应用ToListViewItem函数,并把函数返回的结果添加到ListView。由于ToListViewItem函数是一个和Book对象相关的操作,你也可能会考虑把它纳入Book类型的定义,使它变成Book类型的实例成员函数:

附件:
您所在的用户组无法下载或查看附件 代码 17 您可能对 [F#] 的这些文章也感兴趣: