假期7天學(xué)會Elixir,掌握函數(shù)式編程與 Actor 模型
為什么你一定需要學(xué)習(xí) Elixir?
Elixir 是一門基于 erlang 開發(fā)的新語言,復(fù)用了 erlang 的虛擬機(jī)以及全部庫(站在已經(jīng)生存了20多年巨人的肩膀上),定義了全新的語法以及構(gòu)造了現(xiàn)代語言必不可少生態(tài)環(huán)境—包管理器,測試工具,formatter等。使用 Elixir,你可以方便的構(gòu)建可用性高達(dá)99.9999以及天然分布式的程序(代碼隨手一寫就是穩(wěn)定的分布式),可以秒開成千上萬 Elixir 里專屬的進(jìn)程(比起系統(tǒng)的進(jìn)程更輕量級),處理高并發(fā)請求等等。
Elixr/Erlang 天然支持分布式, 而作者 Joe Armstrong 在論文中是這樣認(rèn)為的:幾乎所有傳統(tǒng)的編程語言對真正的并發(fā)都缺乏有力的支持 —— 本質(zhì)上是順序化的,而語言的并發(fā)性都僅僅由底層操作系統(tǒng)而不是語言提供。而用對并發(fā)提供良好支持的語言(也就是作者說的面向并發(fā)的語言 Concurrency Oriented Language) 來邊寫程序,則是相當(dāng)容易的:
- 從真實世界的活動中識別出真正的并發(fā)活動
- 識別出并發(fā)活動之間的所有消息通道
- 寫下能夠在不同消息通道中流通的所有消息
Elixir 是怎么樣的語言?
Elixir 是函數(shù)式語言,與 java,C++等過程式語言不通,沒有變量?;蛘哒f,變量全都imutable(不可改變)。通過學(xué)習(xí)Elixir, 你可以學(xué)習(xí)多一種編程范式。
python 中你是這樣子處理列表的:
- mylist = []
- mylist.append('Google')
- mylist.append('Facebook')
- print mylist #結(jié)果是['Google', 'Facebook']
Elixir 中是這樣子的:
- myList = []
- myList = List.insert_at(myList, 0, "Google")
- myList = List.insert_at(myList, 1, "Facebook")
- IO.inspect myList
elixir 中這不是正常的寫法,不過我只是用這些例子來介紹異同點。
注意到,在面向?qū)ο笏季S的語言中,處理列表,是 用對象的方法,mylist.append: 對象.動作來處理;而函數(shù)式因為變量是不可變的,是要 List.append(mylist, xx), 對象模塊.動作(哪個對象)來處理,同時會返回修改后的新對象。
數(shù)據(jù)不可變,好處就是在高并發(fā)中,并不會因為狀態(tài)多且不斷變化,引致debug 異常困難——本來人的大腦就不適應(yīng)多線程。
不可變,就意味著 for 與 while循環(huán)用不了,因為不存在變量 不斷 變化,達(dá)到某值就中止循環(huán)~因此,你只能用遞歸來實現(xiàn) while。
但是不怕,Elixir 提供了強(qiáng)大無比的抽象, each 函數(shù),map 函數(shù),reduce 函數(shù),all? 函數(shù)(判斷列表所有值是否滿足此條件),group 函數(shù)(類似數(shù)據(jù)庫的 group) 等等,只有你想不到。相比之下,golang 真的是乏善可陳。
Go 語言的創(chuàng)始人之一,Unix 老牌黑客羅勃?派克(RobPike)的忠告:
所以說,沒有泛型就是不行的。。。
管道
是的,類似 linux 的管道 |,把處理結(jié)果傳遞給下一個函數(shù)。
- 1..100
- |> Enum.map(fn x-> x+1 end)
- |> Enum.filter(fn x-> rem(x, 2)==0 end)
- |> Enum.filter(fn x-> rem(x, 3)==0 end)
- |> Enum.filter(fn x-> rem(x, 5)==0 end)
- |> IO.inspect
與以下的 代碼相比,python是否相形見絀?
- numbers = range(1, 100)
- numbers = map( (lambda x: x+1), numbers )
- numbers = filter( (lambda x: x%2 == 0), numbers )
- numbers = filter( (lambda x: x%3 == 0), numbers )
- numbers = filter( (lambda x: x%5 == 0), numbers )
- print(numbers)
再來一個例子,來自Dave Long 的博客 Playing with Elixir Pipes[1] :
代碼的作用是:取出請求的頭部 x-twilio-signature 簽名,并且校驗是否有效。
沒有管道時,代碼是這樣子的:
- 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
邏輯就非常清晰了。還可以這樣子寫:
- 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)
進(jìn)程 Actor Model
輕量級的進(jìn)程
在 Elixir 里,Elixir進(jìn)程(以下簡稱進(jìn)程,與系統(tǒng)進(jìn)程區(qū)分開)是輕量級的進(jìn)程,與操作系統(tǒng)的概念相差不多,只不過 Elixir 進(jìn)程運行在虛擬機(jī)中。那為什么 Elixir 進(jìn)程更快呢?
- Erlang 進(jìn)程的堆棧是動態(tài)分配、隨使用增長的,新建一個 Erlang 進(jìn)程的開銷遠(yuǎn)比系統(tǒng)進(jìn)程 / 線程小得多,開銷就像在 OO 語言中建立一個新對象般簡單。
- 普通進(jìn)程 / 線程的內(nèi)存管理是基于頁的,而頁對于一個函數(shù) + 一點點零碎來說都太大了。而實際中 OS 分配給普通進(jìn)程的初始??梢赃_(dá)到 Megabytes 級別。
- Erlang 進(jìn)程之間是隔離的,沒有共享狀態(tài),所有的消息都是異步的,不會繼承大量的已有狀態(tài)。
- Erlang 進(jìn)程的調(diào)度是在 Erlang VM 內(nèi)發(fā)生的,跟 OS 層沒啥關(guān)系,無需普通進(jìn)程 / 線程切換時的各種開銷
- Erlang 進(jìn)程的切換是一種類似直接 “跳轉(zhuǎn)” 的方式,以 O(1) 復(fù)雜度實現(xiàn)。Erlang 調(diào)度器會管理這些切換,大概只需要幾十個指令和數(shù)十納秒的時間。普通線程的切換會需要數(shù)百上前納秒,OS 調(diào)度器的運作復(fù)雜度可能是 O(logn) 或者 O(log(logn))。如果有上萬個線程,這個時間將會大幅提升。來自知乎[2]
像指揮交響樂隊一樣,指揮你的 Elixir 進(jìn)程
對于Elixir 進(jìn)程,你可以方便的用一個進(jìn)程(supervisor)去管理子進(jìn)程,supervisor會根據(jù)你設(shè)定的策略,來處理意外掛掉的子進(jìn)程(這種情況不多的是,錯誤處理稍微做不好就會掛) , 策略有:
- one_for_one:只重啟掛掉的子進(jìn)程
- one_for_all:有一個子進(jìn)程掛了,重啟所有子進(jìn)程
- rest_for_one:在該掛掉的子進(jìn)程 創(chuàng)建時間之后創(chuàng)建的子進(jìn)程都會重啟。
老夫敲代碼就是一把梭!可不,只要重啟就行。
實質(zhì)上,這是有論文支持的: 在復(fù)雜的產(chǎn)品系統(tǒng)中,幾乎所有的故障和錯誤都是暫態(tài)的,對某個操作進(jìn)行重試是一種不錯地解決問題方法——Jim Gray的論文[3]中指出,使用這種方法處理暫態(tài)故障,系統(tǒng)的平均故障間隔時間(MTBF)提升了 4 倍。
因此,你就可以創(chuàng)建一課監(jiān)控樹,根節(jié)點就是啥事都不做,只負(fù)責(zé)監(jiān)控的進(jìn)程。其他都是它的子進(jìn)程,如果不是 coredump(幾乎不發(fā)生),那么根節(jié)點就不可能會掛;因此其他子進(jìn)程就會正確的被處理。
當(dāng)然,這有前提:5 秒內(nèi)重啟超于 3 次,就會不再重啟,讓進(jìn)程掛掉。為什么呢?因為重啟是為了讓進(jìn)程回到當(dāng)初啟動時的穩(wěn)定態(tài),既然穩(wěn)定態(tài)都不穩(wěn)定了,重復(fù)做重啟是沒有意義的,這時迫切需要人來處理。
方便的通信
一切皆消息。
進(jìn)程間通信,就像微風(fēng)一樣自然。你所監(jiān)管的進(jìn)程而來的信息,調(diào)用的庫的消息,全部都可以自己來 handle 并作相應(yīng)處理。甚至還有抽象好的 GenServer 來讓你專門處理消息與狀態(tài)邏輯。
定時器?不需要的,我們甚至可以自己發(fā)送消息來實現(xiàn)更好的定時器:
Process.send_after 會在 xx 秒后發(fā)消息到指定的進(jìn)程,通過這個功能,不斷往自己發(fā)消息,從而實現(xiàn)定時器的功能。請看實現(xiàn):
- defmodule Periodically do
- require Logger
- use GenServer
- def start_link do
- GenServer.start_link(__MODULE__, %{})
- end
- def init(state) do
- schedule_work(:do_some_work)
- {:ok, state}
- end
- def handle_info(:do_some_work, state) do
- doing_now()
- schedule_work(:do_some_work)
- {:noreply, state}
- end
- defp schedule_work(update_type) do
- Process.send_after(self(), update_type, 30*1000)
- end
- end
相較于 setTimeOut 之類的,好處是什么?
Elixir 自帶工具,可以查看所有進(jìn)程的狀態(tài)并管理,上面把 Periodically 作為一個進(jìn)程啟動起來了,自然可以管理他:P
let it crash 思想的提出與實現(xiàn)。
程序不可能處理一切錯誤,因此程序員只要力所能及的處理顯然易見的錯誤就好了,而那些隱藏著的,非直覺性的錯誤,就讓他崩掉吧 —— 本來就很有可能是極少見的錯誤,經(jīng)常出現(xiàn)的?就需要程序員人工處理了,這是緊急情況,就算 try catch 所有錯誤也無法避免,因為系統(tǒng)已經(jīng)陷入崩潰邊緣了,茍延殘喘下去只是自欺欺人。
要注意的是 let it crash ,并不是說你用 golang,C++ 等語言打包個二進(jìn)制,用 supervisorctl 等工具監(jiān)控,錯誤就讓他馬上崩,掛了自動重啟 - = -
模式匹配與宏
這個相較于平常我們的賦值語言比較新穎,介紹的篇幅過長。
請看http://szpzs.oschina.io/2017/01/30/elixir-getting-started-pattern-matching/ 以及https://elixir-lang.org/getting-started/pattern-matching.html
通過模式匹配,我們可以避免 if else 的嵌套地獄;可以利用語言自己的匹配來做 搜索,
宏可以讓你實現(xiàn)自定義的 DSL(當(dāng)然太強(qiáng)大的功能自然導(dǎo)致濫用出 bug),可以屏蔽掉很多不優(yōu)雅的細(xì)節(jié)。
以上就是 Elixir 的簡單介紹,建議諸位學(xué)習(xí)一下 Elixir,洗刷一下自己的 OO 以及過程式編程思維。
本文轉(zhuǎn)載自微信公眾號「山盡寫東西的cache」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系山盡寫東西的cache公眾號。