Erlang程序设计
目录 |
顺序编程
- COP:面向并发的编程(消息传递,无共享变量)
- atom:小写字母开头
- -module(mymodule). -export([func/2]). -import(lists,[map/2,sum/1]). 模块、导入导出
- 模式匹配:(注意这里total/1对于参数的模式匹配)
- total( [{What,N}|Tail] ) -> cost(What)*N + total(Tail); 但是这里似乎有一个问题:对数据的模式匹配似乎限制了函数的处理能力范围?
- total( [] ) -> 0.
- 元组:Person = {person, {name, joe},...}.
- 列表:[1,2,3,4]
- ->的作用:左边是待匹配的模式,右边是表达式(动作)——可理解为一个规则系统?
- 字符串仅仅是整数的列表
- 匿名函数(fun):注意这里以end.结束,如果需要多个参数匹配,继续增加; (args) -> expr 即可
- Z = fun (X) -> 2*X end.
- 以fun为参数:高阶函数
- Even = fun (X) -> (X rem 2) =:= 0 end. 在其他一些语言里返回true/false的lambda称为‘谓词’
- lists:filter(Even, [1,2,3,4,5]).
- 返回fun的函数(闭包?)
- MakeInListTest = fun(L) -> ( fun(X)->lists:member(X,L) end) end.
- 模式匹配+高阶函数 => 控制流程(for,if,switch,while等)
- for(这个例子跟Common LISP很相似)
- for(Max,Max,F) -> [F(Max)];
- for(I,Max,F) -> [F(I)|for(I+1,Max,F)].
- for(这个例子跟Common LISP很相似)
- map/2:
- map(_, []) -> [];
- map(F, [H|T]) -> [F(H)|map(F,T).
- Erlang里的列表解析(注意与Python语法的区别!)
- [2*X || X <- L ]
- 更简洁的map定义:map(F,L) -> [F(X) || X<-L].
- ||右边是生成器(过滤器),多维情况时用,分隔即可
- 利用列表解析实现qsort(这里使用了++操作,这个符号来自于Standard ML?)——注意,这里为了展现代码的优雅性而不是效率
- qsort([]) -> [];
- qsort([Pivot|T]) -> qsort([X || X<-T, X<Pivot]) ++ [Pivot] ++ qsort([X || X<-T, X>=Pivot])
- 生成所有可能的排列(列表解析+递归调用):
- perms([]) -> [[]];
- perms(L) -> [[H|T] || H<-L, T<-perms(L--[H])].
- 断言(guard)
- max(X,Y) when X>Y -> X; max(X,Y) -> Y.
- when序列:;代表‘或’ ,代表‘与’
- 断言表达式(不允许副作用):
- 断言谓词:is_number(X) is_pid(X) is_port(X) is_function(X,N) ...
- BIF:abs(X) element(N,X) hd(X) tl(X) length(X) node() self() trunc(X) ... 这里hd,tl的缩略写法很明显的来自于Standard ML
- 短路布尔表达式:andalso orelse
- 记录(带命名属性的对象?)
- -record(Name, {key1, key2=Default2, key3, ....}).
- 每次修改都会创建记录的新副本!(这里的写法有明显的FP语言特征)
- X1 = #todo{status=urgent, text="fix it"}.
- X2 = X1#todo{status=done}.
- 访问属性:X2#todo.text
- 记录只是元组的伪装(命名属性)
- case-if:为每件事情都使用单独的函数子句不方便,so ...
- if(通常最后一个会是原子true)
- *odds_and_evens_acc的例子:通过外部传入参数避免2次遍历列表(?Erlang里list参数是引用传递的?似乎不能这么理解)
- 实际上参数作为名字,每次递归调用时都重新绑定了?但是最外层递归处的名字引用递归调用完成后的新数据?
- 异常:try te of
case表达式+ catch (ExceptionTypeLabel:case表达式)+ after ae end- 为简化描述,把“Pattern [when Guard] -> Expr”部分称为case表达式
- 异常类型标签:a throw(a) exit(a) {'EXIT',a} erlang:error(a)
- {ok,Value}、{error,Reason}返回惯例
- 捕获所有错误:... catch _:_ -> ... ,如果只使用_相当于错误标签(类型)是throw
- 二进制数据:<<5,10,20>>(Pdf文件里的字典类型数据也使用了<<>>符号)
- @spec list_to_binary(IoList) -> binary()
- 比特语法:M = <<X:3, Y:7, Z:6>> (注意:数据怎么封包就怎么解包,反过来模式匹配就可以了)
- <<16#12345678:32/big>> 大端顺序(只有当:后的长度大于8才有意义!),/后的类型规格:End-Sign-Type-Unit
- 真实世界的例子:
- 在MPEG中查找同步帧
- decode_header(<<2#11111111111:11, B:2, C:2, _D:1, E:4, F:2, G:1, Bits:9>>) -> ...
- 解析COFF数据
- 定义宏?-define(DWORD, 32/unsigned-little-integer).
- ... -> <<Characteristics:?DWORD, ..., _/binary>> = Dir, ...
- 解析IPv4数据包头部(也就是说,用Erlang编写底层的网络协议是完全可能的,当然,LISP也可以)
- 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, .... -> ...
- 在MPEG中查找同步帧
- 小问题集锦
- apply(Modu, Func,[Arg1,Arg2,...,ArgN])
- 属性 -AtomTag(...).
- 块表达式(begin ... end)
- 字符集 Latin-1(从整数列表生成Unicode仍然有一定的限制?)
- 注释 %comment
- epp预处理
- 转义字符 \b \d \e \NNN \^A(Ctrl+A)
- 函数引用
- fun LocalFunc/Arity
- fun Mod:RemoteFunc/Arity
- 包含文件 -include -include_lib
- ++ --列表操作
- 宏
- -ifdef(debug). -define(TRACE(X), io:format("~p:~p ~p~n", [?MODULE,?LINE,X])). -else. -define(TRACE(X),void). -endif.
- 格式化输出与Python3一样,比较难于记忆理解!
- c(mymod, {d, debug}).
- -ifdef(debug). -define(TRACE(X), io:format("~p:~p ~p~n", [?MODULE,?LINE,X])). -else. -define(TRACE(X),void). -endif.
- 模式中使用=
- func1([{tag, {one,A}=Z1, B}=Z2|T) -> ... 这里引入的临时名字写在=的右边?
- 进程字典
- @spec put(Key, Value) -> OldValue. 下略
- 引用 erlang:make_ref()
- X /= Y(不等于),X =:= Y(全等于),X =/= Y(不全等于)
- 大小比较的顺序:number < atom < reference < fun < port < pid < tuple < list < binary
- 下划线变量
- 当此变量没有被使用时,编译器不会报错(相对于_来说,至少用名字说明了其意义,虽然并不用它?)
并发编程
- 3个原语:spawn send receive
- Pid = spawn(Func)
- Pid ! Message(消息发送是异步的)
- receive ...(case表达式)... end
- loop() -> receive ... loop(); Other -> ....loop() end. (尾递归优化?TCO)
- Pid = spawn( fun area_server0:loop/0).
- Pid ! {rectangle, 6, 10}.
- 双向通信:Pid ! {self(), rectangle, 6, 10}.
- 服务器接受端:receive {From,...} -> From ! Width*Height, loop(); ...
- 发送方立即调用 receive 以阻塞接受刚才的异步请求的响应结果(似乎这里客户端可以让它阻塞?不太符合P2P的思想。。。)
- 修正:绑定的Pid
- Pid ! {self(), Request),
- receive {Pid, Response} -> ... end. 不匹配Pid的消息放入队列等待处理?(哦)
- 修正:绑定的Pid
- 带超时的receive
- receive ... after Time(毫秒数) -> Exprs end
- 超时为0的receive:flush_recv_buffer()的定义
- *实现一个timer
- 选择性接受:调用receive其实就是对进程mailbox检查的唯一机会
- ?顺序receive模式匹配时,如果消息不匹配receive的任何一个模式,就将消息从mailbox删除并送入一个“保存队列”
- 一个消息如果匹配,那么存入“保存队列”的所有未匹配消息就会按照达到进程的顺序重新放回mailbox(!!!)——“保存队列”的作用?可以防止另一个receive同时访问mailbox造成消息变成共享变量?
- 注册进程:一旦register(AnAtomName, Pid),就可以通过注册名字发送消息
- spawn(Mod, Func, Args):显式指明模块,可确保代码运行时仍然可用新版本升级
- COP中的异常/错误处理:link、exit信号、系统进程
- 3种简单模式:
- 我不在乎创建的进程是否崩溃:Pid = spawn( fun ... )
- 如果child崩溃则我也消亡:Pid = spawn_link( fun ... )
- 处理child的崩溃错误:... loop(State) -> receive {'EXIT', LinkedPid, Reason} -> ... loop(StateNew); end
- 普通进程:没有运行过process_flag(trap_exit, true)转换为系统进程
- @spec link(Pid) -> true 链接是双向的
- *监视器(非对称link):如果A监视B,则如果A死亡,B不会收到EXIT信号
- 竞争条件:进程有可能在2个语句之间死亡。。。=> 使用OTP库!
- 3种简单模式:
分布式编程
- rpc:call
- 同一机器不同端口:$erl -sname gandalf
- 局域网内不同机器:$erl -name gandalf -setcookie abc
- 因特网上不同主机:需要开放4396端口,并指定额外的min max port范围
- lib_chan
- IRC lite
- spawn_link( fun()->try_to_connect(self(),...) end) 这里self()在子进程内执行,返回的是子进程的Pid!(FP里的“动态绑定”?)
- group_controller(L):状态L是一个列表,维护所有用户/中间人的Pid(这里全局状态直接就在顶层loop里作为参数传递了)
- 与外部程序的接口:通过端口通信
- Port = open_port(PortName, PortSettings)
- Port ! {PidC, {command, Data}}
文件编程
- 操作文件的4个模块:file filename filelib io
- file:consult("1.dat"). 假定文件中包含Erlang格式的数据项,返回{ok,[Term]}
- 一次读取一项:
- @spec file:open(File, read) => {ok,IoDevice} | {error, Why}
- @spec io:read(IoDevice,Prompt) => {ok, Term} | {error, Why} | eof
- 一次读取一行:io:get_line(IoDevice, )
- 读取整个文件内容:file:read_file("1.dat") 返回{ok, Binary}
- 随机访问:file:pread(IoDevice, Start, Len) file:pwrite(IoDevice, Start, Binary)
socket编程
- {ok, Socket} = gen_tcp:connect(Host, 80, [binary, {packet,0}]),
- ok = gen_tcp:send(Socket, "GET / HTTP/1.0\r\n\r\n"),
- receive_data(Socket, []).
- receive_data(Socket, SoFarGot) ->
- receive
- {tcp, Socket, Bin} -> receive_data(Socket, [Bin|SoFarGot]);
- {tcp_closed, Socket} -> list_to_binary( reverse(SoFarGot) ) end.
- receive
- R11B3+,允许多个进程对同一个监听socket调用accept/1
- 主动socket:数据到达时,系统向控制进程发送{tcp, Socket, Data}消息,控制进程无法控制停掉(这相当于底层socket IO在独立的内核线程?)
- 如果socket被动打开,则控制进程需要主动调用gen_tcp:recv(Socket,N)来接受数据(阻塞方式)
- 混合型:{active, once}
- loop(Socket) ->
- receive
- {tcp, Socket, BinData} ->
- %处理接受到的数据(此时socket处于阻塞状态!)
- inet:setopts(Socket, [{active, once}]), %重新enable接受下一条消息(这对socket的另一端来说是不是作弊了?)
- loop(Socket);
- {tcp, Socket, BinData} ->
- receive
- {active, once}实现了流量控制(根据服务器自身的处理能力)
- loop(Socket) ->
- UDP
- loop(Socket) ->
- receive
- {udp, Socket, Host, Port, BinData} ->
- Reply = ...
- gen_udp:send(Socket, Host, Port, Reply);
- loop(Socket)
- {udp, Socket, Host, Port, BinData} ->
- receive
- loop(Socket) ->
- 例子:SHOUTcast
ETS和DETS:大量数据的存储
- ets:new或dets:open_file
- insert(TableId, X) X is a tuple or tuple list
- lookup(TableId, Key)
- ets:delete(TableId)或dets:close(TableId)关闭
- ets不是Erlang实现的,调用的是底层运行时系统,没有垃圾回收
- 。。。在我的机器上,set类型的ETS表每次查找只要0.5us,性能很出色(这相当于一次内存访问开销*hash操作的常量系数?)
OTP
- init() + handle(Request,State):在server1模块中,无须考虑并发模型
- “事务机制”
- 如果handle失败,用之前的State进行loop
- 支持热代码替换
- receive {From, {swap_code, NewCallbackMod}} -> From ! {Name, ack}, loop(Name, NewCallbackMod, OldState); ...
- 开始什么都不做,直到下达指令,变成某种服务器程序
- receive {become, F} -> F() end.
- => Pid ! {become, fun my_fib_server:loop/0}. 程序代码需要先上传吧?(如何编写一个可升级的木马程序。。。)
- gen_server 3要点:
- 模块名称
- 接口函数
- 6个回调函数:init/1 handle_call/3 handle_cast/2 handle_info/2 terminate/2 code_change/3
- 模板?:-behaviour(gen_server).
- handle_call({new, Who}, From, State) ->
- Reply = ...
- NextState = ...
- {reply, Reply, NextState};
- handle_call(stop, _From, Tab) -> {stop, normal, stopped, Tab}.
- handle_info用于处理原生消息(?)
- 错误日志
- @spec error_logger:error_msg(String) -> ok
- SASL:过载保护?
- 应用程序监视器:appmon:start()
Mnesia:Erlang数据库
- ?Mnesia是一个Erlang实现的实时数据库,内部使用了ETS和DETS。
- qlc:q(列表解析做join操作?)/e(Q)
- F = fun() -> foreach( fun mnesia:write/1, data_tuples()) end, mnesia:transaction(F).\
- 设计(plan)?
- @spec mnesia:create_table(Name, Args) -> {atomic, ok} | {aborted, Reason}
- 进一步深入?
- 备份和恢复
- 脏操作
- SNMP表
多核编程
- 顺序瓶颈:。。。正如我们的生老病死,这是一个无法改变的顺序。
- pmap
- SMP erlang
- map-reduce
- @spec mapreduce(F1, F2, Acc0, L) -> Acc
- F1 = fun(Pid, X) -> void, %发送一组{Key,Value}给Pid然后退出
- F2 = fun(Key, [Value], Acc0) -> Acc %归并
- L = [X],
- Acc = X = term()
- @spec mapreduce(F1, F2, Acc0, L) -> Acc