传送门是这样一款游戏:经过往不一样地点传送玩家人物或简单物品来解迷。玩家使用传送枪往相似地板或墙的平面上射击,制造出能够进入的传送门: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. 的插图。
回见!