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