Skip to content

七周七语言#Erlang #3

@Weiting-Zhang

Description

@Weiting-Zhang

比较零散的阅读笔记

Erlang

2018.9.2 改变结构

  • 函数式:Erlang 的列表(list)的一些比较函数式的方法除了有常规的 filter, map, foldl, foldr 等之外,今天还看到两个比较特殊的:takewhiledropwhile。其实也比较简单,是用来根据条件处理或舍弃“头”(head) 的:

    1> Small = fun(X) -> X < 3 end.
    #Fun<erl_eval.6.127694169>
    2> lists:takewhile(Small, [1, 2, 1, 4, 1).
    [1, 2, 1]
    3> lists:dropwhile(Small, [1, 2, 1, 4, 1).
    [4, 1]
  • 列表构造:之前看的一些其他函数式编程(没有可变状态)的语言是通过返回一个添加了新元素的列表,比如 sml,是用 newelement::oldlist 来返回一个 newlist 的,会将新元素添加到列表头。而 Erlang 的列表构造却和它的列表映射一样,同样用到了“模式匹配”,只不过是在匹配的右边(其实感觉和 sml 也差不多,只不过符号由 :: 变成了 |,然后外面带着列表的格式而已):

    double_all([]) -> [];
    double_all([First | Rest]) -> [First + First | double_all(Rest)].
  • 列表解析:这个的确感觉好强大。通常在 JS 里需要得到对数组中的每个对象添加/改变属性的结果时,我只能在 map 里改变每个对象再 return 回去,会改变原来的对象。而使用 Erlang 的模式匹配只需要这样:

    % 原列表
    Cart = [{pencil, 4, 0.25}, {pen, 1, 1.20}, {paper, 2, 0.20}].
    % 为原列表加上税款这一项
    WithTax = [{Procuct, Quantity, Price, Price * Quantity * 0.08} ||
        {Product, Quantity, Price} <- Cart].

    将列表构造放在左边并使用模式匹配。

    个人感觉这样的确避免了可变状态而且非常简洁,但是假如列表中元组的元素特别多的时候,模式匹配岂不是要写特别长?或者 Erlang 会提供一些其他的方式来解决这种问题吗,比如...剩余参数之类的概念。暂时保留这个疑问吧。

  • 第二天自习ArrayBuffer & Buffer #1:考虑包含键-值元组的列表,写一个函数接受列表和键为参数。
    思路:filter 一下给出的 list 并使用模式匹配选出第一个元组,然后返回对应的值

    get(List, Key) ->
        [{ MatchKey, MatchVal} | _ ] = 
            lists:filter(fun({K, V}) -> K == Key end, List), MatchVal.
  • 第二天自习使用 shdowsocks 和 profixer 实现全局代理 #2:返回有 total price 的商品列表。这个和前面例子讲的打折几乎一样:

    total_price(List) -> 
        [{Item, Quantity * Price} || {Item, Quantity, Price} <- List].

    第三道题看不懂啊QAQ

2018.09.02 红药丸

  • 红药丸:这里交代了一下当前整个编程行业的背景:避免使用并发;使用可变状态;函数和方法有副作用;使用共享状态的线程等等,带来了很多问题。我自己是对进程,线程没有太多的认识的,这里放一段维基百科的解释吧~

    In computing, a process is an instance of a computer program that is being executed. It contains the program code and its activity. Depending on the operating system (OS), a process may be made up of multiple threads of execution that execute instructions concurrently.[1]

    简单来说进程就是计算机正在执行的一段程序,可能会由几个并发的执行指令的线程组成。

  • 并发:并发是指能够使几段程序无序或按某种顺序执行而不影响最终的结果的能力。在我们平时写的程序中,并发能力一般是通过多线程或多进程来处理。 为了实现并发,通常是把接收到的多条消息放入共享的内存中,并通过锁或 CAS 来避免错误。而 Erlang 的并发模型是 Actor,采用多进程之间进行消息传递的方式。

  • 三种并发原语

    • spawn 用于产生进程,其参数为一个函数,产生进程后这个作为参数的函数会在一个新的轻量级进程中启动,并会返回一个进程 ID。
    • ! 发送消息。用法是 Pid ! message,含义是向进程 ID 为 Pid 的进程发送 message.
    • receive 接收消息,并使用模式匹配处理接收到的消息。
  • 异步消息:指消息的发送者只管发送消息而不必等待响应。书中以一种不带返回值的程序介绍了如何使用三种原语进行进程间的异步消息传递与接收:

    -module(translate).
    -export([loop/0]).
    
    loop() ->
        receive
            "casa" ->
                io:format("house~n"),
                loop();
            "blanca" ->
                io:format("white~n"),
                loop();
            _ ->
                io:format("I don't understand. ~n"),
                loop()
    end

    在 Erlang 的 REPL 环境中执行

    1> c(translate).

    进行编译和

    2> Pid = spawn(fun translate:loop/0)

    来产生进程,然后就可以发送消息:

    3> Pid ! "casa".
    "house"
    "case"
    4> Pid ! "blanca"
    "white"
    "blanca"
  • 同步消息:发送者发出消息之后,在接收到返回之前只能等待。书中的讲解是通过把上一步的 translate 改为同步的形式进行的。起初不明白为什么改造需要有 P160(中文版)的三部分策略,认为既然是需要返回值,那不是改成这样就好了(下面的代码是错误的):

    -module(trans2).
    -export([loop/0]).
    
    loop() ->
        receive
            "casa" ->
                "house",
                loop();
            "blanca" ->
               "white",
               loop();
            _ ->
                "I don't understand",
                loop()
    end.

    为什么呢. 编译一下就知道了:

    1> c(trans2).
    trans2.erl:7: Warning: a term is constructed, butnever used
    trans2.erl:10: Warning: a term is constructed, but never used
    trans2.erl:13: Warning: a term is constructed, but never used
    {ok,trans2}

    既然返回了字符串后面的 loop 不是就执行不了了吗。。笨死了。所以要怎么做呢?可以把匹配到的结果再发送回去啊!然后把发送者做一些封装,也能够接受消息并返回就好了。具体的话,先来看一下我们想把它做成什么样子(如何使用):

    1> c(translate_service).
    {ok,translate_service}
    2> Translator = spawn(fun translate_service:loop/0).
    {0.83.0}
    3> translate_service:translate(Translator, "blanca").
    "white"
    4> translate_service:translate(Translator, "casa").
    "house"

    第一行是代码的编译,没什么好说的。第二行是开启一个新的进程里执行我们写的模块的 loop 函数,并产生一个进程 id Translator, 第三第四行是使用模块中的 translate 函数,传入一个 pID,也就是之前生成的 Translator 和要发送的消息,然后程序直接返回了翻译的结果。所以要怎么重写之前的异步消息为这种同步消息也就比较明了了。

    • 首先看如何实现 translate 发送者,使用时我们是通过调用 translate(Pid, Word) 来同步获得返回结果的,在这里作为消息的发送者它需要做两件事情,第一是向自身进程(因为和 loop()处于一个进程)发送消息,然后等接受者(loop)接受到消息并又发送消息回来后,translate 还要负责接受消息并返回:
      translate(To, Word) ->
      To ! {self(), Word},
      receive
          Translation -> Translation
      end.
    • 然后看 loop 接收者:接收者在收到 translate 发送来的消息后,应该向发送者发送匹配到的值,并继续循环:
      loop() ->
          receive
              {From, "casa"} ->
                  From ! "house",
                  loop();
              {From, "blanca"} ->
                  From ! "White",
                  loop();
              {From, _} -> 
                  From ! "I don't understand",
                  loop
      end.
  • 链接进程:Erlang 中独特的错误处理哲学是“任其崩溃”,它在语言层面上支持把两个进程链接起来以获得更好的可靠性。只要其中一个进程终止,它就会将退出信号发送给与之链接的同伴。这样一来,在另一个进程会接收到这个信号,并相应作出反应。个人觉得这个设计的确非常轻便巧妙,进程的终止信号就像其他任何的消息一样,通过消息的发送与接收与其他进程通信。
    首先看如何链接两个进程。可以先创建一个易于终止的进程:

    -module(roulette).
    -export([loop/0]).
    
    loop() -> 
        receive 
            3 -> io:format("bang.~n"), exit({roulette, die, at, erlang:time()});
            _ -> io:format("click~n"), loop()
    end.

    在客户端(命令行)中为 roulette 中的 loop 函数生成一个进程之后,向进程发送消息 3,进程就会终止。接下来看如何使其他进程和这个易于崩溃的进程链接起来:

    -module(coroner).
    -export([loop/0]).
    
    loop() ->
        process_flag(trap_exit, true), % 注册进程使之可以捕获到退出
        receive
            {monitor, Process} ->  % 接收 monitor 和一个 PID
                link(Process), % 链接到该进程
                io:format("Montoring process. ~n"),
                loop();
            {'EXIT', From, Reason} -> % 捕获到进程的退出消息
                io:format("The shooter ~p died with reason ~p.", [From, Reason]),
                io:format("Start another one. ~n"),
                loop()
            end.

    运行过程如下:

    1> c(roulette).
    {ok,roulette}
    2> c(coroner).
    {ok,coroner}
    3> Revolver=spawn(fun roulette:loop/0). % 为 roulette 生成进程
    <0.87.0>
    4> Coroner=spawn(fun coroner:loop/0). % 为 coroner 生成进程
    <0.89.0>
    5> Coroner ! {monitor, Revolver}. % 向 Coroner 发送消息使其链接到 Revolver
    Montoring process.
    {monitor,<0.87.0>}
    6> Revolver ! 1. % 向 Revolver 发送消息 1,安全
    click
    1
    7> Revolver ! 3. % 向 Revolver 发送消息 3,进程会退出
    bang.
    3
    The shooter <0.87.0> died with reason {roulette,die,at,{18,35,12}}.Start another one. % Coroner 会捕获到 Revolver 的退出并提醒客户端

    当然,也可以在 Coroner 中对链接到的进程的退出做进一步的处理,比如,为死掉的 Revolver 重新建立一个进程:

    -module(doctor).
    -export([loop/0]).
    
    loop() ->
        process_flag(trap_exit, true),
        receive
            new ->
                io:format("Creating and monitoring process.~n"),
                register(revolver, spawn_link(fun roulette:loop/0)), % `swawn_link` 可以产生进程并把进程链接起来,`register` 使我们可以用 `revolver` 原子向新产生的进程发送消息
                loop();
    
            {'EXIT', From, Reason} ->
                io:format("The shooter ~p died with reason ~p.", [From, Reason]),
                    io:format("Restarting. ~n"),
                self() ! new,
                loop()
            end.

    现在的 doctor 接收两种消息,一种是 new,为 roulette 新建一个进程并与 当前进程链接起来,另一种是与之关联的程序退出的消息,当 doctor 捕获到退出时,会为 roulette 创建一个新的进程并产生新的链接。注意到,因为我们是在 doctor 收到 new 消息时会为 roulette 新建进程,所以也要从创建并注册进程开始运行它:

    > c(doctor).
    {ok,doctor}
    2> Doc = spawn(fun doctor:loop/0).
    <0.82.0>
    3> Doc ! new.
    Creating and monitoring process.
    new
    4> revolver ! 1.
    click
    1
    5> revolver ! 3.
    bang.
    3
    The shooter <0.84.0> died with reason {roulette,die,at,{19,39,43}}.Restarting.
    Creating and monitoring process.
    6> revolver ! 4.
    click
    4

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions