Erlang 之父學習 Elixir 語言的一週

李鼎發表於2016-10-17

// 本文基於開源中國社群的譯文稿: Elixir的一週

譯序

作為Erlang之父Joe Armstrong,對Erlang VM上的新語言Elixir給出很精彩評論和思考。在『特定領域的專家的專業直覺』、『程式語言設計的三定律』、『管道運算子避免噁心程式碼』、『Elixirsigil引出的程度語言如何定義/解釋字串』等等這些討論上,能強烈感受到Joe Armstrong老黑客風範。

自己理解粗淺,而本文討論是語言設計,且作為老一代黑客的作者計算機領域中那被我們現在不再要去理解使用的主題和思想(如Prolog/DCGLisp/巨集、sigil、不可變閉包、語言設計的相容性)真是信手拈來,翻譯中肯定會有不少不足和不對之處,譯文原始碼在GitHub上《與Elixir相處的一週》,歡迎建議(提交Issue)和指正(Fork後提交程式碼)!
PS:為什麼要整理和審校翻譯 參見 譯跋


與 Elixir 相處的一週

差不多一週前我開始看Elixir,關於Elixir之前只有些模糊的瞭解,沒打算花時間去看細節。

但在得知 Dave Thomas 出版了 Programming Elixir 這本書的訊息後,我的想法就徹底改變了。Dave Thomas 幫我修訂過我的那本Erlang的書並且作為Ruby的倡導者做得非常出色,所以要是 Dave 對一樣東西產生了興趣,那說明這樣東西的有趣性是毫無疑問的。

DaveElixir很感興趣,在他的書裡這樣寫道:

在1998年的時候,由於我是comp.lang.misc郵件組的忠實讀者,機緣巧合得知了Ruby,然後下載、編譯、與Ruby墜入愛河。 (沒聽過comp.lang.misc?那去問問你老爹吧。) 就像任何一次相愛經歷一樣,你很難解釋原因是什麼。 Ruby的工作方式和我心裡想的靈犀默契,而且總是有足夠的深度持續點燃著我的熱情。

回首已經逝去15年的時光,而我無時無刻不在尋找下一個也能給出這樣感覺的新『物件』。

很快我遇上了Elixir,由於一些原因,我沒能一見鍾情。 但在幾個月前,和Corey Haines聊了一次,在如何不用那些學院派的書給大家介紹哪些有吸引力的函數語言程式設計概念這個問題上訴了些苦。 他告訴我再去看看Elixir。我照做了,有了第一次看到Ruby時那樣的感覺。

我能理解這種感覺,一種先行於邏輯的內心感性的感覺。就像我知道一件事是對的,我卻並不知道我是如何和為什麼知道這是對的。而對原因的解釋常常在幾周甚至幾年後才顯露出來。Malcolm Gladwell 在他的 Blink: The Power of Thinking Without Thinking 一書中曾探討過這個問題。一個特定領域的專家常常能瞬間感知出一些事情是否正確,但卻不能解釋為什麼。

但得知DaveElixir『看對眼』時,我很想知道為什麼他會這樣。

無獨有偶,Simon St. Laurent也出了本Elixir的書。Simon的 Introducing Erlang 一書表現不俗,我和他還通過郵件溝通過幾次,還有有些熟悉的。而且Pragmatic PressO’Reilly出版社都在爭著要出版Elixir,我知道在Erlang VM上有事要發生,而我自己還沒注意到。毫無疑問我Out了!

我發封郵件給DaveSimon,他們爽快地借給我了樣書,現在可以開始閱讀了……謝了二位……

上週我下載了Elixir然後開始學習……

沒多久我覺得就上手了。確實是個好貨!有趣的是ErlangElixir兩者在在底層一樣的,對於我來說『感覺』是一樣的。事實上也確實這樣,兩者都會被編譯成EVM(Erlang Virtual Machine)指令 —— 實際上EVM這個叫法之前沒人用,都叫成Beam,但為了和JVM區分開,我覺得是時候開始用EVM這個叫法了。

ErlangElixir為什麼有相同的『語意』?這得從虛擬機器底層談起。垃圾回收行為,不共享併發機制,底層的錯誤處理和程式碼載入機制都是一致的。當然這些肯定都是一致的:他們都執行在相同的VM裡。這也是ScalaAkka區別於Erlang的原因。ScalaAkka都執行在JVM之上,垃圾回收和程式碼載入機制從根本上就不一樣。

你直接看到的Elixir是完全不同的上層語法,源自Ruby。看起來不那麼『可怕』語法和很多附加的甜點。

Erlang的語法源自Prolog,並受到SmalltalkCSP和函數語言程式設計的影響很大。Elixir則受到ErlangRuby的影響很大。從Erlang借鑑了模式匹配(pattern matching)、高階函式(higher order function)以及整個程式(process)和任其崩潰的(let it crash)錯誤處理(error handling)機制。從Ruby借鑑了sigil和快捷語法(shortcut syntax)。當然也有自創的甜點,像|>管道操作符(|> pipe operator),讓人想到PrologDCGHaskellMonad(儘管相比要簡單不少,更類似於Unix的管道操作符),還有巨集的引用和反引用操作符(macro quote and unquote operator,對應的是Lisp的反引號和逗號操作符)。

【譯註】:

sigil是指在變數名中包含符號來表達資料型別或作用域,通常作為字首,如$foo,其中$就是個sigil。 像本文中說的例子,sigil也可以能對常量加上字母符號,r"abc",其中rsigil,把字串轉成正規表示式。 詳見wikipedia詞條sigil)


DCGdefinite clause grammar),確定性子句語法,表達語法的一種方式,可以用於自然語言或是形式化語言,比如像Prolog這樣邏輯程式語言。基本的DCG用於描述『是什麼』和『有什麼特性』(簡單的可以認為邏輯程式設計程式設計師要做的就是給出這些描述;剩下的事是邏輯引擎會根據描述的規則生成演算法,然後得出解來)。像這樣:

不展開說明了,對於沒有了解過Prolog/邏輯程式設計的同學意會一下就好,不用糾結了。詳見wikipedia詞條Definite clause grammar


譯文使用英文術語本身,不翻譯成中文,有更好的辨識度。

Elixir還提供一個新的下層AST,取代了每個form都是獨有表示的Erlang ASTElixir AST有一個統一得多的表示,這使得超程式設計(meta-programming)要簡單得多。

Elixir的實現出奇的可靠,儘管有幾個地方和我預想的不一樣。字串插值(string interpolation)的工作方式有時候不好使(字串插值是個很棒的想法) :

x求值後把x友好格式化的表示(a pretty-printed representation)插入到字串中。但是隻對簡單形式的x可行。

這點可以通過從Elixir呼叫Erlang的方式很簡單就能解決掉。

IO.puts "...#{pp(x)}..."這樣就總是可行的。我只是把pp(x)定義成

Erlang則表述成:

很『顯然』這和Elixir的版本表述是一樣。當然Elixir的寫法要更容易閱讀。上面用到的|>操作符意思是把io_lib:format的結果輸入到lists:flatten,然後再到list_to_binary。就像好用的老傢伙Unix的管道符|

Elixir打破了一些Erlang神聖信條 —— 在順序結構中變數可重繫結(re-bound)。實際上這也是可以做到的,因為最終結果還是可以規範化成靜態單賦值(static-single-assignmentSSA)的形式。儘管在順序結構中這是可以的,但在迴圈結構中,一定肯定以及確定不要這麼做。但這不是個問題,因為Elixir木有迴圈,只有遞迴。實際上Elixir不可能在迴圈中包含可變的變數(mutable variables),因為這樣編譯出來的東西在下層的EVM是支援不了的。順序結構的SSA變數挺好的,EVM知道如何對其做優化。但在迴圈結構不行,所以Elixir沒有這麼做。關於這方面的優化甚至可以更往下挖到LLVM彙編器(LLVM assembler) —— 但又是另一個很長的故事先就此打住吧。

0. 程式語言設計的三定律

  1. 你做對的,無人為你提。
  2. 你做錯的,有人跟你急。
  3. 難於理解的,你必須一而再再而三地去給人解釋。

一些語言有的設計做得太好,結果大家都懶得去提,這些好的設計是正確的、是優雅的,是易於理解的。

對於錯誤的設計,你完了。你成了2B,如果好設計比壞設計多,你可能被原諒。你想在以後幹掉這些壞設計,卻因為向後相容性或者是有些SB已經用上所有那些壞設計寫上了1T行程式碼,結果你是改不了了。

而難以理解的部分才是真正無賴。你必須一而再再而三地解釋,直到你吐血,可還是有些人永遠不懂,你必須寫上百郵件和數千文字來一遍又一遍地地解釋這是什麼意思以及為什麼會如此。對於一個語言的設計者或作者來說,這是個痛苦的深淵。

下面我要說到的幾件事,我認為也會落入這三類情況中。

在我開始前,我先要說,Elixir做對了灰常灰常多對的事情,而且遠遠多於錯的。

關於Elixir有利的是,要改正它的錯誤還不算晚。這隻能在無數程式碼行被寫下和眾多程式設計師開始使用它之前才能做到 —— 所以解決這些問題的時日並不多了。

1. 在原始檔中沒有版本

XML檔案總是這樣開始的:

這點非常好。讀取XML檔案的第一行就像是聽到拉赫瑪尼諾夫的第三鋼琴協奏曲的第一小節(【譯註】:指其富有辨識度)。這是一個令人讚歎的經驗。讚美XML設計師,願他們的名字得到榮光,給這幫夥計頒圖靈獎吧。

所有原始檔中加上語言的版本是必要的。為什麼呢?

早期的Erlang沒有列表推導(list comprehension)。如果我們對一個新版的Erlang模組用一箇舊的Erlang編譯器去編譯。新版的程式碼含有列表推導,但舊編譯器並不知道列表推導,所以舊編譯器會認為這是一個語法錯誤。

如果 版本3 Erlang編譯器處理這樣開始的檔案:

則可以給出這樣提示資訊:

啊~~~~咦~~~~

哦,煩炸了,我只是版本3的編譯器,看不懂未來。

你剛剛給我一個版本5的程式,這說明我在地球上的壽命已過。

你將不得不殺掉我,把我解除安裝,然後安個版本5的新編譯器。曾經玉樹臨風的我現在沒了價值,我將不再存在。

再見吧,老朋友。

我感覺頭痛。我要休息一下……

這是資料設計的第一法則:

所有未來可能會改變的資料應該標記上版本號。

而 模組 資料。

2. fundef不同

在寫Programming Erlang一書時Dave Thomas問函式為什麼不能輸入到shell裡。

如果模組裡有這樣的程式碼:

不能直接複製到shell裡執行,得到相同的結果。Dave問這是為什麼,並說這樣很傻。

Lisp等其它語言主中這做是沒問題的。Dave說了『這很會讓人很迷惑』類似這樣的話 —— 他說的對並且這確實讓人迷惑了。在論壇裡關於此的問題肯定有成百上千條。

我解釋這個問題已經無數遍了,從黑髮解釋到白髮,我現在頭髮真白了就是因為這個原因。

原因是Erlang的一個bug

  • Erlang的模組是一系列的 FORM
  • Erlang shell解析的是一系列 EXPRESSION
  • ErlangFORM 不是 EXPRESSION

上面兩個是同的。這小點愚蠢成了Erlang一個永遠的痛,當時我們沒有注意到,到了現在我們就只能學會和它相處。

Elixir模組可以這麼寫

估計很多人都會直接從編輯器複製到shell裡直接執行,然後收到的是出錯資訊:

如果你不解決這個問題就要花後面20年的時間去解決為什麼 —— 就像Erlang曾經所做的。

順便說一下,修復這個問題真的真的很簡單。我在erl2作為了嘗試就解決了。Erlang中沒法修復這個問題 (版本相容問題),所以我就在erl2解決。只需要erl_eval的小改和解析器的幾個微調。

主要原因是FORM不是EXPRESSION,所以加了個關鍵字def

這就定義了一個有副作用的表示式。由於是個表示式,可以在shell中求值了,記住在shell中只能對錶達式求值。

副作用指的是需要建立一個shell:fac/1功能(就像在模組中定義的一樣)。

上面兩者應該是一致的,並且都是定義一個名為Shell.double的函式。

做了這樣的修改,媽媽再也不用擔心我會白頭了。

3. 函式名稱中有個額外的點號

在學校裡我學會了寫f(10)來呼叫函式而不是f.(10) —— 這是個『真正』的函式,函式名是Shell.f(10)(一個在shell中定義的函式)。shell部分是隱式的,所以可以只用f(10)來呼叫。

如果這點你置之不理,那就等著用你生命的接下來的二十年去解釋為什麼吧。等著在數百論壇裡的數千封郵件吧。

4. 傳送操作符

這是啥玩意?你知道從occam-pi轉成Elixir有多難麼。

這點讓你現在在失去occam-pi社群路上。傳送操作符就應該是!,像這樣:

接下來的一週,我的大腦會變成漿糊,我的神經網路要被重程式設計,這樣我才能『看到』<-時才能反應成! —— 這點不是在說如何我思考,而是指要重程式設計我更深植在脊柱裡無意識反應。傳送操作符已經不在我大腦裡,而是在我的脊柱裡。我的大腦想著『傳送一個訊息給一個程式』併傳送訊號給我的手指,我的脊柱馬上加上!,接著大腦要回退刪除這個字元改成<-

這是一個語法問題。讓人愛恨交織的語法。如果10分制的評級標準,10代表『非常非常爛』,1代表『好吧,我可以適應』的話,這個問題我給3分。

這點會使Occam-pi的程式設計師很難轉到Elixir,什麼,只需要簡單地使用!就能完成<-的功能?這可真是出人意料啊。相信會有很多人受到鼓舞的。

5. 管道運算子

這是一個很好很好的東西並且很簡單就能掌握,以至於沒人會給你稱讚。這就是生活。

這是來自Prolog語言的隱性基因(recessive gene):monad。 在Prolog中的基因是顯而易見的, 但是在Erlang中確實不明顯的(Prolog的兒子)但是又在ElixirProlog的兒子的兒子)中重新顯現了。(【譯註】:隔代遺傳)

x |> y意味著呼叫了x然後獲取了x的輸出並且將它作為y的另外一個引數(第一個引數)。

所以

等價於下面的程式碼:

非常有用。假設我們要把握的是把一個變數abc轉換為Abc。在Elixir中沒有利用的函式但是還有一個功能,就是去控制一個字串。所以我們需要現將這個變數轉換為string,在Erlang中,我們可以這樣寫:

這樣的寫法太驚悚了。我們還可以寫成這樣:

但是,這更糟 —— 好惡心的程式碼。像這樣德性的程式碼我都不知道寫過多少次了!浪費我大把的青蔥歲月。

於是|>來了:

為什麼我認為|>是隱性基因?

ErlangProlog中演化而來,而且Elixir也繼承了Erlang

PrologDCG,所以

擴充套件後的形式:

這基本上是同樣的想法。我們通過新加一個額外的隱藏引數把函式呼叫序列的輸入輸出串接起來了。這類似Haskellmonad用法,但做得很隱祕。

PrologDCGErlang沒有,Elixir有管道操作符!

6. Elixirsigil

sigil很棒 —— 愛之。我們應該加到Erlang裡。

字串是一個程式設計抽象。程式語言都有字串常量,通常使用雙引號包著的一串字元。就像這樣的一行程式碼:

編譯器會轉換成字串的內部表示,關聯上對應的語義。

Erlang

表示『X是字元a, b, cASCII碼值對應的整數的列表』。

但也可以選擇成任何其它我們想要的含義。在Elixir裡,x = "abc"代表x是一個UTF8編碼二進位制(binary)(【譯註】:binaryEVM的內建型別)。通過在雙引號前面加上r可以改變字串含義成和Erlang一樣:

當然也可以被定義成代表編譯過的正規表示式,也就是說和等價於X = re:compile("...") —— 基於我們確定字串的含義,可以以不同的方式去解釋(interpret)內容。可以寫上這樣的程式碼:

B值可以是Hello Joe —— 這裡sigil s改變字串常量解釋行為,『替換變數的值並插入』。

Elixir在這方面做得很好,定義了很多不同的sigil

Elixirsigil語法不太一樣,如下:

C是單個字元(【譯註】:Erlang中大寫開頭的是變數不是常量,C是單個字元,表示可以是ab$),後面跟著一對{}[]

sigil很棒。Erlang本可以在15年前就有這個功能,而現在也可以引入,並且不會帶來向後相容的問題。

7. docstring

大愛docstring

但有個小意見。請把docstring放到函式定義裡面

Elixir是這樣:

放到函式裡面會是這樣:

否則成了『沒有歸屬的註釋』(detached comment):當你編輯程式時,就可能出這樣的問題。註釋會與它要註釋函式脫離開。

Erlang裡,沒有辦法確定註釋的是下一個函式還是上一個函式,或是模組。如果註釋的物件是函式那就應該放到函式裡面而不是外面。

8. defmacro引用

愛之。在解析轉換這個正確階段所做的正確的事。這讓可以讓人舒舒服服得不用去知道抽象語法了。引用(quote)和反引用(unquote)為你把魔法都做好了。

這就是那種是對的事 —— 非常棒卻真真兒難於解釋。就像Haskellmonad —— 啊哈,monad真很容易解釋,難怪有上千篇文章來解釋它有多簡單。

Elixir巨集真是簡單 —— 引用(quote)對應Lisp的反引號(quasiquote),反引用(unquote)對應Lisp的列表逗號操作符(list comma operator) —— 這就我說的簡單 :-)

9. 額外的符號

像這樣:

而不是這樣:

列表後面額外的冒號讓人迷惑。

10. 奇怪的空白符

哎呦~ 一定要是do:do :不行。

個人認為,空白符(whitespace)就是空白符。在字串裡面不能隨便新增。在字串外面,為了格式化程式碼我可以按自己喜好新增空白,這樣可以讓程式碼更美觀。

Elixir不能這麼做 —— 不討我喜歡。

11. 閉包行為完全正確 —— 哦耶

Elixir的閉包(closure)(即fn表示式)和Erlang完全一樣。

fn表示式有一個很好的特性:能捕獲所在作用域的任何變數的當前值(換句話說:能建立不可變的閉包(immutable closure)),這點令人難以置信的有用。需要說一下,JavaScript在這點上非常錯誤。給一個JavaScriptElixir的例子,方便看到這點上的差異:

啥!函式f被打破了。定義的f,開始使用;修改了變數a有副作用打破了函式f。函數語言程式設計的好處之一就是使程式變得容易推理。如果f(10)的值是15,那麼就應該一直是15,不應該能在其它的地方打破。

Elixir呢? 閉包的處理是對的:

正確的閉包只應該包含不可變資料的指標 (Erlang中資料正是不可變的) —— 而不是可變資料的指標。如果閉包裡有指向可變資料的指標,後面修改了資料就會破壞閉包的一致性。這樣的結果就是不能把程式並行化,甚至順序執行的程式碼也會詭異的錯誤。

在傳統語言裡要建立合適的閉包的代價會很高,因為捕獲環境裡的所有變數都需要做深拷貝,但ErlangElixir不用這樣,資料都是不可變的。你所要做的就是引用需要的資料。內部實現是通過指標引用資料(指標對程式設計師是不可見的),並且不再有指標引用的資料會被垃圾回收掉。

shell中可以有閉包,但不能寫到模組裡。

shell裡,如果可以這樣寫

為什麼不能在模組裡這樣寫呢?

這個問題完全是可以解決的,我在erlang2語言實驗並解決了。

最後

這就是我與Elixir的相處一週,非常興奮的一週。

Elixir沒有令人生畏的語法,融合了RubyErlang優秀的特性。它不是Erlang也不是Ruby,有自己創新的想法。

這是門新興的語言,但在語言的開發的同時介紹的書也同步在寫了。第一本介紹Erlang 的書在Erlang被發明後7年才出現,而暢銷書更是在14年後才出現。用21年的時間去等一本真正的介紹書籍實在是太長了。

Dave很喜歡Elixir,我也覺得很酷,我想我們會在使用過程中找到更多樂趣的。

像是WhatsApp這個應用和全世界一半手機網路的關鍵部分都是搭建在Erlang之上。當技術變得更加親和,當新一批熱衷者進入陣營,讓我現在懷著非常欣喜的心情關注著後續要發生的變化。

這是篇即興的文章。也許會有些不妥之處,歡迎大家指正。

打賞支援我寫出更多好文章,謝謝!

打賞作者

打賞支援我寫出更多好文章,謝謝!

任選一種支付方式

Erlang 之父學習 Elixir 語言的一週 Erlang 之父學習 Elixir 語言的一週

相關文章