用 Erlang Process 是 Erlang 最核心的部分。下面将用一个聊天机器人的例子来说明 Erlang Process 的运行机制。
聊天机器人比较有名的是 github 的 hubot,但 hubot 本质是输入到命令的映射,与其说是聊天机器人,不如说是指令机器人。
而聊天机器人首先要有上下文,一个主题会由多句话组成的。现实中会话也可能是混乱的,比如答非所问。最后还应该是具有较好的并发性。
Sequential programming 的方式并不容易处理,但 Erlang process 却很擅长做这样的事。
因为是为了说明 Erlag Process 的,所以功能尽可能简答。
这个机器人会问用户名字和年龄,之后用户可以问机器人自己的名字。
我们直接来看代码
-module(robot).
-export([start/0]).
-export([loop/2]).
start() ->
spawn(robot, loop, [ask_name, #{}]).
loop(ask_name, State) ->
io:format("What's your name?\n"),
receive
{my_name, Name} ->
io:format("Ok, I know your name\n"),
NextState = maps:put(name, Name, State),
loop(ask_age, NextState)
end;
loop(ask_age, State) ->
io:format("How old are you?\n"),
receive
{my_age, Age} ->
io:format("Ok, I know your age.\n"),
NextState = maps:put(age, Age, State),
loop(answer, NextState)
end;
loop(answer, State) ->
io:format("You can ask me question.\n"),
receive
{ my_age_is } ->
Age = maps:get(age, State),
io:format("Your age is ~p~n.", [Age]),
loop(answer, State);
_ ->
loop(answer, State)
end.
start
是程序的入口,spawn 了一个进程,开始进入循环。
1> c(robot).
2> Robot = robot:start().
What's your name?
robot:start spawn 了一个进程,之后这个进程等待我们给它发消息。
我们发条消息,Robot ! {my_age_is}.
Robot 没有任何返回。
这是因为, receive
block 了 Robot 这个进程,同时 my_age_is
不是当前 recieve 要接收的消息。
虽然 process 处于等待状态,但我们仍能给它发消息,也就是说消息的发送是异步的。
如果我们想要同步发消息,只需要把消息传回调用的 process,然后调用的 process 使用 receive 接收返回的消息即可。
用 Erlang 做同步、异步调用还是很简单的。
我们先看正常调用过程。
3> Robot ! {my_name, "Mike"}.
Ok, I know your name
How old are you?
4> Robot ! {my_age, 18}.
Ok, I know your age.
You can ask me question.
6> Robot ! {my_age_is}.
Your age is 18.
我们看到,这个流程是先回答名字、年龄,之后询问 robot 自己的年龄。
那如果先说年龄,再说名字呢?Erlang process 是如何处理的呢?
7> Robot2 = robot:start().
What's your name?
8> Robot2 ! {my_age, 18}.
Robot2 没任何反应,现在输入名字。
10> Robot2 ! {my_name, "Edward"}.
Ok, I know your name
How old are you?
Ok, I know your age.
You can ask me question.
我们看到机器人,收到 my_name
这条消息后,开始问我们年龄,之后告诉我们它知道我们的年龄。
这说明,机器人收到、并记录了 my_age
这条信息,当从 ask_name
这个循环,进入 ask_age
的时候,处理了 my_age
这条消息。
这是因为,每个 Erlang process 有一个 mailbox,所有消息,都会被放入 mailbox 里。
当调用 receive
的时候, receive
会从最先进入 mailbox 的消息进行处理,如果处理不了,则会尝试处理下一条,
如果都不能处理,则等待新的消息。如果可以处理,则执行对应的代码。
如果想查看 mailbox 里有多少消息,可以使用 process_info(Pid).
这条命令。
我们回过头再仔细看一下代码,会发现,代码很简单,也很有趣。
进入第一个 loop
的 recieve
的时候,Robot 这个 process 只会处理 my_name
这条消息,
处理完这条消息后,进入下一个 loop
,这个时候只会处理 my_age
。
这就是说,Erlang process 会等待执行一段代码,随着状态的变化,需要执行的代码也会随着变化。
这个也是和 sequential programming 最不同的一点。sequential programming 是逻辑的集合,而 concurrent programming 是运行代码的集合。
函数式编程最核心的想法是让状态的变化变得透明,比如 Clojure 的变量就是不可变的。但现实并非如此美好,因为现实世界是有状态的。
用 Clojure 处理有状态的问题的时候,会隐隐的蛋痛。而且 Clojure 和 Erlang 比起来也不那么纯粹,因为变量可以多次赋值,同样让状态的流转变得模糊。 再加上 hash-map 漫天飞(好吧我跑题了)。。。
回头再看面向对象,面向对象非常擅长处理状态,对象内部的状态对于外部是一个黑盒,用起来非常方便。但对象的状态对内部状态依旧是模糊的,并不容易 debug。
而 Erlang process 可以很好处的处理这两个问题。
我们把 Erlang 变一变,把!改为 send,然后再换一种语法,robot.send({my_name, "Mike"})
,这其实就是面向对象的方法调用啊。
process 有自己的状态,对外部是个黑盒,外部的调用都是通过消息传递(吐槽下 Python、Java,为什么有方法的同时还要有 field 这种东西,学学 ruby)。 process 自身知道如果处理这些消息。
Erlang 又是函数式的,当状态 (State) 变化了,就要生成一个新状态 (NewState),然后再把这个新状态传到下一个 loop 里。也就是状态的流转完全可见。
debug 的时候,只要知道当前状态,传入对对应的方程,对对应的方法最调试就可以了,非常清晰。
Erlang 的 process 对外是黑盒,方便条用,对内是黑盒,方便维护和 debug。
最后要要如何学 Erlang 呢?