与fold类似的白象,还有currying,Hindley-Milner类型推导等特性。看似很酷,但等你仔细推敲才发现,它们带来的麻烦,比它们解决的问题其实还要多。有些特性声称解决的问题,其实根本就不存在。现在我把一些函数式语言的特性,以及它们包含的陷阱简要列举一下:
- fold。fold等“递归模板”,相当于把递归函数定义插入到调用的敌方,而不给它们名字。这样导致每次读代码都需要理解几乎整个递归函数的定义。
- currying。貌似很酷,可是被部分调用的参数只能从左到右,依次进行。如何安排参数的顺序成了问题。大部分时候还不如直接制造一个新的lambda,在内部调用旧的函数,这样可以任意的安排参数顺序。
- Hindley-Milner类型推导。为了避免写参数和返回值的类型,结果给程序员写代码增加了很多的限制。为了让类型推导引擎开心,导致了很多完全合法合理优雅的代码无法写出来。其实还不如直接要程序员写出参数和返回值的类型,这工作量真的不多,而且可以准确的帮助阅读者理解参数的范围。HM类型推导的根本问题其实在于它使用unification算法。Unification其实只能表示数学里的“等价关系”(equivalence relation),而程序语言最重要的关系,subtyping,并不是一个等价关系,因为它不具有对称性(symmetry)。
- 代数数据类型(algebraic data type)。所谓“代数数据类型”,其实并不如普通的类型系统(比如Java的)通用。很多代数数据类型系统具有所谓sum type,这种类型其实带来过多的类型嵌套,不如通用的union type。盲目崇拜代数数据类型的人,往往是因为盲目的相信“数学是优美的语言”。而其实事实是,数学是一种历史遗留的,毛病很多的语言。数学的语言根本没有经过系统的,全球协作的设计。往往是数学家在黑板上随便写个符号,说这个表示XX概念,然后就定下来了。
- Tuple。有代数数据类型的的语言里面经常有一种构造叫做Tuple,比如Haskell里面可以写(1, "hello"),表示一个类型为(Int, String)的结构。这种构造经常被人看得过于高尚,以至于用在超越它能力的地方。其实Tuple就是一个没有名字的结构(类似C的structure),而且结构里面的域也没有名字。临时使用Tuple貌似很方便,因为不需要定义一个结构类型。然而因为Tuple没有名字,而且里面的域没法用名字访问,一旦里面的数据多一点就发现很麻烦了。Tuple往往只能通过模式匹配来获得里面的域,一旦你增加了新的域进去,所有含有这个Tuple的模式匹配代码都需要改。所以Tuple一般只能用在大小不超过3的情况下,而且必须确信以后不会增加新的域进去。
- 惰性求值(lazy evaluation)。貌似数学上很优雅,但其实有严重的逻辑漏洞。因为bottom(死循环)成为了任何类型的一个元素,所以取每一个值,都可能导致死循环。同时导致代码性能难以预测,因为求值太懒,所以可能临时抱佛脚做太多工作,而平时浪费CPU的时间。由于到需要的时候才求值,所以在有多个处理器的时候无法有效地利用它们的计算能力。
- 尾递归。大部分尾递归都相当于循环语句,然而却不像循环语句一样具有一目了然的意图。你需要仔细看代码的各个分支的返回条件,判断是否有分支是尾递归,然后才能判断这代码是个循环。而循环语句从关键字(for,while)就知道是一个循环。所以等价于循环的尾递归,其实最好还是写成特殊的循环语句。当然,尾递归在另一些情况下是有用的,这些情况不等价于循环。在这种情况下使用循环,经常需要复杂的break或者continue条件,导致循环不易理解。所以循环和尾递归,其实都是有必要的。