(譯文)如何開始學習Elixir

傳送門!

傳送門是這樣一款遊戲:經過往不一樣地點傳送玩家人物或簡單物品來解迷。玩家使用傳送槍往相似地板或牆的平面上射擊,製造出能夠進入的傳送門:html

portal-drop.jpeg

本教程將會使用Elixir來實現這樣的傳送門:咱們將使用不一樣的顏色來創造門,並在它們之間傳送數據!甚至還將學習如何經過網絡在不一樣的機器上建造門。shell

portal-list.jpeg

咱們將學到: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

一個匿名函數由fnend包圍,而且用->箭頭來分離參數與函數體。咱們使用匿名函數來初始化,獲取和更新代理狀態:

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,以獲取leftright門的文本表示,也就是獲取門內數據的表示。最終,咱們返回了一個帶有對齊好了的傳送門表示的字符串。

啓動另外一個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@jvroom1是咱們給節點的名稱,jv是節點所在的計算機的網絡名。在這裏,個人機器名稱是jv,而你會有不一樣的結果。在後面,咱們將看到room1@COMPUTER-NAMEroom2@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_leftpush_right間的代碼重複?

  • 學習更多關於Elixir的測試框架,ExUnit的內容,併爲咱們剛纔建立的功能編寫測試。記住咱們在test目錄中已經有了一個默認框架。

  • 使用ExDoc爲你的項目生成HTML文檔。

  • 將你的代碼上傳到相似Github的網站,並使用Hex包管理工具發佈包。

歡迎到咱們的網站閱讀更多關於Elixir的教程。

最後,感謝 Augie De Blieck Jr. 的插圖。

回見!

相關文章
相關標籤/搜索