首页 > 代码库 > F# 的血继限界(1)

F# 的血继限界(1)

       如我们所见,在 F# 的产生和发展过程中,给 C# 引进了很多函数式的革新理念。然而这些革新仍未足以让 C# 进行完全函数式编程。C# 进行函数式编程时存在如下问题:

1、C# 中不能声明“
局部不可变,不能处理尾递归优化。
        很遗憾,不可变量和尾递归,可以说是函数式语言的绝对标识,至于有什么好处就不赘言。 而且这两点在C++都支持,把它们放在一起说也就是因为这个原因,它们可以算是 C# 相对 C++ ,进化不完全的“罪证”。 C++中的 const 关键字,在C#里分成 const 和 readonly 两个,前者代表常量,而后者代表不可变,可惜 readonly 只能用来标识字段。替换方案:
        a、使用单元素 Tuple。.net 中支持只有一个元素的 
Tuple,这下可派上用场了。但是不推荐,因为 Tuple 在.net 里面是 Object,每个变量开一个 Tuple 开销大了点,如果使用多元素 Tuple,却无法使用自定义的变量名,反而增加阅读代码的难度。另外,Tuple 只能保证 Tuple 里面的成员值不变,Tuple 还是可以整个被替换掉这点倒可以忽略,这种替换稍嫌麻烦,我们只要达到变量不会被轻易误修改的目的就行。
 
        b、 使用匿名类型,例如:var locals = new { Value = http://www.mamicode.com/10 };
                这种方法比用 Tuple 好一点。如果多个变量一起放在大括号里,可以保证只读性,也没有太大开销。
                只是代码需要相当多的调整。会不会因此反而丢失可读性,看施主的因缘际遇了。

        c、最实用的方法,也是没有办法的办法:放弃不可变的追求,把函数缩短,尽量写短的函数,增强可读性,方便重构,好处多多。

        至于尾递归的替换方案,无非就是少用递归,多用循环,也没什么好的建议了。


2、C# 中缺乏元组、记录( record )。

        元组其实很像 C 里面的 struct,只是它不保证内存是连续的。F# 语法上表示的元组,实际上传递的是松散值,
只有在对外接口时,它被才被编译成 Tuple 对象。所以元组在 F# 中被广泛应用,而不影响程序性能。C# 中不能表达元组的概念,如果不想每用一个元组就声明一个struct,也可以用 Tuple 对象来表示,只是有一定额外开销,对于通常只有两三个值的元组来说,Tuple 太不轻便了。而且它的元素只能用 Item1,Item2 ... 来表示,很不直观。Tuple 刚推出来的时候,不少文章说可以用 Tuple 来替代 out, 但是这种意见很快就被反对的声音淹没了,毕竟不是同一个重量级的东西啊。

        record 是个轻量级的类,某 MVP 说,它就是个带标签的元组。但是我们看到,
record 是对象 ,而不是值类型。
        record 某种程度上也很像 C 里面的 struct,相对元组来说,它继承的是 C struct 的另一些特性:
        第一,它的成员带名字标签。C struct 是 C++ class之祖,希望不要介意我这样说。 
        
第二,它在 F# 内部可以当接口使用。因为 record 的属性可以是函数类型,C 的 struct 也可以包含函数指针。所以它们都常常用来做简单的接口。
        
而 record 第三个跟 
C stuct 相像的特性,在函数式编程里面尤其重要: 它没有构造函数,不可继承,它的属性一旦声明就会公开它是如此简单的一个类,只要检查 record, 看看它的属性是否可变,就可以知道它是不是一个不可变的类。我们可以把不可变的 record 当成一个值来优化(类似不可变的 string )。
 

3、 C# 声明序列的方式很单调。
 

        函数式编程的一个重要特征,就是延迟式计算,而序列是延迟计算的一个常用方式,
        譬如,我要对一个大范围的 IP 地址发查询码,直至有一个 IP 有回应为止,我无须一开始就生成大波 IP 地址,只须写一个 IP 地址枚举器(也就是序列),每发一个码,生成一个新的 IP 即可,(仅作例子,不谈实际操作)。
        在 F# 中,到处都可以使用 seq { ... } 的方式声明一个序列(F# 的大括号系列有很多方便的应用,以后会一一提到)。
        C# 只有两种方式产生序列的方式:
            a、可以通过 Linq.Enumerable,通常你必须先有另一个序列,不是通用方式。
            b、写一个返回 IEnumerable 接口的函数,是通用方式,但是不方便。另外,匿名函数里面不支持 yield return。增加了使用 
IEnumerable 的死板系数。基本上,你要用 IEnumerable 接口,你就只能在你当前的函数外面另外写一个函数,因此,你就不能做闭包采集,有时不得不写出一大串调用参数。

 
 

 

F# 的血继限界(1)