为什么 Haskell 不适合作为编程入门语言

2012-09-14 17:21

为什么 Haskell 不适合作为编程入门语言

by 王垠

at 2012-09-14 09:21:29

original http://www.udpwork.com/item/8113.html

在之前的一篇博文里,我推荐从函数式语言入手掌握程序语言。推荐的两种语言是 Scheme 和 Haskell。可是出于多种原因,我觉得 Haskell 其实不适合作为编程的入门语言。当然我说的是针对学习而言,如果是因为工作需要的话,那就另当别论了 


这里的原因比较深入,可能不容易说清楚,所以只简述一下。如果有异议的话可以来信跟我讨论,这样也可以帮我理清思路。先说说之前推荐 Haskell 的原因吧。推荐它其实是因为是它的类型关系较 Scheme 清晰,并且有模式匹配等方便的功能。可是类型系统和模式匹配,却不是 Haskell 所专有的。其它的一些语言,比如 OCaml 和 Racket,也有很方便的模式匹配和能力相近的类型系统。

现在停止推荐 Haskell,其实是出于很多原因的积累:


1. 类型系统过于复杂

类型系统的作用应该是辅助程序员减少不必要的错误,但是如果类型系统成为了程序员思维和建模的障碍,那么还不如不要。

最开头的时候,Haskell 使用的是普通的 Hindley-Milner 类型系统(HM 系统)。使用这种类型系统的原因是因为程序员不需要写任何类型标记(type annotation)就可以“静态”的确保类型的正确。可是这样做的代价是,这个类型系统的表达能力太弱。很多程序需要拐弯抹角的绕过这个类型系统的种种限制才写得出来。比如,Haskell 的 sum type 导致 constructor 的非常麻烦的多重嵌套,这我已经在一篇英文博文里面比较隐晦的批评了一下。显然 HM 系统灵活性太差,所以 Haskell 内部后来引进了 System Fw。可是这些系统发展了好多年,还是不能解决问题。到现在,你仍然会在 Haskell 里面遇到莫名其妙的限制。你觉得程序应该编译通过,可是它就是编译不过(比如我这篇英文博客所述)。究其原因,其实是类型系统有问题,而不是程序员的思路有问题。

有的 Haskell 程序员可能会反驳,说我不理解 Haskell 的类型系统。其实,我不但亲自实现了 Haskell 所用的 HM 系统和 type class,而且实现了比 HM 还要强大的 MLF, intersection type 等类型系统。Haskell 推导不出来的一些类型,我的系统可以推导出来(比如上面那个例子)。所以我说的话其实是出自第一手依据的 


2. 参数和返回值的类型标记很有必要

但是我并不是想要贬低 Haskell 而推广我的这个类型推导系统。实际上,虽然我的系统比 Haskell 的强大而且简单,但是它其实也是在做一些无用功。这是因为,如果把我们的“设计”和“期望”稍微改变一下,要求程序员写出参数和返回值的类型标记,那么这些问题很大部分就消失了。

与 Haskell 同门的 SML 和 OCaml 的类型系统也有类似的问题,甚至更加严重。比如 ML 有 value restriction,导致不必要的约束和困惑。但是很多“常规语言”,特别是像 Java, C++ 等需要类型标记的语言,却没有这个问题。很多人喜欢 Haskell 是因为用它可以“不写类型标记”,可是现在呢,最好的 Haskell 程序员都是先把类型写下来,才开始写函数。一来这样思路清晰,你知道这函数要处理哪些类型的数据,你就明确的把它写下来。以后再来看,或者给其他人看,都一目了然。二来是因为 Haskell 的类型系统由于加入的一些“不可判定”(undecidable)的扩展功能,有时候已经无法推导出类型了。而给函数的参数和返回值加上类型标记之后,就可以轻松推导出类型。

不过需要注意的是,函数的局部变量,其实是不需要类型标记的。比如在 Java 程序里常见的:

  List<String> ls = new ArrayList<String>();

这样的赋值语句,其实是没必要在左边加一个类型标记的,因为右边的类型我们已经知道。在这一点上 C++11 的 "auto" 是一个不错的方向。比如在 C++11 里,你可以写:

  auto ls = new ArrayList<String>();

对任何语言,具体是哪些地方有必要加上类型标记呢?其实有一个很简单的方法来判断:观察信息进出函数的“必经之路”,把这些端口都做上标记。直观一点说,函数就像是一个电路模块,只要我们知道输入和输出是什么,那么中间的线路里面是什么,我们其实都可以推出来。类型推导的过程,就像是模拟这个电路的运行。这里函数的输入就是参数,输出就是返回值,所以基本上把这两者加上类型标记,里面的局部变量的类型都可以推出来。

需要注意的是,你必须观察到所有的“信息通路”,包括隐性的通道。这种隐性通道通常是由“副作用”(side-effect)产生的。比如如果函数使用了全局变量,那么全局变量就是函数的一个“隐性”的输入输出端口,所以如果程序有全局变量,就需要在它定义的地方加上类型标记。忽视这些隐性通道,是造成很多类型系统不安全(unsound)的主要原因。历史上 ML 的类型系统就发现过几次这样的现象。

所以你看到,其实真正需要写类型的地方是很少的。给参数和返回值加上类型标记,不管是对人还是对编译器都有好处,也不费很多工夫。所以经过我一学期的研究得出的结论是,HM 系统所实现的“不需要写类型”的类型推导,其实是多此一举,造成了很多不必要的问题。


3. “纯函数式”并不是好主意

我最近常常跟同学开玩笑,说“纯函数式”语言是什么意思。“纯函数式”语言是用来描述这样一个世界的,在这个世界里,所有的东西都是“有线”的(wired)。不存在收音机,卫星电视,手机,无线网…… 所谓的 monads,其实就是这个布满电缆的世界里的“接线盒”。

Haskell 编程之麻烦,就是因为这些电缆。你必须小心翼翼的把它们拼接在一起,安排好,否则就会有各种问题,甚至绊到脚。连生成随机数这么简单的事情,你都得使用“随机数 monad”。这是因为我们需要记录随机数发生器的“状态”,所以随机数 monad 输入一个随机数发生器,返回一个随机数以及一个“新的随机数发生器”。在任何使用随机数的地方,你都需要把这些发生器通过参数和返回值传递下去,而不能说把它放在某个众所周知的地方,到时候去拿就是了。想一想,在 C 语言里面,你只需要一个全局变量或者函数内部的 static 变量来记录随机数发生器的状态。我想你可能已经意识到,全局变量其实就是“无线网”。如果你没有无线网,当然就会跟很多网线纠结。Haskell 盲目的排斥这种“副作用”,实际上使得对副作用的处理变得复杂,碍手碍脚。

Haskell 的支持者常说,纯函数的语言容易“推理”,容易确保程序的正确。因为它的程序就像“数学的函数”,给同一个输入,就会得到同一个输出。这叫做“referential transparency”。可是这种性质,真的可以让程序容易推理吗?我没有看到任何证据支持这个说法。如果 Haskell 的函数使用了 monads,比如“状态”(state monad),那么这个函数的“输入”,就包含这个状态。因为状态每次调用都可能不同,所以你实际上没法知道那里面是什么,也就没法对这个函数进行推理。所以 Haskell 比起有“副作用”的语言,在可推理性上几乎没有任何优势。

记住这一点:世界上没有包治百病的神药。


4. 惰性求值(lazy evaluation)不是好主意

关于惰性求值,我基本同意 Robert Harper 的观点。惰性求值让类型变得混乱,让程序的时间和空间复杂度难以分析,而且跟并行计算的原则有根本性的矛盾。而惰性求值却不是经常有用的。即使需要,在普通的语言里也可以通过 thunk 来实现。所以,惰性求值带来的问题恐怕比它解决的问题还要多。

很多自称“从 Haskell 衍生”的语言,其实都只是有其“形”而无其“实”,所以它们有可能不受以上这些问题的限制。一个例子就是 Bluespec,一种硬件描述语言。它虽然自称是从 Haskell 演变来的,看起来像 Haskell,但是它却不是惰性的,类型系统也很简单,所以基本上它已经不是 Haskell。


5. 语法设计的弊端

语法貌似一个次要问题,但是其实对程序员的影响非常大。因为语法是程序语言与程序员直接的界面,直接影响到程序的编辑的方便性和美观程度。

在我的这篇英文博文里我已经指出,Haskell(以及 Python)使用“缩进”(indentation)来标注语句的“block”,其实是得不偿失的。第一,这样的程序容易因为缩进的变化(比如不小心按了 TAB 键,或者拷贝粘贴代码)而产生难以察觉的错误。其次,这样的语言完全不可能在编辑器里进行自动缩进。原因很简单,因为不同的缩进有可能代表着不同的程序,所以除非编辑器知道你在“想”什么,否则就没有可能实现自动缩进。

我的实际经验显示,Haskell 的程序由于“缩进语法”和其它的一些语法设计,明显比相应的 Scheme 程序难看很多。比如,如果你想把复杂的 case 表达式按 "->" 对齐,你就会发现有些行几乎是空的,而有些却很饱满,看不清楚。但是如果你不对齐这些箭头,你就分不清哪些是模式,哪些是结果。所以进退两难。而且因为它大量使用中缀表达式,以及“尽量不用括号”的设计倾向,导致阅读 Haskell 程序的时候进行“mental parsing”所耗费的精力,比其它好些语言(C, C++, Java, Scheme)都要大。有时候我简直觉得 Haskell 程序是 optical illusion,看着费神。对于编译器的作者,Haskell 的语法就是一个噩梦,因为它很难 parse!

这样的语法到底是在解决问题,还是在制造问题?


6. 设计模式

所以我觉得,Haskell 的几乎每一个特性都被实践所质疑。可是 Haskell 还有一个很不好的地方,就是它鼓励程序员使用晦涩的“设计模式”。大家都知道 OOP 的设计模式带来了过度的工程(over-engineering),可是其实我觉得 Haskell 也差不多。很多人都热衷于 monads, point-free style, 过度使用像 zip, fold 一类的东西,使用 monoid, applicative functor, *morphism 一类的晦涩术语。本来很简单的事情,怎么一到 Haskell 就换了一些名字。其实这些都属于“设计模式”,它们不但让程序难看明白,而且在效率上很难说清楚。

由于以上所有这些原因,我停止向大家推荐 Haskell 作为编程的入门语言。不过需要申明一下的是,我停止推荐 Haskell 并不是因为我想力推 Scheme。实际上 Scheme 也有自己的问题,但是相对来说,它更加直接了当,简单易懂。另外,以前对 C 和 C++ 的批评也许过于偏激。最近为了在 LLVM 上做一些事情,开始重新理解 C++,发现它虽然有些毛病,但是对好些事情的处理,其实是挺简单而且合理的。世界上那么多给我们带来福利的系统,其实都是用 C 或者 C++ 写的,没有很多是用很酷很新的语言,这不得不说明它们具有某种合理性。

所以现在我觉得,其实世界上的语言并没有什么绝对的标准。在一段时间认为是错的东西,过一段时间却可能发现是对的。所以不用盲目的排斥一些语言,把它们都拿来看一下,互相对比,不时的走出自己的圈子,才会知道到底什么是好的。


 青春就应该这样绽放  游戏测试:三国时期谁是你最好的兄弟!!  你不得不信的星座秘密

0     1

IT 牛人博客聚合网站(udpwork.com) 聚合 | 评论: 1 | 10000+ 本编程/Linux PDF/CHM 电子书下载