開拓下眼界
2014-01-01
Bruce A.Tate. 七周七语言[M]. 巨成,戴玮,白明,譯. 人民邮电出版社,2012-5. ISBN 978-7-115-27611-7.
Ruby
----
> Matz: 1993年,當我看到Perl的時候,不知怎麼的,這種混合了Lisp和Smalltalk特徵的面向對象語言讓我的靈感一下子迸發出來。我意識到Perl將成爲一門可提高我們生產力的偉大語言。於是,出於自娛自樂的動機,我着手開發一門與之類似的語言。(p. 9)
這讓我想起鳳姐的「Ruby就是沒有到處打廣告的Perl 6」.
> Matz: 我喜歡它寓編程於樂的方式。說到某個具體的技術點,我最喜歡的是“代碼塊”(block)。代碼塊即是一種易於控制的高階函數,也爲DSL及其他特性的實現提供了極大的靈活性。(p. 9)
這裏Matz說謊了。block很*像*高階函數,但*不是*函數。
```ruby
->(x){x+3}.call 3
```
返回6.
而
```ruby
{|x| x + 3}.call 3
```
將導致`syntax error`.
Ruby的祖先Smalltalk中,`block`倒是可以接受消息的:
```smalltalk
[:x | x + 3] value: 3
```
返回6.
所以Ruby的block是個奇怪的東西。縱向來說,它很像Smalltalk的block,橫向來說,它很像匿名函數。但是事實上,它和兩者都不一樣。Ruby的口號是「Principle of Least Surprise」,但是這個block卻讓我吃驚。
語意上塊讓人迷惑,語法上也不好。塊有兩種表達法:大括號或者do...end,問題在於優先級是不同的。例如`f x {|x| puts x}`和`f x do |x| puts x end`是不一樣的,前者等於`f(x {...})`,後者等於`f(x) do...end`。初學的時候很容易搞混。
Matz在他寫的《まつもとゆきひろコードの世界 : スーパー・プログラマになる14の思考法》一書中提到了設計塊的緣由:
1. 減少對象的生成數,因爲早期Ruby生成閉包對象的代價很高。
2. 外觀上看起來像控制結構。
Matz還提到,傾向於使用高階函數的OCaml的2239個庫函數,沒用函數參數的佔87.2%,用一個函數參數的佔12.1%,也就是有兩個以上的不到1%。因此,大多數情況下,只能有一個參數的塊也夠用了。
所以說,塊就是一個語法糖,讓習慣過程式編程的程序員可以使用類似高階函數的東西(同時讓Smalltalk和Lisp的來客大吃一驚)。
> 代碼塊是沒有名字的函數。(p. 20)
連以Ruby爲主力語言的人也這麼說…… 匿名函數在哭泣:「block算哪門子函數了?人家纔是沒有名字的函數好不好!」
> 使用method_missing (p. 30-31)
舉了一個利用`method_missing`實現的羅馬數字轉換爲阿拉伯數字:
```ruby
class Roman
def self.method_missing name, *args
roman = name.to_s
roman.gsub!("IV", "IIII")
roman.gsub!("IX", "VIIII")
roman.gsub!("XL", "XXXX")
roman.gsub!("XC", "LXXXX")
(roman.count("I") +
roman.count("V") * 5 +
roman.count("X") * 10 +
roman.count("L") * 50 +
roman.count("C") * 100)
end
end
```
作者說這個API很簡單:
> 對比一下`Roman.i`和`Roman.number_for "i"`就能看出來。 (p. 31)
這不是一個好例子。首先,"i"是不行的,因爲代碼邏輯裏沒寫大小寫轉換。其次,如果調用函數的時候使用了`Romain.A`,那又會出現什麼情況?調試起來會很麻煩。相比之下,`Roman.number_for "I"`也很清晰。如果乾脆不使用類, 可以寫成`romanize("I")`,也不錯。
Io
--
Io的語法和語義非常簡約,類似Lisp。創始人解釋了這麼設計的原因。
> Steve: 我鍾愛它簡潔一致的語法和語義。這有助於理解代碼做了些什麼。你可以迅速學會這門語言的基礎。我本人的記性不太好,經常忘記C語言的語法和那些古怪的語義規則,因此不得不去查閱它們。而我在使用Io的時候,就不必老惦記這查閱這種事了。(pp. 48-49)
> Steve:如果充分簡化語義,它就會更具靈活性。你可以就此創作一些實現這門語言時尚未定義的東西。(p. 49)
Prolog
------
作者舉了一個解數獨的例子:
```prolog
valid([]).
valid([Head|Tail]) :-
fd_all_different(Head),
valid(Tail).
sudoku(Puzzle, Solution) :-
Solution = Puzzle,
Puzzle = [S11, S12, S13, S14,
S21, S22, S23, S24,
S31, S32, S33, S34,
S41, S42, S43, S44],
fd_domain(Solution, 1, 4),
Row1 = [S11, S12, S13, S14],
Row2 = [S21, S22, S23, S24],
Row3 = [S31, S32, S33, S34],
Row4 = [S41, S42, S43, S44],
Col1 = [S11, S21, S31, S41],
Col2 = [S12, S22, S32, S42],
Col3 = [S13, S23, S33, S43],
Col4 = [S14, S24, S34, S44],
Square1 = [S11, S12, S21, S22],
Square2 = [S13, S14, S23, S24],
Square3 = [S31, S32, S41, S42],
Square4 = [S33, S34, S43, S44],
valid([Row1, Row2, Row3, Row4,
Col1, Col2, Col3, Col4,
Square1, Square2, Square3, Square4]).
```
然後說:
> 程序在哪裏?我們沒有編寫什麼程序。我們只是描述了這個遊戲的規則。(p. 91)
感覺這個例子並沒有體現邏輯編程的威力。如果用函數式的方法來寫,一樣用不了多少行,也只需要描述下規則。
Scala
-----
從作者的介紹中沒看到Scala的特色——給我的感覺是,Scala之於Java,類似CoffeeScript之於JavaScrip,只不過和CoffeeScript相比,Scale是靜態類型的,而且抄了更多函數式編程語言的東西,語法也複雜得多。
Erlang
------
> Erlang還有一些別的古怪之處,比如說,符合條件的數字數組會顯示爲字符串。(p. 169)
作者說的其他Erlang的缺點我覺得都不算什麼缺點,但是這個真的是……用數字字符……還有Erlang孱弱的字符串處理能力。
其實Erlang還有一些奇怪的地方。比如在shell裏無法輸入函數定義,因爲Erlang裏模塊是FORM,而FORM不是EXPRESSION。所以在shell裏只能用匿名函數(這個是表達式)。
```erlang
double(X) -> 2*X. // FORM
Double = fun(X) -> 2*X end. //EXPRESSION
```
畢竟Erlang已經算古老的語言了,爲了維持向後兼容,一些不太好的設計也只好保留下來了。[elixir](http://elixir-lang.org/)是基於EVM的語言,吸收了Ruby的一些東西。
Clojure
-------
> 要調整適應前綴表達法並不容易。它需要更好的記憶力,並且要求開發者有內向外地理解代碼,而不是由外向內。有時,我覺得閱讀Clojure代碼迫使我過早地去瞭解過多細節。(p. 203)
如果排版合理的話,閱讀起來沒有問題。
> 由於在JVM上,Clojure限制了尾遞歸優化,Clojure程序員必須用可怕的recur語法。不信就試試看分別用遞歸和loop/recur來實現返回序列x長度的函數(size x)。(p. 203)
沒有尾遞歸優化的函數式編程語言……
> 消滅用戶定義的宏讀取器也是一個典型的例子。好處很明顯,宏讀取器被濫用時,可以導致語言分裂。代價也很明顯,你又失去了一樣元編程工具。(p. 204)
Lisp基於S表達式的語法,使得宏特別方便。現在把宏搞殘了,蠻可惜的。宏乃重器,不可輕用。然而這應該靠個人自律和團隊規範來約束,而不是在語言層面直接限制機制。
Haskell
-------
> Philip: 隨着分佈式結構變得越來越重要,我們需要關注運行在多個機器上程序,這些程序將數值從一個機器發到另外一個機器。當發送一個數值時,你可能更想要的是這個本身,而不是一個通過求值才能產生這個值的程序(以及這個程序的所有自由變量的值)。所以,在分佈式世界中,我認爲默認採用渴望求值的方式更好,不過當你需要的時候使用惰性方法也會很容易。(p. 217)
Philip Wadler是Haskell設計委員會的成員,連他也承認(至少在分佈式的世界中),默認eager evaluation是個好主意。其實在非分佈式的環境下,可能也是默認eager evaluation好一些。
落幕時分
-------
> 作爲悲催的Java開發者,我不得不爲閉包苦苦等上十年,只因像我這樣熱切期盼閉包的人都是「文盲」或「半文盲」,沒法給閉包搖旗吶喊、鼓吹造勢,也因爲Spring這類主流框架,堅持用匿名內部類解決大量本可用閉包解決的問題。沒有閉包,輸入代碼的工作就太繁重了,我的手指頭都敲出血了;閱讀代碼的任務也不輕,我的雙眼也佈滿了紅紅的血絲。(p. 242)
諷刺的是,Spring的作者Rod Johnson貌似現在很是鼓吹函數式編程。(不過[被噴了](https://github.com/yinwang0/blog-cn/blob/gh-pages/_posts/2013-10-05-random-words.md):「简直可以用浑水摸鱼,空话大话天花乱坠,专骗外行来形容。跟 Spring 的那些教程一个德行,广告成分多于实质内容,说半天抓不住关键。」)
這本書的作者對編程語言的理解太有限了,所以深度不足,對語言的描述很多都搔不到癢處。好在裏面收了很多某語言社區裏面的核心人物的訪談,部分章節也請人審閱,所以還是值得一看的。
語言選擇上,如果讓我來選的話:
- 首先把Ruby去掉,太熱門了,要選一門面向對象的,不如換Smalltalk/Squeak.
- Scala沒有什麼特別的,不如介紹一門面向堆棧的語言,Forth,或者現代的Factor.
- Clojure,我覺得也可以直接換Racket,更純的Lisp風味。
這樣子就好多了。當然還有空間:
- Prolog太老了,不如換成miniKanren。
- Erlang有些奇怪的地方,可以考慮換Elixir,穿插介紹些Erlang。
- Haskell比較複雜,可以考慮去掉。惰性求值和類型系統放Racket裏講,Racket有Lazy Racket和Typed Racket。
- 把Haskell去掉省出來的空間可以給Lua。Lua也是很有意思的語言。而且Lua雖然是用table的lisp,但是外表可以冒充過程式編程。這樣也算把主要的編程範式都覆蓋到了。
也就是以下7種:
1. Lua
2. Squeak
3. Io
4. Factor
5. Racket
6. miniKanren
7. Elixir
----
https://gist.github.com/weakish/8208971