初学《Erlang程序设计》两周感想

时间:2022-06-01 19:09:28
初步接触
刚开始是属于C/C++这一块的程序工作,接触过的语言大多也是偏与面向对象,初步接触Erlang这样的函数式编程语言确实有种难以适从的感觉,就比如C++中常见的for循环,在Erlang中却只能进行尾递归或者列表推导才可以实现,明明眼前有座桥,我却只能从河里游过去,跟这门语言的第一次接触并不友好。
通过学习两周多Joe老爷子写的《Erlang程序设计(第二版)》,终于对Erlang有了个初步的认识,慢慢将初始接触Erlang的感官转变过来。
优势:
1 简介的语法特性
语法规则简单,关键字少,模块清晰
2 强大的容错功能
包含进程的节点监控与连接,OTP的Supervisor模式,进程隔离性好,支持异常捕捉机制(try...catch)
3 强大的OTP框架
gen_server|gen_fsm|gen_event|supervisor这些模式组成OTP的分层架构
4 轻量级的进程(process)
进程是最小的执行单元,隔离性好,每个进程都拥有独立的内存空间,进程开辟花销小
5 精炼的分布式模型
节点位置透明(通过节点名称即可连接)|支持自定义通讯协议(TCP,SSL)|互通连接(AlinkB,BlinkC,产生AlinkC)|同节点,跨节点,跨网络同意消息传递(Pid ! Msg)
6 代码热更替
函数式编程,所有功能都是基于函数式的实现,无关上下文,热更新只需要调整函数执行入口
7 全世界都是常量
变量(名不副实)单次赋值,没有全局变量也没有共享内存,突然感觉全世界都安静了
总结: 根据Erlang自身特性,多用于高并发,高可靠,高容错的系统。

劣势:

1 一般程序员都会选择以面向过程或者面向对象的语言起步,再接触函数式编程需要一定时间的观念转变
2 程序不易调试,虽然使用C(Mod,debug_info),debugger:start()命令可以打开调试窗口,打断点查看局部变量,但由于Erlang实际应用时会存在高并发的情况,这样针对每个进程会产生一个调试窗口,容易感到困惑(当然后期熟悉后,可操作日志文件查看)
3 针对复杂的程序逻辑,计算量较大的程序设计,处理能力较弱
Erlang格言:
万物皆进程,进程要么死,要么活,没有中间状态,没有半死不活。

Hello World !
源文件
-module(hello).
-compile([export_all]).

main() ->
io:format("hello world~n").
输出
hello world
ok
学习一门新的语言我喜欢的方式是从最简单的程序入口,提前能对该们语言的编程风格,有一个整体基本的感官,一般都喜欢从Hello World !开始,也许多年程序工作退休后,闲来想练字陶冶情操,第一个写下的毛笔字会自然而然的写下Hello World,然后哑然失笑。

Erlang语法
变量

这里顺带一提Joe老爷子书上写“所有变量名都必须以大写字母开头”,在当前版本是不准确的,变量名以大写字母或者"_"开头,只包含数字,字母,"_","@"更为准确
例如:>_A2@4=[1,2,3].
           [1,2,3]
8种原始的数据类型 
integer(整型) -4,2#111,16#3,9201928440
float(浮点数) -4.2,6.8e4(erlang没有单精度浮点数,is_float(3/2)与is_float(2/3)返回值都为true,区分"/"和"div")
atom(原子) -stone,my_love,root@host,'I am_Atom'(每个原子仅占一个字长,系统内部使用是个索引值)
binary(二进制) -<<"your name">>
reference(引用) -全局唯一标识符,只用来比较两个引用是否相等
fun(匿名函数) -"fun() ->some_expr end"
port(端口) -与外界进行交互的接口
Pid(进程表示符号) -process identifier(属于Erlang进程的引用)
关于atom的思考
atom在系统内部使用是个索引值,每个原子的索引值都是唯一的
1> erlang:float(55).
55.0
2> M = list_to_existing_atom("erlang").
erlang
3> F = list_to_existing_atom("float").
float
4> M:F(55).
55.0
5>N='erlang:float'.
'erlang:float'
6>N(55).
** exception error: bad function 'erlang:float'

2种复合类型

tuple(元组) -包含固定个数数据的容器(有点类似C中的strcut结构)
list(列表) -包含可变个数数据的容器(与C中的链表有共同之处)
关于tuple和list的思考
tuple和list是erlang程序中必不可少的组成成分,比如erlang中记录类型(record)实际是属于一种特殊的元组,字符串类型(string)属于列表,下面跟大家一起探索tuple和list的实际构成
在OTP包下XXXOPT/erts/emulator/beam/bif.c文件中可以看到所有内置函数(BIF)的C实现,从tuple_to_list()和list_to_tuple入手
list_to_tuple:
/* convert a list to a tuple */
BIF_RETTYPE list_to_tuple_1(BIF_ALIST_1)
{
Eterm list = BIF_ARG_1;
Eterm* cons;
Eterm res;
Eterm* hp;
Sint len;
//获取list长度
if ((len = erts_list_length(list)) < 0 || len > ERTS_MAX_TUPLE_SIZE)
{
BIF_ERROR(BIF_P, BADARG);
}
//开辟元组存储空间 len+1?
hp = HAlloc(BIF_P, len+1);
res = make_tuple(hp);
*hp++ = make_arityval(len);
while(is_list(list))
{
//指针类型转化
cons = list_val(list);
//CAR返回list中的第一个元素
*hp++ = CAR(cons);
//CDR返回list中首元素后的元素
list = CDR(cons);
}
BIF_RET(res);
}
从以上可以看出人们常说的相同的数据,列表(list)所占的存储空间是元组(tuple)的2倍是不准确的,元组的实际长度是存储数据数加一,元组头部包含一个Header用于保存数据个数。再从CAR与CDR可以猜测出list的一个结构,类似链表如下
A1|P1->A2|P2->A3|P3... A代表数据,P表示指针,所以才会存在list存储空间是tuple两倍的说法,从tuple_to_list()的实现代码将得到更加清晰的认识
tuple_to_list:
/* convert a tuple to a list */
BIF_RETTYPE tuple_to_list_1(BIF_ALIST_1)
{
Uint n;
Eterm *tupleptr;
Eterm list = NIL;
Eterm* hp;

if (is_not_tuple(BIF_ARG_1))
{
BIF_ERROR(BIF_P, BADARG);
}
//指针类型的转换
tupleptr = tuple_val(BIF_ARG_1);
//tuple的头元素存储tuple的数据长度
n = arityval(*tupleptr);
hp = HAlloc(BIF_P, 2 * n);

tupleptr++;

while(n--)
{
//将新的元素放到列表的开头,它将新元素推入list中
list = CONS(hp, tupleptr[n], list);
hp += 2;
}
BIF_RET(list);
}
从BIF中tuple_to_list的实现可以看到两点:
1 list的实际长度是tuple_size*2-4
2 tuple转化list的时候是从尾部数据开始依次给list头部赋值,然后list更新头指针,也就是说构建list时,list的指针是始终指向头部的,所以list进行数据增删时,从头部进行将会有较高的效率
list和tuple总结:
1 list结构类似与C中的链表,但是又与C中的链表存在区别啊a.C链表只能存储同一类型的数据,而Erlang中list可以存储不同类型的数据,形如[1,2,{1,2},"my_world"],指针的类型是可变的 b.Erlang中list数据与指针是单独节点存储,不似C中是数据与指针声明为struct单节点存储
2 Erlang中编程中如果tuple可以满足的场景,尽可能的使用tuple,减少内存的消耗,list可多用于遍历和推导
比较运算符工作机制(补充)
首先对比较运算符两边求值(比如在运算符两边存在算术运算或者包含BIF保护式函数),然后再进行比较
比较定义如下偏序关系:
number<atom<reference<port<pid<tuple<list
列表推导
Joe老爷子在书上写的列表推导格式可总结如下
[Expression || Generators,Generator,...]
但是现在实际还可以支持Gaurds,推导格式如下
[Expression || Generators,Guards,Generators,...]
1>L=[1,2,3].
[1,2,3]
2>[2*X || X <- L,X<3].
[2,4]
列表深层嵌套
构建同构列表(输入为列表,对列表元素进行处理,然后输出列表)
isomorphic([X|T]) ->
[something(X)|isomorphic(T)];
isomorphic([]) ->
[].
比如常见的列表元素翻倍
double([H|T]) ->
[2*H|double(T)];
double([]) ->
[].
操作如下
>list_deal:double([1,2,3,4]).
[2,4,6,8]
以上情况实际只能针对列表最上层数据,若输出列表为[1,2,3,4,[5,6]]则会报错,若想嵌套所有层次,则可以修改如下
double([H|T]) when is_integer(H) ->
[2*H|double(T)];
double([H|T]) when is_list(H) ->
[double(H)|double(T)];
double([]) ->
[].
操作如下:
>list_deal:double([1,2,[3,4],[5,[6,7],8]]).
[2,4,[6,8],[10,[12,14],16]]

有兴趣的可以在考虑如果出掉列表中嵌套的列表符

> list_deal:double([[[1,2,[3,[4,5],6],7],8],9]). 
[2,4,6,8,10,12,14,16,18]
Case/If/when/pattern match判断机制

Erlang语言所把握的case/if与其他语言存在一定程度的差异,如果是从其余语言转过来的程序猿,可能会在刚开学习这门语言的时候产生一定的记忆错乱,所以就编写了一个常见的年份月份天数输出的程序练习,整合了三个判断,帮助记忆
-module(case_if_when).
-export([month_length/2]).

month_length(Year, Month) ->
%%判断当前Year是否为闰年
Leap = if
trunc(Year / 400) * 400 == Year -> leap;
trunc(Year / 100) * 100 == Year -> not_leap;
trunc(Year / 4) * 4 == Year -> leap;
true -> not_leap
end,
case Month of
jan -> 31;
feb when Leap == leap -> 29;
feb -> 28;
mar -> 31;
apr -> 30;
may -> 31;
jun -> 30;
jul -> 31;
aug -> 31;
sep -> 30;
oct -> 31;
nov -> 30;
dec -> 31
end.
case执行过程:将case...of中间表示与Pattern-n逐个进行match ,若match成功则执行相应代码,否则继续往下尝试,直至结束
if执行过程:与C/C++/java所不同点在于只有if,没有else,不需要多层嵌套,只需要逐个进行match,若match成功,则执行相应代码,否则继续往下判断,直至结束,一般会在程序结束位置添加一个true条件,作为前面全部条件为假时的程序入口
when执行过程:与case和if不同的是每次断言都需要一个when表示,when后断言用逗号(",")隔开表示与操作

Erlang OTP
OTP(Open Telecom Platform),包含了一系列项目开发所需的开发模式,轻量部署框架等,多用于构建大规模,容错和分布式的应用程序
OTP行为:gen_server(用于实现C/S结构中的服务端) gen_fsm(用于实现有限状态机)
gen_event(用于实现事件处理功能) supervisor(用于实现监督树中的督程)
应用(application):Mnesia(具备数据库编程所需功能) Debugger(用于调试Erlang程序)
gen_server简单描述
在整个设计中,服务器实际上只是充当代理的角色,真正处理客户端请求的是callback模块
客户端发送请求=>服务器接收客户端请求=>回调模块(callback)处理=>返回客户端结果
OTP惯例将服务器框架代码调用的回调模块和客户端调用的接口方法放入同一模块
实现步骤:1 确定回调模块名 2 编写接口函数 3 在回调模块里编写留个必须的回调函数
六个必须的回调函数:
init/1 -> 服务初始化时被回调,用于初始化,若初始化成功,则返回{ok,State}
handle_call/3 -> 用于处理gen_server:call(ServerRef,Request) 请求(同步调用)-核心
handle_cast/2 -> 用于处理gen_server:call(ServerRef,Request) 请求(异步调用)
handle_info/2 -> 用于处理程序设定之外的请求,常见用来接受退出消息
handle_terminate/2 -> 用于关闭gen_server服务
code_change/3 -> 用于代码热更替
优点:
1 解耦,服务器功能部分与非功能部分分离,专注于服务器功能模块编写(callback模块)
2 复用,服务器可以复用,开发新功能服务器,可以沿用server框架,只需重新编写功能模块()