(译文)如何开始学习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. 的插图。

回见!

相关文章
相关标签/搜索