elixir:靈丹妙藥?or 徒有其名?
13年的時候正在追Erlang,有天看見Joe老爺子的一篇博客介紹Elixir [1],才第一次聽到這個語言。
This has been my first week with Elixir, and I』m pretty excited.
Elixir has a non-scary syntax and combines the good features of Ruby and Erlang. It』s not Erlang and it』s not Ruby and it has ideas of its own.
It』s a new language, but books are being written as the language is being developed. The first Erlang book came 7 years after Erlang was invented, and the first popular book 14 years later. 21 years is too long to wait for a decent book.
Dave loves Elixir, I think it』s pretty cool, I think we』re going to have fun together.
這評價從Joe老爺子嘴裡吐出實屬不易 —— 頗有點拍拍 Jose Valim 的肩膀,說「小夥子,努力干,偶看好你噢」的趕腳。噢,我忘了介紹,Joe老爺子是Erlang的創始人,Jose是Elixir的創始人(之前是Rails的core member),兩人看上去相差三十歲。去年的文章我曾經講過程序員不得不看的幾本編程書,老爺子的 "Programming Erlang: Software for a Concurrent World",就是其中不得不看的一本(現在有2nd edition了)。好的編程書重在講語法背後的思想,設計背後的初衷,如果單純是要了解語法那些膚淺的東西,看看 "Learn X in Y minutes" 就好。所以大家看編程書的話,最好看語言作者的書,實在沒有,也要看社區里的大牛的 —— 因為他們聊的不是語法(當然也不是寂寞 _)!
扯遠了,那時候把玩了一下Elixir,就像『七周七語言』那樣,玩兩周就算了,沒有深入。唯一的感覺是:哇,BEAM [2] 上總算有一個讓人好好寫代碼的語言了 [3]。
兩年多的時光彈指過去,Elixir在最近終於發布了1.0.x版本,而Jose本人又頻頻上鏡,到處佈道Elixir,我才重新關注起這門語言。一門社區驅動的語言(或者框架),在沒有到1.0之前,都意味著語法和庫函數的極不穩定。1.0以後,起碼意味著你可以拿它寫點什麼,而不至於寫下的代碼半年後就完全沒法工作 [4]。所以我就重新拾起Elixir的文檔,邊啃邊寫。
差不多一個月下來,寫了二三十個小項目,從ip packet的parsing,到http reverse proxy,都是幾百行以內,一兩個小時頂多到一整天能搞定的東東。借著這股興奮勁,我來講講自己對Elixir的淺顯認知。
驚艷的語法
Elixir的語法在向Ruby致敬,同時透著Erlang和Prolog的靈氣。任何語言語法的設計都和其創始人的偏好和目標分不開,Ken Thompson/Rob Pike的golang看上去很C,Jose Valim的Elixir自然就很Ruby。當然,植根於Erlang的Elixir,又有有很多自己的特點。
最讓人愛不釋手的是pipe |>,它讓你把一層層的逆著你的思維的函數調用變成了更直觀的表現,比如說我們常常這麼寫代碼:
IO.puts(tabularize(to_map(Store.get_host(host))))nn或者nnlist_data = Store.get_host(host)nmap = to_map(list)nformatted_output = tabularize(map)nIO.puts(formatted_output)n
這樣的代碼在Elixir中可以被寫成:
hostn|> Store.get_hostn|> to_mapn|> tabularizen|> IO.putsn
非常清晰 - 最重要的是,它更符合你的思維模式,讓代碼更容易在指尖流淌。我們寫代碼的時候,基本就是一個不斷「分治」的過程:把大問題分解成小問題,小問題分解成更小的問題,最終解決問題。而Elixir讓你的代碼和你的思路高度一致。
這個語法特點來源於Prolog,遺憾的是,繼承自Prolog的Erlang沒有將其撿來,卻把它遺給了繼承於Erlang的Elixir。
看到這裡,有同學也許會問?這不是object chaining么?老娘/老子在Ruby里,或者在jquery中,經常這麼寫代碼。。。
雖然pipe和chaining表述代碼的方式有些類似,但背後的思想不太一樣。chaining是在對象上不斷執行其方法,類似於語法糖,而pipe是把上一次的執行結果傳遞給下一個函數的第一個參數,和unix的pipe類似。chaining的限制很大,為此你要犧牲方法的特性 [5],而pipe非常靈活,你可以一邊組織思路一邊組合函數,有點搭積木的節奏。
其它的語法細節,如函數式編程,sigils,first class doc等等,就不提了,感興趣的可以自行了解。
Pattern matching
我們知道,Erlang在concurrency以外的另一大特點是pattern matching,它能讓你把繞來繞去的if/else變得簡單明了。如果你不幸在工作中遇見if/else hell,再看過支持pattern matching的語言,你一定會淚流滿面。
那麼問題來了,當pipe遇見pattern matching是什麼光景?看下面的代碼:
淺顯易懂,還很難有邏輯錯誤。這個代碼里同一個 run 被定義了很多次,根據參數的不同,會調用不同的函數。我們再看一個例子:
除了pipe,裡面用到了pattern matching + recursion,這裡還是分治的思想。它是Elixir下寫代碼的一個很自然的模式:任務不斷拆解,每個函數專註只干一件事。當然,幾乎所有的語言都希望開發者這麼做,但不少都沒有提供正確的工具讓開發者自然而然這麼做。
使用pattern matching取代大部分條件分支是件相當偉大的事情:代碼的簡潔自不必說,其效率還有可能進一步優化。ifelse是一種順序執行的邏輯,因為其語法結構的靈活(if的條件里是個函數這事大家都干吧),頂多是對一些特殊的情況使用跳轉表優化,大多數情況是O(N),而且很難並行處理。而pattern matching由於其語法上的限制,很多情況可以被優化成decising tree,時間複雜度是O(logN),而且未來還有並行處理的優化空間。
當pattern matching遇見macro
當然以上的好處也是erlang的好處,但Elixir在此基礎上做了一件也許是跨時代的事情:支持macro。Ruby也支持macro,任何從lisp演進或者接受lisp思想的語言也支持macro,為什麼Elixir支持macro如此特殊?目前已有的支持macro的語言,macro更多地被用作突破語法的極限 —— 要麼用於定義DSL讓代碼簡潔,如rails;要麼用於生成繁雜的介面代碼而不必手工撰寫。但Elixir在BEAM上支持macro,不管是有心還是無心,跟pattern matching一配合,帶來了無窮的想像空間。
Elixir的unicode的大小寫轉換不必再提,我在「顛覆者的遊戲」一文已經介紹過。類似的問題都可以這麼處理。比如說我昨天做了一個中文簡繁轉換的模塊:把wikipedia的最新詞庫導入,使用macro在編譯時生成近10,000個按詞進行正向最大匹配的遞歸函數,代碼卻僅需200行(見 http://github.com/tyrchen/chinease_translation)。以此類推,中文短句的slugify也就是同等規模的問題。
還有數據清洗和數據過濾。比如說眾所周知的敏感詞過濾。敏感詞詞庫一更新,只需要重新編譯出新的代碼,載入即可(BEAM支持hot code reload)。
再講一些做系統的新思路:
用戶名保留。使用一個文本字典,記錄要保留的用戶名。用macro生成pattern matching的代碼。
弱密碼防護。把黑客用於攻擊的常見字典編譯出一個密碼黑名單的pattern matching的代碼。
文本分析。對於格式各異的日誌文件,定義抓取範式,然後通過這些範式生成pattern matching的代碼。
等等。它們共同的特點是把原來依賴於資料庫才能完成的事情,交給了編譯時完成。花了很小的代碼,我們就享受運行時的高效,還有組件化,沒有外部依賴等等好處。我還沒有具體測試過對於某種pattern,生成的函數超過10k級別的時的BEAM的處理效率,但在10k及以下的pattern,效率非常非常高。
天生的concurrency支持
這個就不多說了,Erlang的基於actor的並發模型,let it crash的處理思想,supervision tree,error kernel,都是在二十多年來與並發作鬥爭過程中不斷總結出來的best practice,無論在思想上,還是實操上,在可預見的未來,沒有語言能夠超越它。Elixir站在巨人的肩膀上,坐享其成。
服務周到的工具鏈
進入21世紀以來,新興的語言都在工具鏈上卯足了勁,工具鏈(幾乎)成為語言的一部分(一起ship),而非附屬品。從產品設計的角度上,這是非常英明的 —— 語言間的競爭如此激烈,光碟兒正條兒順是不夠的,得舍下身段做丫鬟 —— 誰把程序員哄的開心,誰勝出的幾率要更大些。
你仔細想想golang的 go xxx(test/fmt/get),nodejs的npm,就大體知道我想講什麼了。作為一個C程序員,為了使用一個lib所犯下的折騰,讓我感覺自己活在史前文明;而作為一個python(java/ruby)程序員,過度繁榮(各有優劣)的工具鏈市場又讓我在適配上花太多時間 —— 三宮六院七十二妃,就是不如一個琴操姑娘好啊。
Elixir自身攜帶了mix —— 從項目的創建和scaffolding(mix new),編譯(mix compile),到測試(mix test),到文檔(mix doc),到依賴管理(mix http://deps.xxx),全部包圓,還有其它語言自帶的工具如此全面么?據說以後還要把release和deployment管理起來,嗯,很好很強大。
總結
做硬體的兄弟總是嘲笑我們這些寫軟體的笨蛋們 —— 當他們做的硬體能夠不斷以搭積木的方式自我累積,數十億個晶體管組成的複雜系統可以bug free時,我們寫的軟體卻糟糕得一塌糊塗。原因有多方面的,如果從語言本身出發找原因,那就是語言本身並未讓你以搭積木的方式組織系統。
那麼,什麼樣的語言更容易貼近搭積木的組織方式呢?
提倡使用遞歸(遞歸就是以自身為積木)
以pattern matching的方式組織代碼(每個代碼快儘可能小,只處理一件簡單的事情)
語言層面提供解耦的工具(如erlang的process,golang的chan,scala的actor)
系統的一部分損壞,並不影響未損壞的部分
嗯,Elixir是靈丹妙藥,還是徒有其名?讓時間來說明一切。就這麼多(真沒想到這篇文章斷斷續續寫了快一周了)。
如果您覺得這篇文章不錯,請點贊。多謝!
歡迎訂閱公眾號『程序人生』(搜索微信號 programmer_life)。每篇文章都力求原汁原味,北京時間中午12點左右,美西時間下午8點左右與您相會。
1. 見:A Week with Elixir2. Erlang的VM3. 初學者在Erlang的世界裡很容易找不到北,這個,走過這段路的人都有感受4. 這一點,我在meteor下吃了大虧,我的teamspark寫於0.5.x,然後每一次版本升級,就各種crash…5. 比如說本來可以返回一個結果,卻不得不返回自己,而把結果存儲在對象中推薦閱讀:
TAG:迷思 | 1 | 2 | 3 | 4 | 5 | 1 | AWeekwithElixir | 2 | 3 | 4 | 5 |