王垠:不再推薦 Haskell

0 前言

在看論文的時候,發現用了Haskell,本來想溫故一下,發現有篇文章寫的不錯,轉過來,大家分享一下。以下是原文。

在之前的一篇博文裏,我推薦從函數式語言入手掌握程序語言。推薦的兩種語言是 Scheme 和 Haskell。可是出於多種原因,我必須告訴大家,我已經不再推薦 Haskell。這裏的原因比較深入,可能不容易說清楚,所以只簡述一下。如果有異議的話,可以來信跟我討論,這樣也可以幫我理清思路。

先說說之前推薦 Haskell 的原因吧。推薦它其實是因爲是它的類型關係較 Scheme 清晰,並且有模式匹配等方便的功能。可是類型系統和模式匹配,卻不是 Haskell 所專有的。其它的一些語言,比如 OCaml 和 Racket 也有很方便的模式匹配和能力相近的類型系統。

現在停止推薦 Haskell,其實是出於很多原因的積累:

1. 類型系統過於複雜

最開頭的時候,Haskell 使用的是普通的 Hindley-Milner 類型系統(HM 系統)。使用這種類型系統的原因是因爲程序員不需要寫任何類型標記(typeannotation)就可以“靜態”的確保類型的正確。可是這樣做的代價是,這個類型系統表達能力太弱。很多程序需要拐彎抹角的繞過這個類型系統的種種限制才寫得出來。比如,Haskell 的 sum type 導致 constructor 的非常麻煩的多重嵌套,這我已經在一篇英文博文裏面比較隱晦的批評了一下。顯然 HM 系統靈活性太差,所以 Haskell 內部後來引進了 SystemFw。可是這些系統發展了好多年,還是不能解決問題。到現在,你仍然會在 Haskell 裏面遇到莫名其妙的限制。你覺得程序應該編譯通過,可是它就是編譯不過(比如我這篇英文博客所述)。究其原因,其實是類型系統有問題,而不是程序員的思路有問題。

有的 Haskell 程序員可能會反駁,說是因爲我不能理解 Haskell 的類型系統。那麼我可以告訴你,我不但實現了 Haskell 和 ML 所用的 HM 系統,而且實現了比 HM 還要強大的 MLF, intersection type 等類型系統。Haskell 推導不出來的類型,我的系統可以推導出來。所以我說的話其實是出自第一手的依據。

2. 參數和返回值的類型標記很有必要

與 Haskell 同門的 SML 和 OCaml 的類型系統也有類似的問題,甚至更加嚴重(比如 ML 有 value restriction,導致不必要的約束和困惑)。但是很多“常規語言”,特別是像 Java,C++ 等需要類型標記的語言,卻沒有這個問題。很多人喜歡 Haskell 都是因爲用它可以“不寫類型標記”,可是現在呢,最好的 Haskell 程序員都是先把類型寫下來,纔開始寫函數。一來這樣思路清晰,你知道這函數要處理哪些類型的數據,你就明確的把它寫下來,以後再來看,或者給其他人看,都一目瞭然。二來是因爲 Haskell 的類型系統由於加入的一些“不可判定”(undecidable)的擴展功能,有時候已經無法推導出類型了。而給函數的參數和返回值加上類型標記之後,就可以輕鬆推導出類型。所以你看到,給參數和返回值加上類型標記,不管是對人還是對機器,都有好處。所以經過我一學期的研究得出的結論是,HM 系統的類型推導,其實是多此一舉。

不過需要注意的是,函數的局部變量,其實是不需要類型標記的。比如在 Java 程序裏常見的:

List ls = newArrayList();

這樣的賦值語句,其實是沒必要在左邊加一個類型標記的,因爲右邊的類型我們知道。在這一點上C++11 的 “auto” 是一個正確的方向。比如在 C++11 裏,你可以寫:

auto ls =newArrayList();

這種類型推導不難做,基本就是一個抽象解釋器。我給 Python 做的 PySonar 類型推導系統裏面就實現了這樣的功能。

對任何語言,具體是哪些地方有必要加上類型標記呢?其實有一個很簡單的方法來判斷:觀察信息進出函數的“接口”,把這些接口都做上標記。直觀一點說,函數就像是一個電路模塊,只要我們知道輸入和輸出是什麼,那麼中間的導線裏面是什麼,我們其實都可以推出來。類型推導的過程,就像是模擬這個電路的運行。這裏函數的輸入就是參數,輸出就是返回值,所以基本上把這兩者加上類型標記,裏面的局部變量的類型都可以推出來。另外需要注意的是,如果函數使用了全局變量,那麼全局變量就是函數的一個“隱性”的輸入,所以如果程序有全局變量,都需要加上類型標記。

3. “純函數式”並不是好主意

我最近常常跟同學開玩笑,說“純函數式”語言是什麼意思。“純函數式”語言是用來描述這樣一個世界的,在這個世界裏,所有的東西都是“有線”的(wired)。不存在 3G,4G,不存在 wifi,收音機,衛星電視…… 所謂的 monads,其實就是這個佈滿電纜的世界裏的“接線盒”。

Haskell 編程之麻煩,就是因爲這些電纜。你必須小心翼翼的把它們接在一起,安排好,否則就會有各種問題,甚至絆到腳。連生成隨機數這麼簡單的事情,你都得學會使用各種各樣的“隨機數 monads”。這是因爲我們需要記錄隨機數發生器的“狀態”,所以隨機數 monad 輸入一個隨機數發生器,返回一個隨機數以及一個新的隨機數發生器!想一想,在 C 語言裏面,你只需要一個全局變量或者函數內部的 static 變量來記錄隨機數發生器的狀態。到底是誰簡單,誰複雜?我想你可能已經意識到,全局變量其實就是 wifi!

Haskell 的支持者常說,純函數的語言容易“推理”,容易確保程序的正確。因爲它的程序就像“數學的函數”,給同一個輸入,就會得到同一個輸出。這叫做“referentialtransparency”。可是這種性質,真的可以讓程序容易“推理”嗎?如果 Haskell 的函數使用了 monads,比如“狀態”(statemonad),那麼這個函數的“輸入”,就幾乎永遠不會相同。因爲那個狀態每次都可能變化,所以你實際上還是沒法知道那裏面是什麼!

記住這一點:世界上沒有包治百病的神藥。

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

關於惰性求值,我基本同意 Robert Harper 的觀點。惰性求值讓類型變得混亂,讓程序的時間和空間複雜度難以分析,而且跟並行計算的原則有根本性的矛盾。而惰性求值的功能,卻不是經常有用的。即使需要,在普通的語言裏也可以通過 thunk 來實現。所以,惰性求值帶來的問題恐怕比它解決的問題還要多。

很多自稱“從 Haskell 衍生”的語言,很多其實都只是有其形,而無其實。一個例子就是 Bluespec,一種硬件描述語言。它雖然自稱是從 Haskell 演變來的,看起來像 Haskell,但是它卻不是惰性的,類型系統也很簡單,所以基本上它已經不是 Haskell。打着 Haskell 的旗號,恐怕是想借助 Haskell 的名聲來擡高自己,或者是因爲 Bluespec 的創造者 Lennart Augustsson 最早的時候是 Haskell 的主要發起人之一。

5. 思想侷限

所以綜上所述,Haskell 自稱的“特性”幾乎被實踐一一推翻。可是 Haskell 程序員往往炫耀自己的“函數式編程”水平,其實經常陷入一些很難理解的“設計模式”,無法自拔。鑑於這個原因,我停止向大家推薦 Haskell。

另外需要申明一下的是,我停止推薦 Haskell 並不是因爲我想力推 Scheme。實際上 Scheme 也有自己的問題,但是相對來說,它更加簡單易懂,符合學習的需要。另外,以前對 C 和 C++ 的批評也許過於偏激。最近爲了在 LLVM 上做一些事情,開始重新理解C++,發現它做的好些事情其實是挺不錯的,甚至超過好些最炫的,帶有“dependenttype”的函數式語言。所以現在我覺得,其實世界上的語言並沒有什麼絕對的標準。在一段時間認爲是錯的東西,可能卻是對的。所以不用盲目的排斥一些語言,把它們都拿來看一下,互相對比,纔會知道到底什麼是好的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章