傳送門是這樣一款遊戲:經過往不一樣地點傳送玩家人物或簡單物品來解迷。玩家使用傳送槍往相似地板或牆的平面上射擊,製造出能夠進入的傳送門:html
本教程將會使用Elixir來實現這樣的傳送門:咱們將使用不一樣的顏色來創造門,並在它們之間傳送數據!甚至還將學習如何經過網絡在不一樣的機器上建造門。shell
咱們將學到:bash
Elixir的shell交互cookie
建立新的Elixir應用網絡
模式匹配數據結構
爲狀態使用代理框架
自定義結構體編輯器
使用協議來擴展語言分佈式
監督樹和應用ide
分佈式Elixir節點
讓咱們開始吧!
Elixir官網上有詳細的安裝教程,只須要跟隨其中的步驟就能完成安裝。安裝好以後,你的終端裏就會多了一些命令。iex
就是其中之一。輸入iex
便可運行:
$ iex Interactive Elixir - press Ctrl+C to exit (type h() ENTER for help)
iex
表明Elixir交互,你能夠輸入任何表達式並獲得結果:
iex> 40 + 2 42 iex> "hello" <> " world" "hello world" iex> # This is a code comment nil
你還可使用以下數據類型:
iex> :atom # An identifier (known as Symbols in other languages) :atom iex> [1, 2, "three"] # Lists (typically hold a dynamic amount of items) [1, 2, "three"] iex> {:ok, "value"} # Tuples (typically hold a fixed amount of items) {:ok, "value"}
在完成了咱們的傳送門應用後,就能夠在iex
中輸入以下命令:
# Shoot two doors: one orange, another blue iex(1)> Portal.shoot(:orange) {:ok, #PID<0.72.0>} iex(2)> Portal.shoot(:blue) {:ok, #PID<0.74.0>} # Start transferring the list [1, 2, 3, 4] from orange to blue iex(3)> portal = Portal.transfer(:orange, :blue, [1, 2, 3, 4]) #Portal< :orange <=> :blue [1, 2, 3, 4] <=> [] > # Now every time we call push_right, data goes to blue iex(4)> Portal.push_right(portal) #Portal< :orange <=> :blue [1, 2, 3] <=> [4] >
看上去不錯!
Elixir搭載了一個叫作Mix的工具。Mix可用於建立,編譯和測試新項目。讓咱們使用mix
來建立一個名爲portal
的項目。在建立時咱們加上--sup
選項,這樣會一同建立一個監督樹。在後面的部分咱們將講解監督樹的做用。如今只須要輸入:
$ mix new portal --sup
上面的命令創造了一個名爲portal
的新目錄,以及其中的一些文件。將工做目錄移動到portal
並運行mix test
來啓動項目測試:
$ cd portal $ mix test
很好,如今咱們已經有了一個工做項目和一套測試。
讓咱們使用編輯器打開項目。我通常使用Sublime Text 3,你也能夠選擇你喜歡的編輯器,只要它支持Elixir。
在編輯器中查看如下文件:
_build
- Mix在此存放編譯後的文件
config
- 項目和依賴的配置文件
lib
- 存放代碼
mix.exs
- 定義項目名稱,版本和依賴
test
- 定義測試
如今咱們能夠在項目中啓動一個iex
會話。只須要輸入:
$ iex -S mix
在實現咱們的應用以前,先來聊聊模式匹配。Elixir中的=
號和其它語言中的有所不一樣:
iex> x = 1 1 iex> x 1
還不錯,若是咱們翻轉表達式,會發生什麼?
iex> 1 = x 1
成功了!這是由於Elixir試圖將右邊的匹配到左邊。因爲兩邊都是1
,因此可以運做。讓咱們試試其它的:
iex> 2 = x ** (MatchError) no match of right hand side value: 1
如今不匹配了,因此獲得了一個錯誤。在Elixir中咱們也對數據結構進行模式匹配。例如,咱們可使用[head|tail]
來獲取一個列表的頭部(第一個元素)和尾部(其他的部分)。
iex> [head|tail] = [1, 2, 3] [1, 2, 3] iex> head 1 iex> tail [2, 3]
使用[head|tail]
匹配空列表會形成匹配錯誤:
iex> [head|tail] = [] ** (MatchError) no match of right hand side value: []
最後,咱們也可使用[head|tail]
表達式來向列表的頭部添加元素:
iex> list = [1, 2, 3] [1, 2, 3] iex> [0|list] [0, 1, 2, 3]
Elixir數據結構是不可變的。在上面的例子中,咱們沒有改變列表。咱們能夠打碎一個列表或往頭部添加元素,但原始的列表是不變的。
也就是說,當咱們須要保持某種狀態,例如經過傳送門傳送數據,咱們必須使用某種抽象來存儲這個狀態。Elixir中這樣的抽象之一叫作代理。在使用代理以前,咱們須要簡短地聊聊匿名函數:
iex> adder = fn a, b -> a + b end #Function<12.90072148/2 in :erl_eval.expr/5> iex> adder.(1, 2) 3
一個匿名函數由fn
和end
包圍,而且用->
箭頭來分離參數與函數體。咱們使用匿名函數來初始化,獲取和更新代理狀態:
iex> {:ok, agent} = Agent.start_link(fn -> [] end) {:ok, #PID<0.61.0>} iex> Agent.get(agent, fn list -> list end) [] iex> Agent.update(agent, fn list -> [0|list] end) :ok iex> Agent.get(agent, fn list -> list end) [0]
注意:你可能會獲得與例子中不一樣的
#PID<...>
,這是正常的!
上述例子中,咱們建立了一個新的代理,傳送了一個用於返回初始狀態空列表的函數。這個代理反悔了{:ok, #PID<0.61.0>}
。
Elixir中的花括號表明元組;上面的元組包含了原子:ok
和一個進程辨識符(PID)。咱們使用原子做爲標籤。上述例子中,咱們將代理標記爲成功啓動。
#PID<...>
是代理的進程辨識符。當咱們談論Elixir中的進程時,咱們不是在說操做系統的進程,Elixir的進程是輕量且獨立的,容許咱們在同一臺機器上運行數十萬的進程。
咱們將代理的PID存放在變量agent
中,這樣咱們就能經過發送信息來獲取和更新代理的狀態。
咱們將使用代理來實現咱們的傳送門。使用以下內容建立一個名爲lib/portal/door.ex
的文件:
defmodule Portal.Door do @doc """ Starts a door with the given `color`. The color is given as a name so we can identify the door by color name instead of using a PID. """ def start_link(color) do Agent.start_link(fn -> [] end, name: color) end @doc """ Get the data currently in the `door`. """ def get(door) do Agent.get(door, fn list -> list end) end @doc """ Pushes `value` into the door. """ def push(door, value) do Agent.update(door, fn list -> [value|list] end) end @doc """ Pops a value from the `door`. Returns `{:ok, value}` if there is a value or `:error` if the hole is currently empty. """ def pop(door) do Agent.get_and_update(door, fn [] -> {:error, []} [h|t] -> {{:ok, h}, t} end) end end
在Elixir中,咱們把代碼定義在模塊裏,它是一個基本的函數羣。咱們定義了四個函數,而且都寫好了文檔。
讓咱們來測試一下。用iex -S mix
開啓新會話。啓動時咱們的新文件會自動被編譯,因此咱們能夠直接使用:
iex> Portal.Door.start_link(:pink) {:ok, #PID<0.68.0>} iex> Portal.Door.get(:pink) [] iex> Portal.Door.push(:pink, 1) :ok iex> Portal.Door.get(:pink) [1] iex> Portal.Door.pop(:pink) {:ok, 1} iex> Portal.Door.get(:pink) [] iex> Portal.Door.pop(:pink) :error
很好!
Elixir中一個頗有趣的地方就是文檔被當作一等公民。因爲咱們已經爲Portal.Door
代碼寫好了文檔,咱們能夠從終端中輕鬆地獲取到文檔。試試:
iex> h Portal.Door.start_link
是時候爲咱們的傳送門開啓傳送了!爲了存儲傳送數據,咱們將創造一個名爲Portal
的就夠。讓咱們如今IEx中嘗試一下結構:
iex> defmodule User do ...> defstruct [:name, :age] ...> end iex> user = %User{name: "john doe", age: 27} %User{name: "john doe", age: 27} iex> user.name "john doe" iex> %User{age: age} = user %User{name: "john doe", age: 27} iex> age 27
結構在模塊中定義,並有着和模塊相同的名字。在結構被定義後,咱們可使用%User{...}
形式來定義新的結構或是匹配它們。
讓咱們打開lib/portal.ex
並添加如下代碼到Portal
模塊。注意當前的Portal
模塊已經有一個名爲start/2
的函數。不要刪除這個函數,咱們將在後面討論它,如今你只須要添加新的內容到Portal
模塊:
defstruct [:left, :right] @doc """ Starts transfering `data` from `left` to `right`. """ def transfer(left, right, data) do # First add all data to the portal on the left for item <- data do Portal.Door.push(left, item) end # Returns a portal struct we will use next %Portal{left: left, right: right} end @doc """ Pushes data to the right in the given `portal`. """ def push_right(portal) do # See if we can pop data from left. If so, push the # popped data to the right. Otherwise, do nothing. case Portal.Door.pop(portal.left) do :error -> :ok {:ok, h} -> Portal.Door.push(portal.right, h) end # Let's return the portal itself portal end
咱們已經定義了Portal
結構和一個Portal.transfer/3
函數(/3
代表該函數須要三個參數)。讓咱們來試試。用iex -S mix
啓動一個新會話,這樣咱們的改動就會被編譯,而後輸入:
# Start doors iex> Portal.Door.start_link(:orange) {:ok, #PID<0.59.0>} iex> Portal.Door.start_link(:blue) {:ok, #PID<0.61.0>} # Start transfer iex> portal = Portal.transfer(:orange, :blue, [1, 2, 3]) %Portal{left: :orange, right: :blue} # Check there is data on the orange/left door iex> Portal.Door.get(:orange) [3, 2, 1] # Push right once iex> Portal.push_right(portal) %Portal{left: :orange, right: :blue} # See changes iex> Portal.Door.get(:orange) [2, 1] iex> Portal.Door.get(:blue) [3]
一對傳送門彷佛能夠運做了。注意在左邊/橙色門中的數據是反向的。這正是咱們所預期的,由於咱們須要讓列表的末尾(這裏是數字3)成爲第一個進入右邊/藍色門的數據。
還有一點與教程開頭咱們所看到的不一樣,那就是咱們的傳送如今是以結構的形式展示%Portal{left: :orange, right: :blue}
。若是咱們須要打印傳送過程,這能幫助咱們查看傳送的進程。
那就是咱們接下來要作的。
咱們已經知道數據能夠被打印在iex
中。當咱們在iex
中輸入1 + 2
,咱們獲得了3
。那麼,咱們是否能夠自定義返回的格式呢?
是的,咱們能夠!Elixir提供了協議,它能爲任何數據類型擴展並實現某種行爲。
例如,每當有iex
上打印一些東西,Elixir都使用了Inspect
協議。因爲協議能夠在任什麼時候候擴展到任何的數據類型,這就意味着咱們能夠爲Portal
實現它。打開lib/portal.ex
,在文件的末尾,在Portal
模塊以外,添加:
defimpl Inspect, for: Portal do def inspect(%Portal{left: left, right: right}, _) do left_door = inspect(left) right_door = inspect(right) left_data = inspect(Enum.reverse(Portal.Door.get(left))) right_data = inspect(Portal.Door.get(right)) max = max(String.length(left_door), String.length(left_data)) """ #Portal< #{String.pad_leading(left_door, max)} <=> #{right_door} #{String.pad_leading(left_data, max)} <=> #{right_data} > """ end end
咱們爲Portal
結構實現了Inspect
協議。該協議只實現了一個名爲inspect
的函數。函數須要兩個參數,第一個是Portal
結構自己,第二個是選項,咱們暫時不用管它。
而後咱們屢次調用inspect
,以獲取left
和right
門的文本表示,也就是獲取門內數據的表示。最終,咱們返回了一個帶有對齊好了的傳送門表示的字符串。
啓動另外一個iex
會話,來查看咱們新的表示:
iex> Portal.Door.start_link(:orange) {:ok, #PID<0.59.0>} iex> Portal.Door.start_link(:blue) {:ok, #PID<0.61.0>} iex> portal = Portal.transfer(:orange, :blue, [1, 2, 3]) #Portal< :orange <=> :blue [1, 2, 3] <=> [] >
咱們常常聽到Erlang VM,也就是Elixir所運行於的虛擬機,以及Erlang生態系統很善於構建容錯率高的應用。其中的一個緣由就是監督樹機制。
咱們的代碼尚未被監督。讓咱們來看看當咱們關閉了一個門的代理時會發生什麼:
# Start doors and transfer iex> Portal.Door.start_link(:orange) {:ok, #PID<0.59.0>} iex> Portal.Door.start_link(:blue) {:ok, #PID<0.61.0>} iex> portal = Portal.transfer(:orange, :blue, [1, 2, 3]) # First unlink the door from the shell to avoid the shell from crashing iex> Process.unlink(Process.whereis(:blue)) true # Send a shutdown exit signal to the blue agent iex> Process.exit(Process.whereis(:blue), :shutdown) true # Try to move data iex> Portal.push_right(portal) ** (exit) exited in: :gen_server.call(:blue, ..., 5000) ** (EXIT) no process (stdlib) gen_server.erl:190: :gen_server.call/3 (portal) lib/portal.ex:25: Portal.push_right/1
咱們獲得了一個錯誤,由於這裏沒有:blue
門。你能夠在咱們的函數調用以後看到一個** (EXIT) no process
信息。爲了解決這個問題,咱們須要設置一個能在傳送門崩潰以後自動重啓它們的監督者。
還記得咱們在建立項目時設附帶的--sup
標誌嗎?咱們附帶了這個標誌,是由於監督者一般運行在監督樹中,而監督樹一般做爲應用的一部分啓動。--sup
的默認做用就是建立一個被監督的結構,咱們能夠再Portal
模塊中看到:
defmodule Portal do use Application # See http://elixir-lang.org/docs/stable/elixir/Application.html # for more information on OTP Applications def start(_type, _args) do import Supervisor.Spec, warn: false children = [ # Define workers and child supervisors to be supervised # worker(Portal.Worker, [arg1, arg2, arg3]) ] # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html # for other strategies and supported options opts = [strategy: :one_for_one, name: Portal.Supervisor] Supervisor.start_link(children, opts) end # ... functions we have added ... end
上述代碼將Portal
模塊變成了一個應用回調。應用回調必須提供一個名爲start/2
的函數,該函數必須啓動一個表明監督樹根部的監督者。如今咱們的監督者尚未一個子進程,而稍後咱們將改變它。
將start/2
函數替換爲:
def start(_type, _args) do import Supervisor.Spec, warn: false children = [ worker(Portal.Door, []) ] opts = [strategy: :simple_one_for_one, name: Portal.Supervisor] Supervisor.start_link(children, opts) end
咱們作了兩處改動:
咱們爲監督者添加了一個子進程,類型爲worker
,由Portal.Door
模塊來表達。咱們沒有傳送任何參數給工人,只是一個空列表[]
,由於門的顏色會在稍後被指定。
咱們將策略由:one_for_one
改成了:simple_one_for_one
。監督者提供了不一樣的策略,當咱們想用不一樣的參數,動態地創造子進程時,:simple_one_for_one
是最合適的。而咱們正想用不一樣的顏色來生成不一樣的門。
最後咱們要添加一個名爲shoot/1
的函數到Portal
模塊中,它會接收一個顏色並生成一個新的門做爲監督樹的一部分:
@doc """ Shoots a new door with the given `color`. """ def shoot(color) do Supervisor.start_child(Portal.Supervisor, [color]) end
上述函數訪問了名爲Portal.Supervisor
的監督者,並請求啓動一個新的子進程。Portal.Supervisor
是咱們在statr/2
中所定義的監督者的名字,而子進程會是咱們在那個監督者中所指定的工人Portal.Door
。
在內部,爲了啓動子進程,監督者會調用Portal.Door.start_link(color)
,顏色正是咱們傳送給start_child/2
的參數。若是咱們調用了Supervisor.start_child(Portal.Supervisor, [foo, bar, baz])
,監督者將會試圖啓動一個Portal.Door.start_link(foo, bar, baz)
做爲子進程。
讓咱們試用一下。啓動新的iex -S mix
會話,並輸入:
iex> Portal.shoot(:orange) {:ok, #PID<0.72.0>} iex> Portal.shoot(:blue) {:ok, #PID<0.74.0>} iex> portal = Portal.transfer(:orange, :blue, [1, 2, 3, 4]) #Portal< :orange <=> :blue [1, 2, 3, 4] <=> [] > iex> Portal.push_right(portal) #Portal< :orange <=> :blue [1, 2, 3] <=> [4] >
若是咱們中止:blue
進程會發生什麼?
iex> Process.unlink(Process.whereis(:blue)) true iex> Process.exit(Process.whereis(:blue), :shutdown) true iex> Portal.push_right(portal) #Portal< :orange <=> :blue [1, 2] <=> [3] >
注意到這一次push_right/1
操做成功了,由於監督者自動啓動了另外一個:blue
傳送門。不幸的是藍色門中的數據丟失了。
在實踐中,咱們能夠選擇其它的監督策略,包括在崩潰時保留數據。
很好!
咱們的傳送門已經能夠工做,準備好嘗試一下分佈式傳送了。若是你在同一個網絡中的兩臺機器上運行這些代碼,結果會很是酷。若是你手頭沒有另外一臺機器,它也能夠運行。
咱們能夠在啓動iex
會話時傳送--sname
選項使其變爲網絡中的一個節點。來試試:
$ iex --sname room1 --cookie secret -S mix Interactive Elixir - press Ctrl+C to exit (type h() ENTER for help) iex(room1@jv)1>
你會發現這個iex
與以前的有所不一樣。如今你會看到提示符中的room1@jv
,room1
是咱們給節點的名稱,jv
是節點所在的計算機的網絡名。在這裏,個人機器名稱是jv
,而你會有不一樣的結果。在後面,咱們將看到room1@COMPUTER-NAME
和room2@COMPUTER-NAME
,而你必須將COMPUTER-NAME
替換爲你本身的電腦名。
在room1
會話中,讓咱們射出一個:blue
門:
iex(room1@COMPUTER-NAME)> Portal.shoot(:blue) {:ok, #PID<0.65.0>}
啓動另外一個iex
會話,名爲room2
:
$ iex --sname room2 --cookie secret -S mix
注意:兩臺電腦上的cookie必須相同,這樣Elixir節點才能夠溝通。
代理API容許咱們進行跨節點請求。當調用Portal.Door
時,咱們只須要提供想要鏈接到的代理所運行於的節點名。例如,讓咱們從room2
訪問藍色門:
iex(room2@COMPUTER-NAME)> Portal.Door.get({:blue, :"room1@COMPUTER-NAME"}) []
這意味着咱們已經能夠簡單地使用節點名來分佈式傳送了。在room2
中繼續輸入:
iex(room2@COMPUTER-NAME)> Portal.shoot(:orange) {:ok, #PID<0.71.0>} iex(room2@COMPUTER-NAME)> orange = {:orange, :"room2@COMPUTER-NAME"} {:orange, :"room2@COMPUTER-NAME"} iex(room2@COMPUTER-NAME)> blue = {:blue, :"room1@COMPUTER-NAME"} {:blue, :"room1@COMPUTER-NAME"} iex(room2@COMPUTER-NAME)> portal = Portal.transfer(orange, blue, [1, 2, 3, 4]) #Portal< {:orange, :room2@COMPUTER-NAME} <=> {:blue, :room1@COMPUTER-NAME} [1, 2, 3, 4] <=> [] > iex(room2@COMPUTER-NAME)> Portal.push_right(portal) #Portal< {:orange, :room2@COMPUTER-NAME} <=> {:blue, :room1@COMPUTER-NAME} [1, 2, 3] <=> [4] >
太棒了。咱們沒有修改一行基礎代碼就實現了分佈式的傳送!
儘管room2
在管理傳送,咱們也能夠從room1
中查看到傳送:
iex(room1@COMPUTER-NAME)> orange = {:orange, :"room2@COMPUTER-NAME"} {:orange, :"room2@COMPUTER-NAME"} iex(room1@COMPUTER-NAME)> blue = {:blue, :"room1@COMPUTER-NAME"} {:blue, :"room1@COMPUTER-NAME"} iex(room1@COMPUTER-NAME)> Portal.Door.get(orange) [3, 2, 1] iex(room1@COMPUTER-NAME)> Portal.Door.get(blue) [4]
咱們的分佈式傳送門之因此可以工做,是由於門只是進程,而對門訪問/推送數據,都是由代理API向這些進程發送信息來完成的。咱們說在Elixir中發送信息是位置透明的:咱們能夠對任何PID發送信息,不管是否和發送者在同一個節點。
咱們已經完成了這個「如何開始使用Elixir」的教程!這是一次有趣的經歷,咱們從建立門進程,講到了到高容錯性的門和分佈式的傳送!
完成如下挑戰,你的傳送門應用能夠更進一步:
添加一個Portal.push_left/1
函數,反方向傳送數據。你將如何處理push_left
和push_right
間的代碼重複?
學習更多關於Elixir的測試框架,ExUnit的內容,併爲咱們剛纔建立的功能編寫測試。記住咱們在test
目錄中已經有了一個默認框架。
使用ExDoc爲你的項目生成HTML文檔。
將你的代碼上傳到相似Github的網站,並使用Hex包管理工具發佈包。
歡迎到咱們的網站閱讀更多關於Elixir的教程。
最後,感謝 Augie De Blieck Jr. 的插圖。
回見!