神奇的仙丹,性感的Elixir
在IT世界里,沒有銀彈,但卻有神奇的仙丹(Elixir)。我不知道是什么靈感刺激這門語言的創造者José Valim想到了這么酷的命名,但這枚仙丹確實經由多種神奇的靈藥煉制而成,這些靈藥包括Erlang、Ruby、Clojure、Haskell。
品嘗這枚仙丹確實令人飄飄欲仙,至少,我在淺嘗Elixir時,這種奇妙的感覺一直縈繞在我心間,怦然心動因而不舍離去。或許如Erlang之父Joe Armstrong所說,是“一種先行于邏輯的內心感性的感覺”;又或者如Dave Thomas形容的,那是讓人“墜入愛河”的感覺。
大愛Elixir。
我之所以愛上Elixir,大約還是因為Ruby的緣故。我并非Ruby的狂熱追隨者,甚至沒有從事太多Ruby相關的項目,但我至今在編寫腳本時,Ruby依舊是我的首選。在動態語言中,我甚喜愛Ruby相對簡潔的語法。當我看到Elixir時,那種似曾相識的感覺讓我心動。
雖然說Elixir的煉制來自各位前輩留下的丹方靈藥,然而從成丹之日起,Elixir就是Elixir,她已經具有了完整的語言性格。就我看來,Elixir真正稱得上是“性感”。當然,這一大半要歸功于Erlang美麗的英倫風情(Erlang之父Joe Armstrong是英國人),就Erlang的高顏值打底,只需再加上幾點嫵媚,幾分妖嬈,風采就變得性感撩人了。
并發與分布式
Elixir對并發與分布式的支持,就是正宗的英倫風情,這是從Erlang延續下來的最強悍基因。Elixir建立在Erlang虛擬機(BEAM)之上,使用Erlang的進程,如原生進程那樣在所有的處理器中運行,然而開銷卻非常小。與Erlang一樣,Elixir可以通過spawn輕松地創建進程:
- spawn fn -> 1 + 2 end
Elixir或者說Erlang的進程依靠消息傳遞完成通信。進程接收到的消息實際上是獲取的一份消息副本,這就使得接收方能夠與發送方解耦,接收方對消息的任何操作不會影響接收方。
- send self, {:hello, "world"}receive do
- {:hello, msg} -> msg
- {:world, msg} -> "won't match"
- end
Elixir的核心繼承自Erlang,自然就繼承了對OTP(Open Telecom Platform)的支持。OTP是一個很大的課題,包括進程鏈接、監控以及分布式支持(我正在學習《Erlang/OTP并發編程實戰》,希望從Erlang根源上理解OTP)。Elixir對OTP的支持包括Agent、Task、GenServer以及Supervisor與Application。其中,Agent與Task是Elixir對OTP特性的抽象,而GenServer則更加通用。
在Elixir創建OTP服務器非常簡單,只需要use GenServer即可。它主要的方法為handle_call(request, from, state)與handle_cast(request, state)。如果客戶端發送的請求需要響應時,則消息形式為call,如果為單向調用,則形式為cast。
考慮進程的健壯性問題,在編寫OTP應用時,可能還需要對進程進行監督?;贏ctor模型,父進程將負責監督由其創建的所有子進程,下面的代碼是Elixir官方提供的Supervisor代碼:
- defmodule KV.Supervisor do
- use Supervisor
- def start_link do
- Supervisor.start_link(__MODULE__, :ok)
- end
- def init(:ok) do
- children = [
- worker(KV.Registry, [KV.Registry]),
- supervisor(KV.Bucket.Supervisor, [])
- ]
- supervise(children, strategy: :rest_for_one)
- end
- end
KV.Supervisor為監督進程,其子進程分別為KV.Registry與KV.Bucket.Supervisor,監督策略為rest_for_one。
至于分布式支持,在Elixir其實是水到渠成的事情,因為它的核心是進程間通信,而進程所在的節點位置,對于用戶而言是透明的。
模式匹配
模式匹配是Elixir最妖嬈的部分,雖然很多函數式語言都有模式匹配,但Elixir卻把模式匹配融入到其血肉之中(其實是延續了Erlang的模式匹配特色)。即使是一個賦值語句,也是模式匹配的一部分。在Elixir中,=符號其實被稱之為匹配運算符(match operator)。所以你可以寫出違反程序員常規的1 = x:
- iex> 1 = x
- 1
- iex> 2 = x
- ** (MatchError) no match of right hand side value: 1
模式匹配在Elixir中被廣泛地運用到解構(destructuring )復雜的數據結構,例如Tuple、List等。當然case進行的模式匹配更是它最常見的使用場景。
函數與模式匹配的結合才是體現妖嬈性的關鍵點,如果再結合guard clause,那就真正讓人銷魂了。
大多數語言的函數定義是支持函數重載的,這取決于參數的類型、個數與順序。在動態語言中沒有類型,則與個數與順序相關。這些參數在定義時皆為形參(部分語言支持默認參數值,Elixir也支持,甚至可以將表達式作為默認參數),在調用時才傳入實參。
但是,Elixir則不然,因為Elixir沒有賦值的概念,因此在傳遞參數時,并非賦值的語義,而是匹配的語義。因而出現如下的函數定義,你不要感到詫異哦:
- defmodule Factorial do
- def of(0), do: 1
- def of(n), do: n * of(n-1)
- end
在Elixir語義中,這兩個定義實則是同一個函數,當調用of函數時,傳入的參數會與第一個定義進行匹配,如果匹配不成功,則匹配第二個定義。利用這種模式匹配,既可以規避實現上的if分支,又可以更好地體現遞歸的語義。
Meyer非常強調軟件開發中對“契約”的遵循,在他設計的語言Eiffel中,前置條件與后置條件作為了語法糖中的一等公民被支持。Erlang的guard Cluase與Eiffel的前置條件非常相似,Elixir也保留了這一語法特性。例如在前面的階乘算法中,我們可以通過guard clause避免傳入錯誤的負數:
- defmodule Factorial do
- def of(0), do: 1
- def of(n) when n > 0, do: n * of(n-1)
- end
管道運算符
讓Elixir展現其嫵媚一面的,是超級性感的管道運算符。她讓整段代碼瞬間變得可愛起來。有了她,我們就不用再陷入可怕的函數嵌套地獄中了。Dave Long在博文Playing with Elixir Pipes中給出了一個頗有對照意義的例子。代碼功能是從conn取得Request的header,并判斷它是否有效。如果有效就返回conn,否則終止,并返回Not Authorized。
如果沒有管道運算符,就得承受嵌套函數調用的驚悚感:
- signature = List.first(get_req_header(conn, "x-twilio-signature"))
- is_valid = Validator.validate(url_from_conn(conn), conn.params, signature)
- if is_valid do
- conn
- else
- halt(send_resp(conn, 401, "Not authorized"))
- end
這樣的代碼完全違反人類直覺的,因為你得從函數最里邊閱讀,然后再層層往外逃逸。是否有一種被緊緊捆綁了的感覺呢?當然,在很多語言中我們都無奈地接受了這一點,已經被虐得習以為常了。嘗試一下管道運算符,會怎么樣?
- signature = conn
- |> get_req_header("x-twilio-signature")
- |> List.first
- if conn
- |> url_from_conn
- |> Validator.validate(conn.params, signature)
- do
- conn
- else
- conn |> send_resp(401, "Not authorized") |> halt
- end
當你把管道運算符|>看成是goto的話,我們就能直觀地體會到conn在各個函數中流動的現象了。非??蓯?,不是嗎?
Elixir是純正的函數式語言,本質上講,Elixir中的一切皆為函數,所以if表達式其實也是函數。這就意味著validate后的布爾結果可以通過|>直接傳遞給if:
- signature = conn
- |> get_req_header("x-twilio-signature")
- |> List.first
- conn
- |> url_from_conn
- |> Validator.validate(conn.params, signature)
- |> if(do: conn, else: conn |> send_resp(401, "Not authorized") |> halt)
這才是真正Elixir Style的編程范兒,夠嫵媚吧!
Joe Armstrong認為管道運算符來自Prolog語言的隱性基因DCG,類似Haskell中的monad。Prolog的兒子erlang沒有體現這一點,孫子輩又隔代遺傳上了。
工程支持
Elixir的創造者José Valim乃Rails的核心參與者,所以他把Rails社區(包括Ruby社區)中一套讓人目眩的工程實踐照般過來了。
腳手架
通過mix可以直接幫助我們創建項目的腳手架(用過rails的童鞋感到親切了嗎?):
mix new myproject
執行這條命令,mix就會幫我們創建項目的基本結構和相應文件:
包管理與依賴管理
通過Hex來管理包(記得GEM嗎?)。在http://hex.pm中幾乎可以找到所有你想要的elixir包;當然你還可以享受Erlang的福利,直接重用erlang包。
添加依賴也非常方便,只需要在項目的mix.exs文件中添加依賴即可。例如添加HTTPoison和JSX包的依賴:
- defp deps do
- [
- {:httpoison, "~> 0.11.0"},
- {:jsx, "~> 2.8"}
- ]
- end
最棒的是,Elixir還支持直接對github repository的依賴。
環境配置
對開發環境、測試環境、生產環境的配置支持。在config目錄下的config.exs文件中可以添加必要的配置項,還可以通過如下語句import不同環境的配置:
- import_config "#{Mix.env}.exs"
單元測試
還有不能忘記的單元測試,這可是敏捷社區的隨身法寶啊;Elixir通過內嵌的ExUnit很好地支持了單元測試的編寫:
- defmodule MyprojectTest do
- use ExUnit.Case
- doctest Myproject
- test "sort ascending orders the correct way" do
- result = sort_into_ascending_order(fake_created_at_list(["c", "a", "b"]))
- issues = for issue <- result, do: issue["created_at"]
- assert issues == ~w{a b c}
- end
- end
如此簡單。要運行所有測試,只需運行mix test即可。
其他
Elixir還有很多酷炫的玩意兒,例如Protocol、Behavior,當然還有最棒的(當然也可能是最令人費解的)宏(Macro)。Elixir對DSL的支持也非常友好,這來自它繼承的部分Ruby血統。例如,讓我們看看ECTO(一個基于Elixir開發的支持數據庫訪問的框架)的一段客戶代碼:
- defmodule Sample.App do
- import Ecto.Query
- alias Sample.Weather
- alias Sample.Repo
- def keyword_query do
- query = from w in Weather,
- where: w.prcp > 0 or is_nil(w.prcp),
- select: w
- Repo.all(query)
- end
- def pipe_query do
- Weather
- |> where(city: "Kraków")
- |> order_by(:temp_lo)
- |> limit(10)
- |> Repo.all
- end
- end
因為沒有大括號、括號以及分號的干擾,代碼可以變得更接近領域邏輯,再加上性感的管道運算符,可讀性直接爆表,帥呆了!
【本文為51CTO專欄作者“張逸”原創稿件,轉載請聯系原作者】