Erlang学习笔记/快速入门#

  • Erlang比较神秘,小众。函数式编程。主要适用于服务器端长连接、高并发处理,最初是爱立信针对电信系统专门设计,公认很稳定、容错好,广泛用于通信系统。

  • 很多思想现在看来非常超前并且仍然超前并且不会过时。

  • 各种原因导致不流行(函数式编程不直观,思维方式跟主流语言有差别,不善推广等)

  • Whatsapp用了大量erlang 国内有很多网游公司用了erlang。阿里内部有用(https://www.zhihu.com/question/46442333)。 我们常用的rabbitmq和emq都是erlang做的。

  • 看下来至少在保持住大量长连接方面比较靠谱,还天生支持”协程“、集群、分布式。内置分布式数据库。

  • 语法、思想确实需要适应。有的写法非常简明,有的写法就像数学公式,非常优美,比如三五行代码就可以实现一个简单的快速排序。

  • 嗯。有必要学习一下。


书籍#

  • << Programming Erlang.2Ed >> erlang发明者的书

  • << Designing for Scalability with ErlangOTP Implement Robust, Fault-Tolerant Systems>> 深入学习。otp等。

  • << Erlang and OTP in Action>> otp

  • << Introducing Erlang Getting Started in Functional Programming>> 内容少。可快速学习或当作参考

  • << learn_you_some_erlang_for_great_good>> 结构类似第一本。可作参考。


安装erlang运行环境#

  • https://www.erlang.org/downloads 官网下载erlang环境。目前版本是OTP 23。安装后可用erlang shell直接编译和运行erlang代码。

  • 简单的命令可用erlang shell。 复杂的可用ide。后续会介绍。

  • 官网 https://www.erlang.org/ 在线文档 https://erldocs.com/

  • windows环境变量path里添加erlang的bin目录


基本概念#

  • 注释 %号后面是注释

  • 句号 命令结束时用英文句号.结尾。 X = 20.

  • 变量 大写字母或_开头的字符串表示一个变量。变量只能赋值(绑定)一次。之后不能再赋值,否则报错。

X = 20. % 绑定了20

如果再输X = 20.会报错

  • atom 小写字母开头的字符串。可以看作一个标签。常用于各种匹配。

  • 多元组tuple P = {2, gg, {name, 3}}. PP = {k, P} 可嵌套。其中gg, name, k都是atom。P, PP是tuple变量,绑定了相应的值。 可以反向取出tuple中的值。

{P1, P2, P3} = P. % 这样P1绑定了2,P2绑定了gg,P3绑定了{name, 3}
  • 列表List。跟数组一个意思,可存数字,atom,tuple,list等。 A = [{a, 2}, 3, b]

  • 取出list中的数据 可以用[HEAD|TAIL]的形式去匹配list。|左边的HEAD会匹配开头的一个元素,TAIL会匹配剩下的list。 例如

L = [1,2,3,4,5,6].
[L1|L2] = L. % L1为1,L2为[2,3,4,5,6]。注意L2还是一个list
[L3,L4|L5] = L. % L3为1,L4为2,L5为[3,4,5,6]。
  • 字符串

Name = "Shagua". % 绑定字符串
[83,117,114,112,114,105,115,101]. % 会输出"Surprise"
B = "信仰" % 会输出[20449,20208]

这里会有各种转换,默认貌似是utf8编码。

  • 模式匹配 erlang里充满了匹配。

{X, abc} = {123, abc}. % 成功 X绑定了123
{A, B, C} = {1, 2, "ggg"}. % 成功
{AA, BB, CC} = {{aa, a}, bb, "cc"}. % 成功
{AAA, BBB} = {"a", "b", cc}. % 失败 ”形状“不一致
[H|T] = [1,2,3,4]. % 成功 H=1, T= [2,3,4]
[A, B, C | T] = [a, b, c, d, e, f]. % 成功 A = a, B = b, C = c, T = [d, e, f]
{GG, abc} = {123, gg}. % 失败 abc和gg两个atom匹配不了

模块和函数#

开始用ide#

  • ide用intellij idea社区版。 https://www.jetbrains.com/idea/download/

  • ide的erlang插件。本来可以在ide里装,但是被墙。只能下载后本地安装。 https://github.com/ignatov/intellij-erlang

  • 打开ide,配置plugin,选从硬盘安装上面的插件。

  • 新建erlang项目,src目录下新建Erlang File,选Empty module,指定模块名test。看到有几行默认代码。

    改一下代码:

-module(test). % 指定这个erlang模块名字是test

-export([gg/0]). % 表示这个模块会暴露一个接口gg,斜杠后面是参数个数,0就是0个参数。

gg()->
    io:format("gg~n").

定义一个函数gg,没有参数。 io是erlang自带的io库。format可打印字符。~n是换行。

ide里run。看到打印出gg。这样环境就ok了。

以后每次新学一个内容可以新建一个module,在ide中右键recompile,然后在test中调用。 也可以盯着一个module改代码。


run的配置#

  • 打开Edit Configuration,可修改运行配置。 可以看到Module and function 默认生成了test gg。 即运行test模块的gg函数。 以后要运行多个模块多个函数的话需要自行添加和修改这个config。


函数的例子1:#

-module(geometry).
-export([area/1]).
area({rectangle, Width, Height}) -> 
    Width * Height;
area({square, Side}) -> 
    Side * Side.

同样的函数名area有两个,参数的匹配模式不一样,可以共存。注意中间是用;分隔的。

调用方式:

A = area({rectangle, 3, 4}). 会匹配第一个定义(两个rectangle匹配)

B = area({square, 12}). 会匹配第二个定义(两个square匹配)

可以进行扩展 area({circle, Radius}) -> 3.14159 * Radius * Radius.


函数的例子2:#

erlang没有直接的循环。可以这样做一个循环:

    for(Max, Max, F) -> [F(Max)];
    for(I, Max, F) -> [F(I)|for(I+1, Max, F)].

从第二个函数开始。对I调用F,作为list的第一个元素,再把I+1,再次调for,把返回接到list后面。 如果I增加到与Max相同,则匹配到第一个for,返回F(I)。 这样实现了对I到Max都调用F,把结果组成一个list。


函数的例子3:#

sum([H|T]) -> H + sum(T);
sum([]) -> 0. 

非空list匹配第一个sum,取出第一个元素H,对于list后续部分调用sum,最后返回两者的和。 到最后T为空时,返回0,全过程结束。 这样实现了求list的和。

函数的例子4:#

list可以做这样的变换

L = [1,2,3,4,5].
[2*X || X <- L ].     % [2,4,6,8,10]

这样可以对list每个元素进行操作,得到新的list。

qsort([]) -> [];
qsort([Pivot|T]) ->
    qsort([X || X <- T, X < Pivot])
    ++ [Pivot] ++
    qsort([X || X <- T, X >= Pivot]).

这个例子实现快速排序。快速排序主要思想时分而治之。 简单说是取数组中一个元素A,把比它小的放左边,把比它大的放右边,这样A的位置必然是正确的,然后把左右部分都这样递归操作下去,最后就能排好序。 当然这里只是一个简单的实现,现实实现要考虑各种情况、初始化数据,如何取A等。

[Pivot|T] 这个参数就把第一个元素当作了标点。其余的为T

  1. [X || X <- T, X < Pivot] 把T中所有小于Pivot的元素做成一个list,并对其qsort。

  2. [Pivot]

  3. [X || X <- T, X >= Pivot] 把T中所有大于Pivot的元素做成一个list,并对其qsort。 把123接起来就得到了排好序的list。


case功能#

就是根据匹配,分情况处理。

case Expression of
    Pattern1 -> Expr_seq1;
    Pattern2 -> Expr_seq2;
    ...
end


fall_velocity(Planemo, Distance) ->
    case Planemo of
        earth -> math:sqrt(2 * 9.8 * Distance);
        moon -> math:sqrt(2 * 1.6 * Distance);
        mars -> math:sqrt(2 * 3.71 * Distance) % no closing period!
    end.

二进制数据和位操作#

<<5,10,20>>. 双尖括号包起来就是字节串。里面每个元素值必须在0-255之间。否则会被截取后8位。

按位拼字节

X = 1.
Y = 2.
Z = 233.
MM = <<X:3, Y:1, Z:2>>.
MM. % 得到 <<9:6>>

XYZ分别取最后3位1位2位再接起来得到1001即9。6bit。

FF = <<X:3, Y:1, Z:5>>.
FF. % 得到<<36,1:1>>
233 = 0b11101001 % 最后5位拿出来。前四位拼到前一个字节,最后剩一个1。
0b00100100,0b1

按位取字节

<<A:5, B:1, C:3>> = FF. 

A得到0b00100。B得到0b1。C得到0b1 取的时候总长度要匹配,否则报错。

解析和拼装协议数据可能会非常方便。

其他复杂用法待学习。。


Erlang进程#

  • process是erlang的核心概念。 erlang的进程是在erlang虚拟机层面实现的,不是操作系统层面的进程。可与协程类比。

  • shell里输入self(). 返回<0.102.0>这样的数据,为当前进程的标识,即Pid。

基本概念:#

  • process的创建和销毁非常快 process间传消息非常快 process跨平台,在所有平台表现一致 可以同时存在大量的process process不共享内存,是完全独立的。 process间通信的唯一方式是传消息

基本操作1:#

  • 创建一个process,使用Mod模块的Func函数,参数为Args。返回Pid Pid = spawn(Mod, Func, Args).

基本操作2:#

  • 发Message给Pid Pid ! Message.

基本操作3:#

  • 收数据

receive
    Pattern1 ->
        Expressions1;
    Pattern2 ->
        Expressions2;
    ...
end

现在可以做一个简单的服务。#

起一个server.erl

-module(server).
-author("xc").

-export([loop/0]).                            % 导出loop接口

loop()->
    io:format("loop~n"),
    receive                                   % 接收数据
        {From, {rectangle, Width, Height}}->  % 匹配长方形参数
            From ! Width * Height,            % 发送结果到From
            loop();                           % 再次开始收数据。通常这样递归会不断消耗栈空间,最后溢出。erlang有尾递归优化,不会重复消耗栈。
        {From, {circle, R}}->                 % 匹配圆形参数
            io:format("circle~n"),
            From ! 3.14159 * R * R,
            loop();
        {From, Other}->                       % 匹配其他
            From ! {error, Other},
            loop()
    after 5000 ->                             % 5秒超时会走到这里
        io:format("timeout 1~n"),
        loop()
    end.

起一个test.erl,启动server的loop,并给它发消息。

-module(test).
-author("xc").

-export([test_1/0]).                     % 导出test_1接口
-import(server, [loop/0]).               % 导入loop接口

test_1 ()->
    Pid = spawn(server, loop, []),       % 创建一个process,运行server模块的loop函数。得到Pid
    Pid ! {self(), {circle, 12}},        % 发消息给Pid。消息是{自身Pid, {circle, 12}},匹配到loop中的{From, {circle, R}}
    receive
        Result ->
            io:format("~p~n", [Result])
        after 3000->
            io:format("after 3000")
    end.

可以把某个pid和atom绑定,变的好读好找。以后直接给这个atom发消息就行了。

Pid1 = spawn(bounce,report,[1]).
register(bounce,Pid1).
bounce ! hello.

另外erlang自带一个监控页面。有当前节点的各种重要数据,包括mnesia数据库。还可以进行相关操作。 observer:start().


数据存储#

  • tuple到record -record(planemo, {name, gravity, diameter, distance_from_sun}).

  • map Planemos = #{ earth => 9.8, moon => 1.6, mars => 3.71 }. maps:get(moon, Planemos). %

  • ETS: Erlang Term Storage 一种erlang数据表

    有四种形式 set 同个key只能存一个值

    ordered set 同个key只能存一个值。key有序

    bag 同个key可以存多个值。同个key下的值不能重复。

    duplicate bag 同个key可以存多个值。同个key下的值可以重复。

  • 创建表 T = ets:new(my_table, [ordered_set])

  • 插入数据 ets:insert(T, {gg, {gt, "4wf", <<56>>}}). observer:start().可看数据

  • 取数据 ets:lookup(T, gg).

  • 删除数据 ets:delete(TT, gg).

  • 删除表 ets:delete(TT).

  • mnesia(希腊语memory) erlang自带的数据库。轻量快速、分布式、可同步、支持事务。 可直接存在内存。可直接存erlang的record。 不适用于大规模数据(虽然有成功案例),非常适合小规模应用、集群节点间维护一致的数据等。


分布式相关#

  • erlang自带分布式方案 https://www.erlang.org/doc/reference_manual/distributed.html


程序发布#

  • www.rebar3.org 是erlang常用的打包、发布工具。

  • 下载rebar3

  • 按https://www.rebar3.org/docs/getting-started windows上在rebar3文件同个目录建一个rebar3.cmd

  • 填入 @echo off escript.exe "%~dpn0" %*

  • 保存。再cmd运行rebar3 –help。可正常运行即可。

  • 创建新app rebar3 new app myapp

  • 编译 rebar3两个文件复制进目录 rebar3 compile

  • 创建新release rebar3 new release myrelease


erlang版本管理#

  • erlang otp会持续进化、发布新版本。那么如果需要测试、对比、升级的话,需要不同版本共存。

  • 用kerl可以管理erlang版本 https://github.com/kerl/kerl


Elixr#

  • 原版erlang还是比较晦涩,有诸多不便。 Elixr在erlang基础上发展和改进。沿用erlang的vm。 把erlang的语法和otp改的更舒服。 另外扩展了多种工具、功能(模板、测试、打包发布)。


哈哈那上面erlang是不是白学了?#