现在的位置: 首页 > 综合 > 正文

Erlang程序设计 笔记

2016年07月28日 ⁄ 综合 ⁄ 共 7236字 ⁄ 字号 评论关闭

Erlang程序设计

跳转至:
导航

搜索

目录

顺序编程

  1. COP:面向并发的编程(消息传递,无共享变量)
  2. atom:小写字母开头
  3. -module(mymodule). -export([func/2]). -import(lists,[map/2,sum/1]). 模块、导入导出
  4. 模式匹配:(注意这里total/1对于参数的模式匹配)
    1. total( [{What,N}|Tail] ) -> cost(What)*N + total(Tail); 但是这里似乎有一个问题:对数据的模式匹配似乎限制了函数的处理能力范围?
    2. total( [] ) -> 0.
  5. 元组:Person = {person, {name, joe},...}.
  6. 列表:[1,2,3,4]
  7. ->的作用:左边是待匹配的模式,右边是表达式(动作)——可理解为一个规则系统?
    1. 字符串仅仅是整数的列表
  8. 匿名函数(fun):注意这里以end.结束,如果需要多个参数匹配,继续增加; (args) -> expr 即可
    1. Z = fun (X) -> 2*X end.
    2. 以fun为参数:高阶函数
      1. Even = fun (X) -> (X rem 2) =:= 0 end. 在其他一些语言里返回true/false的lambda称为‘谓词’
      2. lists:filter(Even, [1,2,3,4,5]).
    3. 返回fun的函数(闭包?)
      1. MakeInListTest = fun(L) -> ( fun(X)->lists:member(X,L) end) end.
  9. 模式匹配+高阶函数 => 控制流程(for,if,switch,while等)
    1. for(这个例子跟Common LISP很相似)

      1. for(Max,Max,F) -> [F(Max)];
      2. for(I,Max,F) -> [F(I)|for(I+1,Max,F)].
  10. map/2:
    1. map(_, []) -> [];
    2. map(F, [H|T]) -> [F(H)|map(F,T).
  11. Erlang里的列表解析(注意与Python语法的区别!)
    1. [2*X || X <- L ]
    2. 更简洁的map定义:map(F,L) -> [F(X) || X<-L].
    3. ||右边是生成器(过滤器),多维情况时用,分隔即可
  12. 利用列表解析实现qsort(这里使用了++操作,这个符号来自于Standard ML?)——注意,这里为了展现代码的优雅性而不是效率
    1. qsort([]) -> [];
    2. qsort([Pivot|T]) -> qsort([X || X<-T, X<Pivot]) ++ [Pivot] ++ qsort([X || X<-T, X>=Pivot])
  13. 生成所有可能的排列(列表解析+递归调用):
    1. perms([]) -> [[]];
    2. perms(L) -> [[H|T] || H<-L, T<-perms(L--[H])].
  14. 断言(guard)
    1. max(X,Y) when X>Y -> X; max(X,Y) -> Y.
    2. when序列:;代表‘或’ ,代表‘与’
    3. 断言表达式(不允许副作用):
      1. 断言谓词:is_number(X) is_pid(X) is_port(X) is_function(X,N) ...
      2. BIF:abs(X) element(N,X) hd(X) tl(X) length(X) node() self() trunc(X) ... 这里hd,tl的缩略写法很明显的来自于Standard ML
      3. 短路布尔表达式:andalso orelse
  15. 记录(带命名属性的对象?)
    1. -record(Name, {key1, key2=Default2, key3, ....}).
    2. 每次修改都会创建记录的新副本!(这里的写法有明显的FP语言特征)
      1. X1 = #todo{status=urgent, text="fix it"}.
      2. X2 = X1#todo{status=done}.
      3. 访问属性:X2#todo.text
      4. 记录只是元组的伪装(命名属性)
  16. case-if:为每件事情都使用单独的函数子句不方便,so ...
  17. if(通常最后一个会是原子true)
  18. *odds_and_evens_acc的例子:通过外部传入参数避免2次遍历列表(?Erlang里list参数是引用传递的?似乎不能这么理解)
    1. 实际上参数作为名字,每次递归调用时都重新绑定了?但是最外层递归处的名字引用递归调用完成后的新数据?
  19. 异常:try te of
    case表达式
    + catch (ExceptionTypeLabel:case表达式)+ after ae end

    1. 为简化描述,把“Pattern [when Guard] -> Expr”部分称为case表达式
    2. 异常类型标签:a throw(a) exit(a) {'EXIT',a} erlang:error(a)
    3. {ok,Value}、{error,Reason}返回惯例
    4. 捕获所有错误:... catch _:_ -> ... ,如果只使用_相当于错误标签(类型)是throw
  20. 二进制数据:<<5,10,20>>(Pdf文件里的字典类型数据也使用了<<>>符号)
    1. @spec list_to_binary(IoList) -> binary()
    2. 比特语法:M = <<X:3, Y:7, Z:6>> (注意:数据怎么封包就怎么解包,反过来模式匹配就可以了)
    3. <<16#12345678:32/big>> 大端顺序(只有当:后的长度大于8才有意义!),/后的类型规格:End-Sign-Type-Unit
    4. 真实世界的例子:
      1. 在MPEG中查找同步帧

        1. decode_header(<<2#11111111111:11, B:2, C:2, _D:1, E:4, F:2, G:1, Bits:9>>) -> ...
      2. 解析COFF数据
        1. 定义宏?-define(DWORD, 32/unsigned-little-integer).
        2. ... -> <<Characteristics:?DWORD, ..., _/binary>> = Dir, ...
      3. 解析IPv4数据包头部(也就是说,用Erlang编写底层的网络协议是完全可能的,当然,LISP也可以)
        1. case Datagram of <<?IP_VERSION:4, HLen:4, SrvcType:8, TotLen:16, ID:16, Flgs:3, FragOff:13, TTL:8, Proto:8, HdrChkSum:16, SrcIP:32, DestIP:32, RestData/binary>> when HLen>=5, .... -> ...
  21. 小问题集锦
    1. apply(Modu, Func,[Arg1,Arg2,...,ArgN])
    2. 属性 -AtomTag(...).
    3. 块表达式(begin ... end)
    4. 字符集 Latin-1(从整数列表生成Unicode仍然有一定的限制?)
    5. 注释 %comment
    6. epp预处理
    7. 转义字符 \b \d \e \NNN \^A(Ctrl+A)
    8. 函数引用
      1. fun LocalFunc/Arity
      2. fun Mod:RemoteFunc/Arity
    9. 包含文件 -include -include_lib
    10. ++ --列表操作
      1. -ifdef(debug). -define(TRACE(X), io:format("~p:~p ~p~n", [?MODULE,?LINE,X])). -else. -define(TRACE(X),void). -endif.

        1. 格式化输出与Python3一样,比较难于记忆理解!
      2. c(mymod, {d, debug}).
    11. 模式中使用=
      1. func1([{tag, {one,A}=Z1, B}=Z2|T) -> ... 这里引入的临时名字写在=的右边?
    12. 进程字典
      1. @spec put(Key, Value) -> OldValue. 下略
    13. 引用 erlang:make_ref()
    14. X /= Y(不等于),X =:= Y(全等于),X =/= Y(不全等于)
    15. 大小比较的顺序:number < atom < reference < fun < port < pid < tuple < list < binary
    16. 下划线变量
      1. 当此变量没有被使用时,编译器不会报错(相对于_来说,至少用名字说明了其意义,虽然并不用它?)

并发编程

  1. 3个原语:spawn send receive

    1. Pid = spawn(Func)
    2. Pid ! Message(消息发送是异步的)
    3. receive ...(case表达式)... end
  2. loop() -> receive ... loop(); Other -> ....loop() end. (尾递归优化?TCO)
    1. Pid = spawn( fun area_server0:loop/0).
    2. Pid ! {rectangle, 6, 10}.
  3. 双向通信:Pid ! {self(), rectangle, 6, 10}.
    1. 服务器接受端:receive {From,...} -> From ! Width*Height, loop(); ...
    2. 发送方立即调用 receive 以阻塞接受刚才的异步请求的响应结果(似乎这里客户端可以让它阻塞?不太符合P2P的思想。。。)
      1. 修正:绑定的Pid

        1. Pid ! {self(), Request),
        2. receive {Pid, Response} -> ... end. 不匹配Pid的消息放入队列等待处理?(哦)
    3. 带超时的receive
      1. receive ... after Time(毫秒数) -> Exprs end
      2. 超时为0的receive:flush_recv_buffer()的定义
      3. *实现一个timer
      4. 选择性接受:调用receive其实就是对进程mailbox检查的唯一机会
        1. ?顺序receive模式匹配时,如果消息不匹配receive的任何一个模式,就将消息从mailbox删除并送入一个“保存队列”
        2. 一个消息如果匹配,那么存入“保存队列”的所有未匹配消息就会按照达到进程的顺序重新放回mailbox(!!!)——“保存队列”的作用?可以防止另一个receive同时访问mailbox造成消息变成共享变量?
    4. 注册进程:一旦register(AnAtomName, Pid),就可以通过注册名字发送消息
    5. spawn(Mod, Func, Args):显式指明模块,可确保代码运行时仍然可用新版本升级
  4. COP中的异常/错误处理:link、exit信号、系统进程
    1. 3种简单模式:

      1. 我不在乎创建的进程是否崩溃:Pid = spawn( fun ... )
      2. 如果child崩溃则我也消亡:Pid = spawn_link( fun ... )
      3. 处理child的崩溃错误:... loop(State) -> receive {'EXIT', LinkedPid, Reason} -> ... loop(StateNew); end
    2. 普通进程:没有运行过process_flag(trap_exit, true)转换为系统进程
    3. @spec link(Pid) -> true 链接是双向的
    4. *监视器(非对称link):如果A监视B,则如果A死亡,B不会收到EXIT信号
    5. 竞争条件:进程有可能在2个语句之间死亡。。。=> 使用OTP库!

分布式编程

  1. rpc:call
  2. 同一机器不同端口:$erl -sname gandalf
  3. 局域网内不同机器:$erl -name gandalf -setcookie abc
  4. 因特网上不同主机:需要开放4396端口,并指定额外的min max port范围
  5. lib_chan
  6. IRC lite
    1. spawn_link( fun()->try_to_connect(self(),...) end) 这里self()在子进程内执行,返回的是子进程的Pid!(FP里的“动态绑定”?)
    2. group_controller(L):状态L是一个列表,维护所有用户/中间人的Pid(这里全局状态直接就在顶层loop里作为参数传递了)
  7. 与外部程序的接口:通过端口通信
    1. Port = open_port(PortName, PortSettings)
    2. Port ! {PidC, {command, Data}}

文件编程

  1. 操作文件的4个模块:file filename filelib io

    1. file:consult("1.dat"). 假定文件中包含Erlang格式的数据项,返回{ok,[Term]}
    2. 一次读取一项:
      1. @spec file:open(File, read) => {ok,IoDevice} | {error, Why}
      2. @spec io:read(IoDevice,Prompt) => {ok, Term} | {error, Why} | eof
    3. 一次读取一行:io:get_line(IoDevice, )
    4. 读取整个文件内容:file:read_file("1.dat") 返回{ok, Binary}
    5. 随机访问:file:pread(IoDevice, Start, Len) file:pwrite(IoDevice, Start, Binary)

socket编程

  1. {ok, Socket} = gen_tcp:connect(Host, 80, [binary, {packet,0}]),

    1. ok = gen_tcp:send(Socket, "GET / HTTP/1.0\r\n\r\n"),
    2. receive_data(Socket, []).
  2. receive_data(Socket, SoFarGot) ->
    1. receive

      {tcp, Socket, Bin} -> receive_data(Socket, [Bin|SoFarGot]);
      {tcp_closed, Socket} -> list_to_binary( reverse(SoFarGot) ) end.
  3. R11B3+,允许多个进程对同一个监听socket调用accept/1
  4. 主动socket:数据到达时,系统向控制进程发送{tcp, Socket, Data}消息,控制进程无法控制停掉(这相当于底层socket IO在独立的内核线程?)
  5. 如果socket被动打开,则控制进程需要主动调用gen_tcp:recv(Socket,N)来接受数据(阻塞方式)
  6. 混合型:{active, once}
    1. loop(Socket) ->

      receive

      {tcp, Socket, BinData} ->

       %处理接受到的数据(此时socket处于阻塞状态!)
      inet:setopts(Socket, [{active, once}]), %重新enable接受下一条消息(这对socket的另一端来说是不是作弊了?)
      loop(Socket);
    2. {active, once}实现了流量控制(根据服务器自身的处理能力)
  7. UDP
    1. loop(Socket) ->

      receive

      {udp, Socket, Host, Port, BinData} ->

      Reply = ...
      gen_udp:send(Socket, Host, Port, Reply);
      loop(Socket)
  8. 例子:SHOUTcast

ETS和DETS:大量数据的存储

  1. ets:new或dets:open_file
  2. insert(TableId, X) X is a tuple or tuple list
  3. lookup(TableId, Key)
  4. ets:delete(TableId)或dets:close(TableId)关闭
  5. ets不是Erlang实现的,调用的是底层运行时系统,没有垃圾回收
  6. 。。。在我的机器上,set类型的ETS表每次查找只要0.5us,性能很出色(这相当于一次内存访问开销*hash操作的常量系数?)

OTP

  1. init() + handle(Request,State):在server1模块中,无须考虑并发模型
  2. “事务机制”
    1. 如果handle失败,用之前的State进行loop
  3. 支持热代码替换
    1. receive {From, {swap_code, NewCallbackMod}} -> From ! {Name, ack}, loop(Name, NewCallbackMod, OldState); ...
  4. 开始什么都不做,直到下达指令,变成某种服务器程序
    1. receive {become, F} -> F() end.
    2. => Pid ! {become, fun my_fib_server:loop/0}. 程序代码需要先上传吧?(如何编写一个可升级的木马程序。。。)
  5. gen_server 3要点:
    1. 模块名称
    2. 接口函数
    3. 6个回调函数:init/1 handle_call/3 handle_cast/2 handle_info/2 terminate/2 code_change/3
    4. 模板?:-behaviour(gen_server).
    5. handle_call({new, Who}, From, State) ->
      Reply = ...
      NextState = ...
      {reply, Reply, NextState};
    6. handle_call(stop, _From, Tab) -> {stop, normal, stopped, Tab}.
    7. handle_info用于处理原生消息(?)
  6. 错误日志
    1. @spec error_logger:error_msg(String) -> ok
  7. SASL:过载保护?
  8. 应用程序监视器:appmon:start()

Mnesia:Erlang数据库

  1. ?Mnesia是一个Erlang实现的实时数据库,内部使用了ETS和DETS。
  2. qlc:q(列表解析做join操作?)/e(Q)
  3. F = fun() -> foreach( fun mnesia:write/1, data_tuples()) end, mnesia:transaction(F).\
  4. 设计(plan)?
  5. @spec mnesia:create_table(Name, Args) -> {atomic, ok} | {aborted, Reason}
  6. 进一步深入?
    1. 备份和恢复
    2. 脏操作
    3. SNMP表

多核编程

  1. 顺序瓶颈:。。。正如我们的生老病死,这是一个无法改变的顺序。
  2. pmap
  3. SMP erlang
  4. map-reduce
    1. @spec mapreduce(F1, F2, Acc0, L) -> Acc

      1. F1 = fun(Pid, X) -> void, %发送一组{Key,Value}给Pid然后退出
      2. F2 = fun(Key, [Value], Acc0) -> Acc %归并
      3. L = [X],
      4. Acc = X = term()

附录 

抱歉!评论已关闭.