美文网首页
Erlang/OTP OCP 面向并发编程入门

Erlang/OTP OCP 面向并发编程入门

作者: 坚果jimbowhy | 来源:发表于2020-06-18 02:51 被阅读0次

    Erlang Introduction

    秀秀我的开发环境

    Erlang 最初是爱立信为开发电信相关产品而产生,即 OTP - Open Telecom Platform 缩写,Erlang 开源前这个名字多少还有点品牌效应。

    无论 Erlang 还是 OTP 都早已不再局限于电信应用:更贴切的名字应该是“并发系统平台”。

    Erlang 是一种面向并发 Concurrency Oriented,面向消息 Message Oriented 的函数式 Functional 编程语言。

    Erlang 应用场景

    • 分布式产品,网络服务器,客户端,等各种应用环境。
    • Erlang 也可以作为一种快速开发语言,进行原型开发。
    • 应用需要处理大量并发活动。
    • 需要良好的软件或硬件 fault-tolerant 容错能力。
    • 软件产品需要在多服务器中具有良好的伸缩能力,而不必大改动。
    • 容易实现不中断服务进行升级过程,如游戏服务器。
    • 软件需要在严格的时间片响应用户,如游戏服务器。

    我学了两天两夜的二郎,学它备用,游戏服务端开发非常有用!如果低延迟对你的应用来说很重要,那么平心而论,不选 Erlang 反而显得很奇怪了。

    游戏服务器是后端,做后端的,每天耳濡目染横向扩展,自动伸缩等炫酷的特性,要说放在以前,这些特性还是巨头的”专利”,我们想要自己实现这些东西挑战性是比较大的,但近几年有了容器生态如 k8s 的加持,只要你实现了一个无状态应用,你几乎马上就可以得到一个可伸缩的集群,享受无状态本身带来的各种好处,机器挂了自动重启,性能不够了就自动扩展等等。而作为一名游戏服务器开发者,自然也想充分享受容器时代的红利。

    面向并发说明 Erlang 支持大规模的并发应用,我们可以在应用中处理成千上万的并发,而不相互影响。面向消息,其实是为并发服务!我们应该都熟悉多线程,熟悉加锁解锁操作,熟悉可能出现的资源竞争与死锁。在 Erlang 的世界里,我们可以将轻轻的抹去这些令人苦恼的词汇。Erlang 的世界,每个处理都是独立的个体,他们之间的交互仅仅靠消息!因此不会有死锁,不会有那种痛苦的编程经历。

    Erlang 中一个非常重要的名词 Process,也就是我们前面提到的个体。它不是我们操作系统中的进程,也不是线程。它是 Erlang 提供给我们的超级轻量的进程。为了适应大规模并发的特性,Process 需要能够快速创建,快速销毁。Process 之间通信的唯一方法就是消息,我们只要知道一个 Process 的名字即 pid,就可以向其发送消息。Process 也可以在任何时候,接收消息。我们这样做只有一个目的:让我们的系统更加简单,用一种朴素的做法,实现一个高效的语言。

    Erlang 是种函数式编程语言,对此我没有很深刻的理解,最明显的特征就是,Erlang 中到处都是函数,函数构成了我们的产品的主体,把这些函数放到一个个的 Process 中去,让他们运行起来,那么就组成了我们朝气蓬勃的产品。

    Erlang 支持对数据的位操作,拥有丰富的数据持久化机制。

    同时需要说明的是 Erlang 内建垃圾回收机制 GC。

    作为 Erlang 创始人,瑞典 Joe Armstrong 在软件工程领域贡献是巨大的,不必说 Erlang 与 OTP, 光他的论文《面对软件错误构建可靠的分布式系统》就足以载入史册,领先现在几十年,提出了OOP 等思想本质上不是并发的正确处理方法。

    2019年4月20日,Erlang 语言设计者 Joe Armstrong 去世,享年 68 岁。

    1986 年,Joe Armstrong 和 Robert Virding、Mike Williams 在电信公司爱立信共同创造了应对大规模并发场景的编程语言 Erlang,这一语言起初是爱立信的私有语言,后于 1998 年开源。

    Erlang 是一门相对小众的编程语言,这一点与 Lisp 很像 —— 小众但影响很大。Joe Armstrong 曾用一句话概括过 Erlang 的优点:一次编写,永远运行。

    Joe Armstrong 在论文中是这样认为的:几乎所有传统的编程语言对真正的并发都缺乏有力的支持——本质上是顺序化的,而语言的并发性都仅仅由底层操作系统而不是语言提供。

    而用对并发提供良好支持的语言,也就是作者说的面向并发的语言 COL - Concurrency Oriented Language 来边写程序,则是相当容易的:

    • 从真实世界的活动中识别出真正的并发活动
    • 识别出并发活动之间的所有消息通道
    • 写下能够在不同消息通道中流通的所有消息

    其次,通过定下的九条原则性思想设计,写出来天然支持分布式系统的 Erlang 以及 OTP 框架,真的做到了他说的实现面向并发的语言。

    • 一切皆进程
    • 进程强隔离
    • 进程的生成与销毁都是轻量的操作
    • 消息传递是进程交互的唯一方式
    • 每个进程有唯一的名字
    • 你若知道进程的名字,就可以向他发消息
    • 进程之间不共享资源
    • 错误处理非本地化
    • 进程要么正常跑着,要么马上挂掉

    就以上九条的观念,设计出的 Erlang 语言,成就了可靠性达到 99.9999999% 的目前世界上最复杂的 ATM 交换机。

    其三,let it crash 思想的提出与实现。

    程序不可能处理一切错误,因此程序员只要力所能及的处理显然易见的错误就好了,而那些隐藏着的,非直觉性的错误,就让他崩掉吧——本来就很有可能是极少见的错误,经常出现的?就需要程序员人工处理了,这是紧急情况,就算 try catch 所有错误也无法避免,因为系统已经陷入崩溃边缘了,苟延残喘下去只是自欺欺人。并且,不恰当地使用 try catch 还会埋下隐患,让系统带病运转。

    其四,一切进程都是轻量级的,都可以被监控 monitor,有 Supervisor 专门做监控。你可以方便的用一个 supervisor 进程去管理子进程,supervisor 会根据你设定的策略,来处理意外挂掉的子进程。这种情况的问题的是,错误处理稍微做不好就会挂,策略有:

    • one_for_one:只重启挂掉的子进程
    • one_for_all:有一个子进程挂了,重启所有子进程
    • rest_for_one:在该挂掉的子进程 创建时间之后创建的子进程都会重启。

    Erlang 语言特性

    1. 简单小巧

    Erlang 简单小巧只有 6 种基本的数据类型,另外提供几种复合结构,这就是 Erlang 的所有数据类型。

    • Atom
    • bitstring
    • Number (float, integer)
    • List
    • Maps
    • Tuple
    • Reference
    • Fun
    • Port
    • Pid
    • String
    • Record
    • Boolean

    在 Erlang 中表示任何类型的数据都叫做 Terms,它是源代码中的基本数据类型。而常见的 string 在 Erlang 中是以位串 bitstringList 表达的。没有 Boolean 类型,使用 atoms 原子类型的 true & false 替代。

    1. 模式匹配

    在 Erlang 的函数中,= 号不是赋值,而是模式匹配,某些语法中,如 C# 8.0 也可以使用 Pattern 匹配,这是一个非常好的特性,我们可以让代码自己去决定如何执行。

    比如,我们定义一个函数,其告诉我们某种水果的价格:

    price(apple) -> 2.0;
    price(banana) -> 1.2.
    

    我们随后调用 price(Fruit),会根据 Fruit 变量的内容返回具体的价格。这样做的好处就是节省了我们的代码量,我们不用 if...else… 或者 switch…case 的来伺候了。也便于代码的扩展:加一个新的水果品种,我们只需要加一行就可以了。

    学习 Erlang 一个非常重要的内容就是模式匹配,但是请不要混淆,这个匹配和正则表达式没有任何干系。

    1. 变量单次赋值

    一个匪夷所思的特性,变量竟然只能单次赋值!是的 Erlang 中变量一旦绑定某个数值以后,就不能再次绑定,这样做的好处是便于调试出错,更深层次的原因是 Erlang 为并发设计,如果变量可以修改,那么就涉及到资源的加锁解锁等问题,当发生错误时,某个变量是什么就永远是什么,不用顺藤摸瓜的查找谁修改过它,省了好多事情。唯一的麻烦就是需要一个信的变量时,你必须再为它想一个名字。

    1. Erlang 中提供丰富的 libs
    • stdlib 中包含大量的数据结构如 lists,array,dict,gb_sets,gb_trees,ets,dets 等
    • mnesia 提供一个分布式的数据库系统
    • inets 提供 ftp client,http client/server,tftp client/server
    • crypto 提供加密解密相关函数,基于 openssl 相关实现
    • ssl 实现加密socket通信,基于openssl实现
    • ssh 实现ssh协议
    • xmerl 实现XML相关解析
    • snmp 实现SNMP协议(Simple Network Management Protocol)
    • observer 用来分析与追踪分布式应用
    • odbc 使 Erlang 可以连接基于SQL的数据库
    • orber 实现 CORBA 对象请求代理服务
    • os_mon 提供对操作系统的监控功能
    • dialyzer 提供一个静态的代码或程序分析工具
    • edoc 依据源文件生成文档
    • gs 可以为我们提供某些 GUI 的功能(基于Tcl/Tk)

    还有很多朋友提供了一些开源的lib,比如eunit,用来进行单元测试。

    1. 灵活多样的错误处理

    Erlang 最初为电信产品的开发,这样的目的,决定了其对错误处理的严格要求。Erlang 中提供一般语言所提供的 exception,catch,try…catch 等语法,同时 Erlang 支持 LinkMonitor 两种机制,我们可以将 Process 连接起来,让他们组成一个整体,某个 Process 出错,或退出时,其他 Process 都具有得知其推出的能力。而 Monitor 顾名思义,可以用来监控某个 Process,判断其是否退出或出错。所有的这些 Erlang 都提供内在支持,我们快速的开发坚固的产品,不再是奢望。

    1. 代码热替换

    你的产品想不间断的更新么?Erlang 可以满足你这个需求,Erlang 会在运行时自动将旧的模块进行替换。一切都静悄悄。

    1. 天生的分布式

    Erlang 天生适合分布式应用开发,其很多的 BIF 内建函数都具有分布式版本,我们可以通过 BIF 在远程机器上创建 Process,可以向远程机器上的某个 Process 发送消息。在分布式应用的开发中,我们可以像 C、C++,Java 等语言一样,通过 Socket 进行通讯,也可以使用 Erlang 内嵌的基于 Cookie 的分布式架构,进行开发。当然也可以两者混合。分布式开发更加方便,快速。Erlang 的 Process 的操作,Error 的处理等都对支持分布式操作。

    1. 超强的并发性

    由于采用其自身 Process,而没有采用操作系统的进程和线程,我们可以创建大规模的并发处理,同时还简化了我们的编程复杂度。我们可以通过几十行代码实现一个并发的TCP服务器,这在其他语言中都想都不敢想!

    1. 多核支持

    Erlang让您的应用支持多个处理器,您不需要为不同的硬件系统做不同的开发。采用Erlang将最大限度的发挥你的机器性能。

    1. 跨平台

    如同JAVA一样,Erlang 支持跨平台(其目前支持linux,mac,windows等19种平台),不用为代码的移植而头疼。

    我们仅仅需要了解平台的一些特性,对运行时进行优化。

    1. 开源

    开源是我非常喜欢的一个词汇,开源意味这更加强壮,更加公开,更加的追求平等。开源会让 Erlang 更好。

    Erlang 与外界的交互

    Erlang 可以与其他的语言进行交互,如 C、C++,Java。当然也有热心的朋友提供了与其他语言的交互,如果需要你也可以根据 Erlang 的数据格式,提供一个库,让 Erang 与您心爱的语言交互。

    Erlang 支持分布式开发,您可以创建一个 C Node,其如同一个 Erlang 节点,前提是你遵照 Erlang 的规范。

    当然最常用的交互还是再同一个 Node 上,比如我们要调用某个 lib,调用一些系统提供的功能,这时候主要有两种方式:Port 和嵌入式执行。

    Port 是 Erlang 最基本的与外界交互的方式,进行交互的双方通过编码,解码,将信息以字节流的方式进行传递。(具体这个通道的实现方式,根据操作系统的不同而不同,比如 Unix 环境下,采用 PIP E实现,理论上任何支持对应 Port 通道实现的语言都可以与 Erlang 进行交互)。Erlang 为了方便 C 和 JAVA 程序员,提供了 Erl_Interface 和 Jinterface。

    采用 Port,您的代码在 Erlang 的平台之外运行,其崩溃不会影响 Erlang。

    嵌入式执行,通过 Erlang 平台加载,因此这是非常危险的,如果您的程序崩溃,没有任何理由,Erlang 也会崩溃。

    Erlang 批评者

    曾经使用过一段时间 Erlang,结论是:方便的地方真的方便,但麻烦的地方真的很麻烦。最终放弃 Erlang 并不是因为社区,文档,或者开源项目的多少,而是因为语言本身。首先是状态问题,比如要在 Erlang 中操作二维地图,很多人都选择用C来实现:Erlang 如何操作游戏中的二维地图?

    游戏引擎 Erlang 写无状态的代码是非常的爽的,代码就像一个个数学公式把程序给 “定义” 出来,模式匹配有时也很高效。确实很适合电信系统这种请求与请求间隔离的,前后逻辑关系不大的“非状态系统”,比如 HTTP,比如棋牌或者回合制游戏。但两个请求间如果逻辑交互很频繁,比如动作游戏,ARPG,两个角色间的交互频繁了,牵扯装太多了,用 Erlang就比较麻烦了,别人一个函数调用解决的问题,Erlang可能要几个actor之间不停的消息中转。

    Erlang是一个专业化定制程度很高的语言(非状态类电信系统,请求隔离),所以不能因为 Erlang 在有的地方比其他语言开发效率高8倍(尽管似乎号称),就觉得 Erlang在任何时候开发效率都很高,比如你在 .BAT 文件里面可以这样:

    DEL d:\temp\*.jpg 
    

    换成 C++ 可能要写7,8行,大家就觉得 .BAT比 C++方便一样。处理文件和目录或许是,但你说用BAT写点除此之外别的东西,它就傻逼了,Erlang 也是一样,方便的地方挺方便,别扭的地方别扭死你,关键还是 Scala 和 Go 的设计充满了“妥协”,而 Erlang 里充满了 “各种原则”。在适合的领域,这些原则能让你很酸爽,而跳出那个圆圈,这些 “绝不妥协的原则” 会让你花数倍的时间和精力去完成原本很直接的事情。

    1. Erlang陡峭的学习曲线

    大多数情况下,学一门新语言,大部分基本概念都可以靠其他语言的经验快速理解,比如你如果学过 C,再学 Java 不是什么难事。但是 erlang 正相反,你要先设法忘掉其他语言的一些概念,比如变量这个概念,在 erlang 中是不存在的。这些概念是如此地根深蒂固,让我很难 think in erlang,以至于读完一本 erlang 的教程,我仍然写不出来斐波那契数列的程序.

    1. 这门语言没有什么 killer app

    每一种流行的语言都一定有用这种语言实现的、应用广泛的系统,以及由此衍生的庞大社区。社区中的布道者会把这个语言推向更多的应用场景。比如 php 的 wordpress、druple,python 的 web 框架 Django,用于机器学习的 sklearn。但是对于 erlang,除了 rabbitmq,jabber,似乎没有太多 killer app。

    Getting Started

    Erlang 官方文档提供以下内容,其中用户手册根据不同的内容特点分成四个部分:

    • Erlang/OTP Documentation
    • Erldocs
    • Erlang Reference Manual - User's Guide
    • Efficiency Guide - User's Guide
    • System Principles - User's Guide
    • OTP Design Principles - User’s Guide
    • OTP Versions Tree

    还提供书籍 Erlang books:

    • Programming Erlang: Software for a Concurrent World
    • Learn You Some Erlang for Great Good!
    • Erlang Programming
    • Erlang and OTP in Action
    • Introducing Erlang
    • Designing for Scalability with Erlang/OTP

    可以直接从 Erlang Reference Manual 开始从零学习,或者跟随 Erlang 实践 Erlang and OTP in Action。

    安装 Erlang 后,需要将 Erlang 的 bin 目录加入环境变量 Path 之中。

    Erlang/OTP 文件类型:

    Extension File Type Documented in
    .erl Module Erlang Reference Manual
    .hrl Include file Erlang Reference Manual
    .rel Release resource file rel(4) manual page in SASL
    .app Application resource file app(4) manual page in Kernel
    .script Boot script script(4) manual page in SASL
    .boot Binary boot script -
    .config Configuration file config(4) manual page in Kernel
    .appup Application upgrade file appup(4) manual page in SASL
    relup Release upgrade file relup(4) manual page in SASL

    在 Sublime 上编写程序,只需发配置以下编译配置,将文件保存到 Packages\User\erlang.sublime-build

    {
        "env": {
            "path":"c:\\Program Files\\erl10.4\\bin;%path%"
        },
        "working_dir": "$file_path",
        "cmd": "csc.exe $file",
        "file_regex":"^([^:]+):(?:([0-9]+):)?(?:([0-9]+):)? (.*)",
        "selector": "source.erlang",
        "encoding": "cp936",
        "quiet": true,
        "variants": [{
            "name": "Run ...",
            "shell_cmd": "erlc $file_name && erl -noshell -s $file_base_name start -s init stop"
        }]
    }   
    

    helloworld

    Erlang 程序的运行一般需要两个步骤,即编译和运行。通过编译生成与程序文件的主文件名相同而扩展名为 .beam。要运行 Erlang 程序,可以在 Erlang 的交互式命令行下或直接在命令行下编译后运行。

    如下,执行 erl 命令或窗口版 werl 开始 HelloWorld,q() 其实只是 shell 上 init:stop() 的别名:

    >erl
    Eshell V10.4  (abort with ^G)
    1> io:format("Hello WOrld!").
    Hello WOrld!ok
    2> q().
    ok
    3>
    

    使用 Erlang shell 编译运行 .erl 程序

    c(hello).
    

    编写一个 hello.erl 程序,后面有三种方式运行它:

    -module(hello).
    -export([fac/1]).
    
    fac(0) -> 1;
    fac(N) -> N * fac(N-1).
    

    把这些存储到文件 hello.erl 中,文件名必须与模块名相同。

    • -module 表示定义一个模块;
    • -export 表示导出一个函数列表,列表格式 [Fun/N1, Fun/N2 ...],数字是参数个数,这里只导出了一个 fac 函数;
    • -import(io,[fwrite/1]). 导入函数的格式类似导出,它需要指定导入的模块;
    • % 注解符号,没有注释块;
    • . 句点表示 Erlang 代码的行的结束,每条语句都需要句点结束。

    使用 erl 编译这个程序使用如下命令,并且运行:

    3> c(hello).
    {ok,hello}
    30> hello:fac(20).
    2432902008176640000
    4> hello:fac(40).
    815915283247897734345611269596115894272000000000
    32> _
    

    确保工作目录与程序所在目录为同一个目录,避免 erl 找不到文件。然后执行编译 c(hello). 出现 {ok,hello} 说明编译成功,可以执行程序了。

    在命令行编译和运行,erlc 命令提供了一个公共的途径来运行所有 Erlang 系统的编译器,erlc 会根据于各输入文件的扩展名来调用合适的编译器。

    $ erlc hello.erl
    $ erl -noshell -s hello fac -s init stop
    

    Erlc 编译一个或一个以上文件,文件必须包括它们的扩展名,需要通过扩展名来调用正确的编译器。例如 .erl 代表 Erlang 源代码,而 .yrl 代表 Yecc 源代码。

    使用 erl 命令来调用模块中的函数运行程序,设置参数如下:

    • -noshell 启动 Erlang 而没有交互式 shell,此时不会提示 Erlang 的启动信息
    • -s hello fac 运行函数 hello:fac() ,注意使用 -s Mod ... 选项时,相关的模块 Mod 必须已经编译完成了。
    • -s init stop 当我们调用 apply(hello,fac,[]) 结束时,系统就会对函数 init:stop() 求值。

    使用 escript 可以直接运行程序,不需要先编译。想要以 escript 方式运行 hello,需要创建如下文件,提供 main(_) 入口函数:

    #! /usr/bin/env escript
    -module (coding).
    -export ([start/0]).
    
    main(_) ->
        io:format("Hello world\n").
    
    start() ->
        io:format("Hello World! ~n").
    
    % io:format("consulting .erlang in ~p~n",[element(2,file:get_cwd())]).
    % c:cd("g:/programing/programingerlang").
    % io:format("Now in:~p~n",[element(2,file:get_cwd())]).
    

    然后执行:

    > escript hello
    Hello world
    

    在 Linux 中编写 Shell 程序运行 Erlang:

    #!/bin/sh
    #---
    # Excerpted from "Programming Erlang",
    # published by The Pragmatic Bookshelf.
    # Copyrights apply to this code. It may not be used to create training material, 
    # courses, books, articles, and the like. Contact us if you are in doubt.
    # We make no guarantees that this code is fit for any purpose. 
    # Visit http://www.pragmaticprogrammer.com/titles/jaerlang for more book information.
    #---
    erl -noshell -pa /home/joe/2009/book/JAERLANG/Book/code\
                 -s hello start -s init stop
    

    使用格式输出:

    -module(helloworld). 
    -export([start/0]). 
    
    start() -> 
       X = 40.00, 
       Y = 50.00, 
       io:fwrite("~f~n",[X]), 
       io:fwrite("~e",[Y]).
    

    Output

    40.000000
    5.00000e+1
    

    输出使用的格式字符串一般格式 ~F.P.PadModC.

    • ~ 波浪号表示格式定义;
    • C 决定输出数据类型,这是必要的,其它如 F、P、Pad、Mod 部分都是可选的;
      • ~c 定义数字显示为 ASCII 字符串格式:
      • ~s 定义字符串格式:
      • ~f 定义浮点数格式:
      • ~n 定义换行符号;
      • ~e 科学计数法格式,默认精度 6 位,至少 2 位;
    • Pad 定义填充符号,默认是空格,比如填充 # 号,~..#C
    • Mod 定义控制修饰序列,如货币是 t,:
    • P 定义精度:
    • F 定义字段宽度 Field width,负值表示左对齐,省略表示按数据要求长度输出,如果指定宽度不足则用 * 填充:
    • B 定义基数,2-36,默认是 10,比如二进制显示 io:fwrite("~.16B~n", [31]).
    • X 类似 B 但使用前缀,比如 16 进制前显示 0x, io:fwrite("~.16X~n", [-31,"0x"]).

    Mod is the control sequence modifier. This is one or more characters that change the interpretation of Data. The current modifiers are t, for Unicode translation, and l, for stopping p and P from detecting printable characters.

    If F, P, or Pad is a * character, the next argument in Data is used as the value. For example:

    1> io:fwrite("~*.*.0f~n",[9, 5, 3.14159265]).
    003.14159
    ok
    

    To use a literal * character as Pad, it must be passed as an argument:

    2> io:fwrite("~*.*.*f~n",[9, 5, $*, 3.14159265]).
    **3.14159
    ok
    

    在 erl 中使用常用 sheel 函数:

    • b() − Prints the current variable bindings.
    • f() − Removes all current variable bindings.
    • f(x) − Removes the binding for a particular variable.
    • h() − Prints the history list of all the commands executed in the shell.
    • history(N) − 设置历史记录为 N 条,返回旧设置值,默认值 20。
    • e(N) − 重复执行 N 号命令,如果 N 为负数则从最的位置回数,如 e(-1) 执行上一条命令。
    • q() - 退出

    Erlang 数字前面可以用 # 来标注其 Base,语法:Base#Value,默认的 Base 是 10 进制:

    10> 2#101010.  %% 2 进制的 101010
    42
    11> 8#0677.  %% 8 进制的 0677
    447
    12> 16#AE.   %% 16 进制的 AE
    174
    

    Erlang 是函数式语言(虽然也支持副作用)。这意味着 Erlang 里的变量 ‘ Immutable’ (不可变的).
    Immutable variables 在设计上简单,减少了并发过程中处理状态改变带来的复杂性。理解这一点很重要。

    Erlang 是动态类型的语言,但它也是强类型的语言。动态类型意味着你声明变量时不需要指定类型,而强类型是说,erlang 不会偷偷做类型转换:

    1> 6 + "1".
    ** exception error: bad argument in an arithmetic expression
    in operator  +/2
    called as 6 + "1"
    

    Erlang 里变量的命名有约定,必须首字母大写。因为首字母小写的,会被认为是 atom (原子) 类型。

    Erlang 里没有赋值语句,= 号在 Erlang 里是 pattern matching 模式匹配。

    Operators 四类操作符

    Arithmetic operators

    Operator Description Example
    + 两数相加 1 + 2 = 3
    两数相减 1 - 2 = -1
    * 两数相乘 2 * 2 = 4
    / 两数相除 2 / 2 = 1
    rem 求余 3 rem 2 = 1
    div 整除 3 div 2 will give 1

    Relational operators

    Operator Description Example
    == 判断是否相等 2 = 2 = true
    /= 判断是否不等 3 /= 2 = true
    < 左侧是否小于右侧 2 < 3 = true
    > 左侧是否大于右侧 3 > 2 = true
    =< 左侧是否小于或等于右侧 2 =<3 = true
    >= 左侧是否大于或等于右侧 3 >= 2 = true

    Logical operators

    | or | 逻辑或运算 | true or true = true |
    | and | 逻辑与运算 | True and false = false |
    | not | 逻辑非运算 | not false = true |
    | xor | 逻辑异或 | True xor false = true |

    Bitwise operators 比特位运算符号有四个,在逻辑运算符前缀 b 就是对应的位运算。另外还有两个移位操作:

    • bsl (Bit Shift Left)
    • bsr (Bit Shift Right)

    注意,以下数值是十六进制,如下:

    -module(helloworld). 
    -export([start/0]). 
    
    start() -> 
       io:fwrite("~w~n",[00111100 band 00001101]), 
       io:fwrite("~w~n",[00111100 bxor 00111100]), 
       io:fwrite("~w~n",[bnot 00111100]), 
       io:fwrite("~w~n",[00111100 bor 00111100]).
    

    Output

    76
    0
    -111101
    111100
    

    Escape Sequences

    转义符号,在字符串或单引号包括的 atoms 原子类型中使用:

    转义符号 意义
    \b Backspace
    \d Delete
    \e Escape
    \f Form feed
    \n Newline
    \r Carriage return
    \s Space
    \t Tab
    \v Vertical tab
    \XYZ, \YZ, \Z 代表八制字符 XYZ, YZ or Z
    \xXY 代表十六进制字符 XY
    \x{X...} 代表十六进制字符, X... 表示多个十六进制字符
    ^a...^z, ^A...^Z 控制字符 Control A to control Z
    ' Single quote
    " Double quote
    \ Backslash

    Decision Making 条件决策

    If 语句的一般形式、多条件判断和嵌入式,如下面的程序所显示,

    if
    condition1 ->
       statement#1;
    condition2 ->
       statement#2;
    conditionN ->
       statement#N;
    true ->
       defaultstatement
    end.
    

    示例:

    -module(helloworld). 
    -export([start/0]). 
    
    start() -> 
       A = 4, 
       B = 6, 
       if 
          A < B ->
             if 
                A > 5 -> 
                   io:fwrite("A is greater than 5"); 
                true -> 
                   io:fwrite("A is less than 5")
             end;
          true -> 
             io:fwrite("A is greater than B") 
       end.
    

    Case Statements

    case expression of
       value1 -> statement#1;
       value2 -> statement#2;
       valueN -> statement#N
    end.
    

    示例:

    -module(helloworld). 
    -export([start/0]). 
    
    start() -> 
       A = 5,
       case A of 
          5 -> io:fwrite("The value of A is 5"); 
          6 -> io:fwrite("The value of A is 6") 
       end.
    

    Function 函数

    函数定义的一般写法,

    FunctionName(Pattern1… PatternN) ->
    Body;
    

    示例:

    -module(helloworld). 
    -export([add/2,add/3,start/0]). 
    
    add(X,Y) -> 
       Z = X+Y, 
       io:fwrite("~w~n",[Z]). 
    
    add(X,Y,Z) -> 
       A = X+Y+Z, 
       io:fwrite("~w~n",[A]). 
    
    start() ->
       add(5,6), 
       add(5,6,6).
    

    匿名函数,没有与任何名称相关联,示例

    -module(helloworld). 
    -export([start/0]). 
    
    start() -> 
       Fn = fun() -> 
          io:fwrite("Anonymous Function") end, 
       Fn().
    

    匿名函数定义要点:

    • 匿名函数是使用 fun() 关键字定义的
    • 该函数被分配给一个名为 Fn 的变量
    • 该函数是通过变量名称来调用的

    函数可以使用保护序列来防止输入无效参数,语法如下:

    FunctionName(Pattern1… PatternN) [when GuardSeq1]->
    Body;
    

    示例,如果 add 函数被调用为 add(3),该程序将会出现错误:

    -module(helloworld). 
    -export([add/1,start/0]). 
    
    add(X) when X>3 -> 
       io:fwrite("~w~n",[X]). 
    
    start() -> 
       add(4).
    

    Erlang 里面函数是用 函数名/参数个数 来表示的,如果两个函数的函数名与参数个数都一样,他们就是一个函数的两个分支,必须写在一起,分支之间用分号分割。

    如下,clauses.erl 模块定义一个函数的多个分支 clause 就要用 ; 分割:

    -module(clauses).
    -export([add/2]).
    
    %% goes into this clause when both A and B are numbers
    add(A, B) when is_number(A), is_number(B) ->
      A + B;
    %% goes this clause when both A and B are lists
    add(A, B) when is_list(A), is_list(B) ->
      A ++ B.
    %% crashes when no above clauses matched.
    

    上面代码里,定义了一个函数:add/2. 这个函数有两个 clause 分支,一个是计算数字相加的,一个是计算字符串相加的。

    代码里 when 是一个 Guard 关键字,匹配模式 Pattern Matching 和保护序列 Guard 后面讲解。

    运行 add/2 时会从上往下挨个匹配:

    $ erl -pa ebin/
    Eshell V8.3  (abort with ^G)
    1> clauses:add("ABC", "DEF").
    "ABCDEF"
    2> clauses:add(1, 2).
    3
    3> clauses:add(1, 2.4).
    3.4
    4> clauses:add(1, "no").
    ** exception error: no function clause matching clauses:add(1,"no") (clauses.erl, line 4)
    

    第一个 clause:add 匹配的是第二个 clause。 最后一个 clauses:add 都没匹配上,崩溃了。

    Pattern Matching 模式匹配

    变量通过模式匹配绑定到值,在 function call, case- receive- try- 和匹配操作符 = 等表达式中进行模式匹配。

    模式匹配通常用来简单嵌套 if-else 结构。

    Erlang 里变量的命名有约定,必须首字母大写。因为首字母小写的,会被认为是 atom 原子类型。

    Erlang 里没有赋值语句,等号 = 是模式匹配符号,如果 = 左侧跟右侧的值不相等,就叫没匹配上,这时那个 erlang 进程会直接异常崩溃,不要害怕,erlang 是高容错系统,程序崩溃挺正常。

    匹配模式中,左则的模式如果和右侧的 term 匹配,那么模式中未绑定的变量就会绑定到匹配到的值。

    Erlang 中的变量在绑定之前是自由的,非绑定变量可以绑定一次任意类型的数据。为了支持这种类型系统,Erlang 虚拟机采用的实现方法是用一个带有标签的机器字表示所有类型的数据,这个机器字就叫做 term。在 32 位机器上,一个 term 为 32 位宽;在 64 位机器上,一个 term 默认为 64 位宽。由于目前大规模的服务器基本上都是 64 位平台,所以本文下面的讨论都基于 64 位平台。

    示例:

    1> X.
    ** 1: variable 'X' is unbound **
    2> X = 2.
    2
    3> X + 1.
    3
    4> {X, Y} = {1, 2}.
    ** exception error: no match of right hand side value {1,2}
    5> {X, Y} = {2, 3}.
    {2,3}
    6> Y.
    3
    

    程序解析:

    • X 变量开始是未绑定的,然后绑定到 2 这个数值,后面的 X + 1 并非给变量加 1,并没有模式匹配。
    • {X, Y} = {1, 2} 这里的模式匹配失败,因为 X 已经绑定,但和右侧的值不一致。
    • {X, Y} = {2, 3} 这里的模式匹配成功,因为已经绑定的变量 X 和右侧的值一致,而 Y 变量是没有绑定的,所以匹配成功对其绑定为 3。

    列如,在更多的匹配条件中获取值:

    3> {X, 1, 5} = {2, 1, 5}.
    {2,1,5}
    4> X. 
    2
    

    使用匹配来解析 List,将第一个元素绑定到 H, 将其余绑定到 T:

    5> [H | T] = [1, 2, 3].
    [1,2,3]
    6> H.
    1
    7> T.
    [2,3]
    

    可以在函数中这么递归下去,下划线表示丢弃赋值:

    8> [_ | T2] = T.
    [2,3]
    9> T2.
    [3]
    10> [_ | T3] = T2.
    [3]
    11> T3.
    []
    

    Erlang 里面变量是 immutable 的,可以使用 f() 解绑所有变量,清理之前用过的变量名。

    下面重新定义了 Add 函数,现在它只接收一个 tuple 参数。然后在参数列表里做 pattern matching 以获取 tuple 中的两个值,解析到 A,B.

    12> f().
    ok
    13> Add = fun({A, B}) -> A + B end.
    #Fun<erl_eval.6.118419387>
    14> Add({1, 2}).   
    3
    

    Erlang 里到处都用匹配的,下面的代码里,定义了一个 greet/2 函数:

    -module(case_matching).
    -export([greet/2]).
    
    greet(Gender, Name) ->
      case Gender of
        male ->
          io:format("Hello, Mr. ~s!~n", [Name]);
        female ->
          io:format("Hello, Mrs. ~s!~n", [Name]);
        _ ->
          io:format("Hello, ~s!~n", [Name])
      end.
    

    case 的各个分支是自上往下依次匹配的,如果 Gender 是 atom 'male', 则走第一个,如果是 'female' 走第二个,如果上面两个都没匹配上,则走第三个。

    有了匹配模式,上面的例子改一下,会更规整一点:

    -module(function_matching).
    -export([greet/2]).
    
    greet(male, Name) ->
      io:format("Hello, Mr. ~s!~n", [Name]);
    greet(female, Name) ->
      io:format("Hello, Mrs. ~s!~n", [Name]);
    greet(_, Name) ->
      io:format("Hello, ~s!~n", [Name]).
    

    这个模块使用函数匹配模式,有三个 clause,与 case 一样,自上往下依次匹配。

    $ erl -pa ebin/
    Eshell V10.4  (abort with ^G)
    1> function_matching:greet(female, "Scarlett").
    Hello, Mrs. Scarlett!
    ok
    2>
    

    erl -pa 参数的意思是 Path Add, 添加目录到 erlang 以查找目录列表里的 beam 文件。

    bitstring & binary 位串与二进制

    比特字符串 bit string 保存在无类型定义的内存 untyped memory。

    位串包含一系列比特位,当元素都是 8-bit 一个字节分组就是二进制数据。

    位串表达式的基本格式:

    <<>>
    <<E1,...,En>>
    

    每个元素 Ei 指定了一段位串值,大小和类型是可选的:

    Ei = Value |
         Value:Size |
         Value/TypeSpecifierList |
         Value:Size/TypeSpecifierList
    

    TypeSpecifierList 类型列表是以下三种组合,使用连字符拼接,如 <<D/integer-signed>> = <<80>>.

    • Type 设置类型 integer, float, binary, bytes, bitstring, bits, utf8, utf16, utf32
    • Signedness 为 integer 设置符号 signed, unsigned 默认值
    • Endianness 字节序,big 默认、little、 native

    Examples:

    1> <<10,20>>.
    <<10,20>>
    2> <<"ABC">>.
    <<"ABC">>
    3> <<1:2, 2:2>>.
    <<6:4>>
    

    上面显示的 <<6:4>> 表示化位串是 4-bit,值是 6,可以根据比特位拼接得到 01 拼接 10 结果为 0110,即十进制的 6。

    Erlang 没有字符串类型,字符串通常用 List 表达,如:

    1> [97, 98, 99].
    "abc"
    

    也可以用二进位来表示字符串,更省空间:

    1> <<"ABC">>.
    <<"ABC">>
    

    使用模式匹配获取位串的值,使用内置函数 bit_size 获取大小:

    1> A = <<255, 256, 16#80>>.
    <<255,0,128>>
    2> <<B,C,D>> = A.
    <<255,0,128>>
    3> B.
    255
    4> bit_size(A).
    24
    5> E = <<B:8>>.
    <<"">>
    6> F = <<B:16>>.
    <<0,255>>
    

    注意,不能直接从位串获取指定的子位串 B = <<A:16>>.,但是可以先将位串绑定到变量再获取:

    1> A = <<1,1>>.
    <<1,1>>
    2> B = <<A:16>>.
    ** exception error: bad argument
         in function  eval_bits:eval_exp_field1/6 (eval_bits.erl, line 101)
         in call from eval_bits:eval_field/3 (eval_bits.erl, line 92)
         in call from eval_bits:expr_grp/4 (eval_bits.erl, line 68)
    3> <<B:16>> = A.
    <<1,1>>
    4> C = <<B:16>>.
    <<1,1>>
    

    内置函数 binary_to_list 可以用于将一个位字符串转换为列表。

    -module(helloworld).
    -export([start/0]).
    
    start() ->
       Bin1 = <<10,20>>,
       X = binary_to_list(Bin1),
       io:fwrite("~w",[X]).
    

    执行上面的程序,输出结果如下:

    [10,20]
    

    位逻辑操作

    bsl (Bit Shift Left),
    bsr (Bit Shift Right),
    band,
    bor,
    bxor,
    bnot.

    Type Casting 类型转换

    除了 tuple_to_list 转换成 list 时都会尽力转成字符串形式

    atom_to_list(hello).
    "hello"
    binary_to_list(<<"hello">>).
    "hello"
    binary_to_list(<<104,101,108,108,111>>).
    "hello"
    float_to_list(7.0).
    "7.00000000000000000000e+00"
    integer_to_list(77).
    "77"
    
    tuple_to_list({a,b,c}).
    [a,b,c]
    

    Number 转 binary 都转成了字符串

    integer_to_binary(77).
    <<"77">>
    float_to_binary(7.0).
    <<"7.00000000000000000000e+00">>
    

    其他的转换

    list_to_atom("hello").
    hello
    list_to_binary("hello").
    <<104,101,108,108,111>>
    list_to_float("7.000e+00").
    7.0
    list_to_integer("77").
    77
    list_to_tuple([a,b,c]).
    {a,b,c}
    term_to_binary({a,b,c}).
    <<131,104,3,100,0,1,97,100,0,1,98,100,0,1,99>>
    binary_to_term(<<131,104,3,100,0,1,97,100,0,1,98,100,0,1,99>>).
    {a,b,c}
    binary_to_integer(<<"77">>).
    77
    binary_to_float(<<"7.000e+00>>").
    7.0
    

    类型判断

    is_atom/1           
    is_binary/1        
    is_bitstring/1      
    is_boolean/1        
    is_builtin/3       
    is_float/1          
    is_function/1       is_function/2      
    is_integer/1        
    is_list/1           
    is_number/1        
    is_pid/1            
    is_port/1           
    is_record/2         is_record/3         
    is_reference/1      
    is_tuple/1
    

    Boolean 布尔比较

    Erlang 没有专用的 Boolean 类型,使用 atom 类型的 true 和 false 两个值,作为布尔处理。

    1> true and false.
    false
    2> false or true.
    true
    3> true xor false.
    true
    4> not false.
    true
    5> not (true and true).
    false
    

    还有两个与 and 和 or 类似的操作:andalsoorelse。区别是 and 和 or 不论左边的运算结果是真还是假,都会执行右边的操作。而 andalso 和 orelse 是短路的,意味着右边的运算不一定会执行。

    来看一下比较:

    6> 5 =:= 5.
    true
    7> 1 =:= 0.
    false
    8> 1 =/= 0.
    true
    9> 5 =:= 5.0.
    false
    10> 5 == 5.0.
    true
    11> 5 /= 5.0.
    false
    

    =:==/= 分别是严格相等运算符和严格不等运算符,/=== 分别是相差很多,大概相等。

    12> 1 < 2.
    true
    13> 1 < 1.
    false
    14> 1 >= 1.
    true
    15> 1 =< 1.
    true
    17> 0 == false.
    false
    18> 1 < false.  
    true
    

    数字和 atom 类型是不相等的, 0 /= false。注意,小于等于的写法 =<,= 在前面,=> 和 <= 两个箭头还有其他的用处。

    虽然不同的类型之间可以比较,也有个对应的顺序,但一般情况用不到的:

    number < atom < reference < fun < port < pid < tuple < list < bit string
    

    Tuples 元组

    Tuple 类型是多个不同类型的值组合成的类型。有点类似于 C 语言里的 struct。

    语法是:{Element1, Element2, ..., ElementN}

    1> X = 10, Y = 4.
    4
    2> Point = {X,Y}.
    {10,4}
    

    上面的 Point 是个 Tuple 类型,包含了两个整形的变量 X 和 Y。

    实践中,经常在 tuple 的第一个值放一个 atom 类型,来标注这个 tuple 的含义。这种叫做 tagged tuple:

    1> Data1 = {point, 1, 2}.
    {point,1,2}
    2> Data2 = {rectangle, 20, 30}.
    {rectangle,20,30}
    

    后面的代码如果要处理 Data1 和 Data2 的话,只需要检查 tuple 的第一项,就知道这个 tuple 是个点坐标,还是个矩形:

    3> case Data1 of
    3>   {point, X, Y} -> "this is a point";
    3>   {rectangle, Length, Width} -> "this is a rectangle"
    3> end.
    "this is a point"
    

    上面用 case 做 pattern matching 模式匹配。

    Map 映射

    映射是复合数据类型,存放各种键值对,一个主键 Key 对应一个值,存放键值对也中元素 Element,其数量就是映射的大小:

    #{Key1=>Value1,...,KeyN=>ValueN}
    

    使用 STDLIB 提供的内置函数 BIFs 操作映射:

    1> M1 = #{name=>adam,age=>24,date=>{july,29}}.
    #{age => 24,date => {july,29},name => adam}
    2> maps:get(name,M1).
    adam
    3> maps:get(date,M1).
    {july,29}
    4> M2 = maps:update(age,25,M1).
    #{age => 25,date => {july,29},name => adam}
    5> map_size(M).
    3
    6> map_size(#{}).
    0
    

    Creating Maps

    #{}
    #{ K => V }
    #{ K1 => V1, .., Kn => Vn }
    

    Examples:

    M0 = #{},                 % empty map
    M1 = #{a => <<"hello">>}, % single association with literals
    M2 = #{1 => 2, b => b},   % multiple associations with literals
    M3 = #{k => {A,B}},       % single association with variables
    M4 = #{{"w", 1} => f()}.  % compound key associated with an evaluated expression
    

    这里的 A 和 B 可以是任何表达式。

    旧的主键值会被新的替换:

    1> #{1 => a, 1 => b}.
    #{1 => b }
    2> #{1.0 => a, 1 => b}.
    #{1 => b, 1.0 => a}
    

    Updating Maps

    M#{ K => V }
    

    更新已经存在的键值,如果不存在 K 主键就触发异常,返回一个新的映射:

    M#{ K := V } 
    

    Examples:

    M0 = #{},
    M1 = M0#{a => 0},
    M2 = M1#{a => 1, b => 2},
    M3 = M2#{"function" => fun() -> f() end},
    M4 = M3#{a := 2, b := 3}.  % 'a' and 'b' was added in `M1` and `M2`.
    

    More examples:

    1> M = #{1 => a}.
    #{1 => a }
    2> M#{1.0 => b}.
    #{1 => a, 1.0 => b}.
    3> M#{1 := b}.
    #{1 => b}
    4> M#{1.0 := b}.
    ** exception error: bad argument
    

    Maps in Patterns

    #{ K := V } = M
    

    映射 M 中的 K 必须是 guard expression,并绑定了变量。如果 V 是没有绑定的值,就会绑定到 K,如果 V 是绑定的值,必需和映射 M 中的主键 K 的值匹配。

    Example:

    1> M = #{"tuple" => {1,2}}.
    #{"tuple" => {1,2}}
    2> #{"tuple" := {1,B}} = M.
    #{"tuple" => {1,2}}
    3> B.
    2.
    

    相似地,多值模式匹配:

    #{ K1 := V1, .., Kn := Vn } = M
    

    主键 K1 .. Kn 是字面表达式或是绑定的变量,如果,所有主键在 M 中存在都匹配,那么 V1 .. Vn 匹配到相应主键的对应值。

    模式匹配满足以下任一条件即为失败:

    • A badmatch exception.
    • Or resulting in the next clause being tested in function heads and case expressions.

    映射的模式匹配只可用 := 分隔符号,顺序是不重要的,重复的主键也是可以的,空映射也可以匹配,只要以下的 Expr 是映射类型:

    #{ K := V1, K := V2 } = M
    #{} = Expr
    

    用表达式作为主键,要求 List 已经绑定变量:

    #{{tag,length(List)} := V} = Map
    

    Matching Syntax

    %% only start if not_started
    handle_call(start, From, #{ state := not_started } = S) ->
    ...
        {reply, ok, S#{ state := start }};
    
    %% only change if started
    handle_call(change, From, #{ state := start } = S) ->
    ...
        {reply, ok, S#{ state := changed }};
    

    List 列表

    List 就是我们经常说的链表,数据结构里学的那个。但 List 类型在 Erlang 里使用极其频繁,因为用起来很方便。

    List 可以包含各种类型的值:

    1> [1, 2, 3, {numbers,[4,5,6]}, 5.34, atom].
    [1,2,3,{numbers,[4,5,6]},5.34,atom]
    

    上面这个 list 包含了 3 个数值类型和一个 tuple,一个浮点数,一个 atom 类型。

    来看看这个:

    2> [97, 98, 99].
    "abc"
    

    卧槽这什么意思?!因为 Erlang 的 String 类型其实就是 List!所以 erlang shell 自动给你显示出来了。就是说如果你这么写 "abc" 等效 [97, 98, 99]。

    注意,链表存储空间比纯字符串数组大,拼接等操作也费时,所以一般使用字符串的时候,用 Erlang 的 Binary 类型,这样写:<<"abc">> 内存消耗就小很多了。

    开始你可能不大明白 tuple 跟 list 的区别,这样吧:

    • 当你知道你的数据结构有多少项的时候,用 Tuple;
    • 当你需要动态长度的数据结构时,用 List。

    List 处理:

    5> [1,2,3] ++ [4,5].
    [1,2,3,4,5]
    6> [1,2,3,4,5] -- [1,2,3].
    [4,5]
    7> [2,4,2] -- [2,4].
    [2]
    8> [2,4,2] -- [2,4,2].
    []
    9> [] -- [1, 3].
    []
    11> hd([1,2,3,4]).  
    1
    12> tl([1,2,3,4]).
    [2,3,4]
    
    • ++ 运算符是往左边的那个 List 尾部追加右边的 List。
    • -- 是移除操作符,如果左边的 List 里不包含需要移除的值,也没事儿。不要拿这种东西来做面试题,这样会没朋友的。
    • hd/1 函数是获取 Head。
    • tl/1 函数是获取 Tail,和 hd/1 都是 Erlang 内置函数 BIF - Built-In-Function。

    链表嘛你知道的,往链表尾部追加,需要先遍历这个链表,找到链表的尾部。 所以 "abc" ++ "de" 这种的操作的复杂度,取决于前面 "abc" 的长度。

    第一行里你也看到了,List 的追加操作会有性能损耗,lists:append/2 跟 ++ 是一回事儿,所以我们需要一个从头部插入 List 的操作:

    13> List = [2,3,4].
    [2,3,4]
    14> NewList = [1|List].
    [1,2,3,4]
    15> [1, 2 | [0]].
    [1,2,0]
    16> [1, 2 | 0].
    [1,2|0]
    

    注意这个 | 的左边应该放元素,右边应该放 List。左边元素有好几个的话,erlang 会帮你一个一个的插到头部。如果右边放的不是 List,像 [1, 2 | 0] 这种叫不适 improper list。虽然你可以生成这种列表,但不要这么做,代码里出现这种一般就是个 bug,忘了这种用法吧。

    List 可以分解为 [ 第一个元素 | 剩下的 List ],仔细看一下这几行体会一下:

    20> [1 | []].
    [1]
    21> [2 | [1 | []]].
    [2,1]
    22> [3 | [2 | [1 | []] ] ].
    [3,2,1]
    

    实践中我们经常会从一个 List 取出需要的那些元素,然后做处理,最后再将处理过的元素重新构造成一个新的元素。

    你马上就想到了 map,reduce。在 Erlang 里,我们可以用列表推理 List Comprehensions 语法,很方便的做一些简单的处理。

    下例,取出 [1,2,3,4] 每个元素,然后乘 2,返回值再组成一个新的 List,后面再取出列表里所有偶数。

    1> [2*N || N <- [1,2,3,4]].
    [2,4,6,8]
    2> [X || X <- [1,2,3,4,5,6,7,8,9,10], X rem 2 =:= 0].
    [2,4,6,8,10]
    

    Atoms 原子类型

    Erlang 里面有 atom 原子类型,它使用的内存很小,所以常用来做函数的参数和返回值。参加 pattern matching 的时候,运算也非常快速。

    在其他没有 atom 的语言里,你可能用过 constant 之类的东西,一个常量需要对应一个数字值或者其他类型的值。

    在 Erlang 里 atom 真是抬头不见低头见,可以通过 atom 来表示各种意义的常量。在其他语言,例如 C/C++ 中使用 #define 宏定义,enum 枚举,或者用 const 常量等方法实现类似的功能。

    但是,使用这些方法的时候,总会觉得不是太舒服,比如使用 #define 宏定义和 const 常量,除了本来就头痛的给宏或常量命名之外,还要真正填上一个值,为了让这些值不冲突,又是一件头痛的事情了。如果用字符串吧,那么每次匹配的时候还要做低效的字符串操作。

    比如:

    const int red = 1;
    const int green = 2;
    const int blue = 3;
    

    但多了这个映射,其实用起来不大方便,后面对应的值 1, 2,3 一般只是用来比较,具体是什么值都关系不大。所以有了 atom 就很方便了,我们从字面上就能看出,这个值是干嘛的:

    1> red.
    red
    

    atom 类型支持的写法:

    1> atom.
    atom
    2> atoms_rule.
    atoms_rule
    3> atoms_rule@erlang.
    atoms_rule@erlang
    4> 'Atoms can be cheated!'.
    'Atoms can be cheated!'
    5> atom = 'atom'.
    atom
    

    包含空格等特殊字符的 atom 需要用单引号括起来。 Erlang 里变量的命名必须首字母大写,小写起头是 atom 原子类型。

    需要注意的是:在一个 erlang vm 里,可创建的 atom 的数量是有限制的,默认是 1,048,576,因为 erlang 虚拟机创建 atom 表也是需要内存的。一旦创建了某个 atom,它就一直存在那里了,不会被垃圾回收。不要在代码里动态的做 string -> atom 的类型转换,这样最终会使你的 erlang atom 爆表。比如在你的接口逻辑处理的部分做 to atom 的转换的话,别人只需要用不一样的参数不停地调用你的接口,就可以攻击你。

    Guards 保护序列

    在函数定义中,可以使用 when 加入保持序列。

    假设,learn-you-some-erlang 的作者那边 16 岁才能"开车" (笑). 那我们写个函数判断一下,某个人能不能开车?

    old_enough(0) -> false;
    old_enough(1) -> false;
    old_enough(2) -> false;
    ...
    old_enough(14) -> false;
    old_enough(15) -> false;
    old_enough(_) -> true.
    

    上面这个又点太繁琐了,所以我们得另想办法:

    old_enough(X) when X >= 16 -> true;
    old_enough(_) -> false.
    

    然后作者又说了,超过 104 岁的人,禁止开车:

    right_age(X) when X >= 16, X =< 104 ->
       true;
    right_age(_) ->
       false.
    

    注意 when 语句里,, 逗号表示 and, ; 分号表示 or, 如果你想用短路运算符的话,用 andalso 和 orelse, 这么写:

    right_age(X) when X >= 16 andalso X =< 104 -> true;
    

    Records 记录体

    前面讲过 tagged tuple,但它用起来还不够方便,因为没有个名字,也不好访问其中的变量。

    -record(Name,{
            key1 = Default1,
            key2 = Default2,
            key3, %% 默认值是 undefined
            ...
            }).
    

    Erlang 的 record 类型可以提个名字访问:

    -module(records).
    -export([get_user_name/1,
             get_user_phone/1]).
    
    -record(user, {
      name,
      phone
    }).
    
    get_user_name(#user{name=Name}) ->
      Name.
    
    get_user_phone(#user{phone=Phone}) ->
      Phone.
    

    编译测试:

    $ erl
    Eshell V8.3  (abort with ^G)
    1> c(records).
    {ok,records}
    2> rr(records).
    [user]
    4> Shawn = #user{name = <<"Shawn">>, phone = <<"18253232321">>}.
    #user{name = <<"Shawn">>,phone = <<"18253232321">>}
    5> records:get_user_phone(Shawn).
    <<"18253232321">>
    6> records:get_user_name(Shawn).
    <<"Shawn">>
    
    7> records:get_user_name({user, <<"Shawn">>, <<"18253232321">>}).
    <<"Shawn">>
    
    9> Shawn#user.name.
    <<"Shawn">>
    10> #user.name.
    2
    

    程序解释:

    • 其实 #user{} 相当 {user, name, phone},是第一个元素为 user 的 tagged tuple。
    • #user.name 是这个 tuple 里 name 字段的位置号 2。
    • Record 字段的位置 Index 等都是约定从 1 开始的。
    • Shawn#user.name 的意思是取 Shawn 里的第 2 个元素。

    定义记录 record 可以包含在 .erl 源代码或在 .hrl 文件中:

    -record(todo,{status=reminder,who=joe,text}).
    

    在 Erlang shell 创建 record 实例,必须先读取记录的定义,使用命令 rr(read records):

    1>rr("records.hrl").
    

    注意: rr 方法支持通配符,比如 rr("*"),使用 rf(record free) 函数释放掉记录的定义。

    有个内置函数 is_record(Term, RecordTag) 判断记录类型:

    is_person(P) when is_record(P, person) ->
        true;
    is_person(_P) ->
        false.
    
    foo(P) when is_record(P, person) -> a_person;
    foo(_) -> not_a_person.
    

    Record 是一个编译时的功能,在 Erlang VM 中并没有专门的数据类型,在线上解决问题有时候会遇到要在 shell 中使用 record。在 shell 中使用 rd 命令构造 record 定义,构造 record 定义编写 ets:match 的匹配模式就方便多了。另一种方法是直接使用 record 对应的 tuple 结构。

    Eshell V5.9 (abort with ^G)
    1> rd(film ,{ director, actor, type, name,imdb}).
    film
    2> F =#film{}.
    #film{director = undefined,actor = undefined,
    type = undefined,name = undefined,imdb = undefined}
    3> F#film.type.
    undefined
    4> F#film.type=23.
    * 1: illegal pattern
    5> F2 =F#film{type=23}.
    #film{director = undefined,actor = undefined,type = 23,
    name = undefined,imdb = undefined}
    

    Record 通过 # 符号来创建,更新 Record 和创建 Record 很类似:

    Opts = #record{name=<<"Jean">>, Phone=<<"020-12345">>},  
    NewOpts = Opts#record{name="Jim"}.  
    

    这里首先创建一个 record 绑定到 Opts 变量,然后 NewOpts 创建了一个 Opts 的副本,并指定新的名字绑定到 NewOpts。

    模式匹配,假如定义 person 记录,下面的模式匹配中会将 P3 中的名子绑定到 Name 变量。

    > rd(person, {name = "", phone = [], address}).
    person
    > P3 = #person{name="Joe", phone=[0,0,7], address="A street"}.
    #person{name = "Joe",phone = [0,0,7],address = "A street"}
    > #person{name = Name} = P3, Name.
    "Joe"
    

    Recursive 递归

    递归是 Erlang 的重要组成部分。

    以下实现阶乘程序来了解简单的递归。

    -module(helloworld). 
    -export([fac/1,start/0]). 
    
    fac(N) when N == 0 -> 1; 
    fac(N) when N > 0 -> N*fac(N-1). 
    
    start() -> 
       X = fac(4), 
       io:fwrite("~w",[X]).
    

    以递归一个更有效的方法可以用于确定一个列表的长度,现在来看看一个简单的例子。列表中有多个值,如[1,2,3,4]。

    让我们用递归的方法来看看如何能够得到一个列表的长度。

    -module(helloworld). 
    -export([len/1,start/0]). 
    
    len([]) -> 0; 
    len([_|T]) -> 1 + len(T). 
    
    start() -> 
       X = [1,2,3,4], 
       Y = len(X), 
       io:fwrite("~w",[Y]).
    

    上述程序关键点:

    • 第一个函数 len([]) 用于特殊情况的条件:如果列表为空。
    • [H|T] 模式来匹配一个或多个元素的列表,如长度为 1 的列表可以定义为 [X|[]],而长度为 2 的列表可以定义为 [X|[Y|[]]]

    注意,第二元素是列表本身。这意味着我们只需要计数第一个,函数可以调用它本身在第二元素上。在列表给定每个值的长度计数为 1 。

    有个比喻可以帮你理解尾递归递归的区别

    假设玩一个游戏,你需要去收集散落了一路,并通向远方的硬币。

    于是你一个一个的捡,一边捡一边往前走,但是你必须往地上撒些纸条做记号,因为不做记号你就忘了回来的路。于是你一路走,一路捡,一路撒纸条。等你捡到最后一个硬币时,你开始沿着记号回来了,一路走,一路捡纸条(保护环境)。等回到出发点时,你把硬币装你包里,把纸条扔进垃圾桶。
    这就是非尾递归,纸条就是你的调用栈,是内存记录。

    下次再玩这个游戏时,你学聪明了,你直接背着包过去了,一路走,一路捡,一路往包里塞。等到了终点时,最后一个硬币进包了,任务完成了,你不回来了!
    这就是尾递归,省去了调用栈的消耗。

    Loops 循环控制

    Erlang 中没有可直接使用的循环控制语句,须使用递归技术在 Erlang 中来实现 while/for 等语句。

    -module(helloworld). 
    -export([while/1,while/2, start/0]). 
    
    while(L) -> while(L,0). 
    while([], Acc) -> Acc;
    
    while([_|T], Acc) ->
       io:fwrite("~w~n",[Acc]), 
       while(T,Acc+1). 
       
       start() -> 
       X = [1,2,3,4], 
       while(X).
    

    此循环程序定义了递归函数模拟 while 循环,在主函数输入一个数值列表,列表绑定到变量 X 中。在 while 函数中,利用中间变量 Acc 保存从列表取出的值,然后递归调用 while 函数。

    -module(helloworld). 
    -export([for/2,start/0]). 
    
    for(0,_) -> 
       []; 
       for(N,Term) when N > 0 -> 
       io:fwrite("Hello~n"), 
       [Term|for(N-1,Term)]. 
       
    start() -> 
       for(5,1).
    

    上述程序实现 for 循环的关键点:

    • 定义一个递归函数来实例和执行 for 循环;
    • 使用 for 函数以确保 N 或限制的值是正值;
    • 递归地调用 for 函数,通过在每一次递归后减少 N 的值。

    Module 模块定义

    模块是在一个文件重新组合的函数集合,在 Erlang 所有函数必须在模块定义。模块的名称必须在模块代码的第一行,并且和文件名一致。

    大部分像算术,逻辑和布尔操作符的基本函数已经 Erlang 内部集成提供并且可以直接调用,因为在运行程序时的默认模块被加载。一个模块中使用定义的所有其他函数需要使用形式 Module:Function (参数) 来调用。

    下面的程序显示了一个叫 helloworld 模块的一个例子。

    -module(helloworld). 
    -author("TutorialPoint"). 
    -version("1.0"). 
    -export([start/0]). 
    -import(io,[fwrite/1]). 
    
    start() -> 
       io:fwrite("Hello World").
    

    模块定义了 author、 version 两个标签属性,可以按 -Tag(Value) 格式定义。

    模块导出函数 export 指定一个导出列表,这里只导出一个 start 函数,参数个数为 0 个。导入语句类似,它指定导入的模块和函数列表。所以,现在每当调用 fwrite 函数,不必每次都要带上模块的名称。

    导入模块和函数 -import(io,[fwrite/1]). 格式类似导出,它需要指定导入的模块。Erlang 没有全部导入的方式,但是可以在运行 erl -pa .\ebin 指定编译后的程序目录,这样 Erlang 会自动查找引用到的函数。

    然后你用 erlc 编译

    mkdir -p ./ebin
    erlc -o ebin helloworld.erl
    

    编译后的 beam 文件会在 ebin 目录下,然后你启动 erlang shell:

    $ erl -pa ./ebin
    
    Eshell V8.3  (abort with ^G)
    1> helloworld:start().
    3
    2> helloworld:start().
    4
    

    erl -pa 参数的意思是 Path Add, 添加 beam 文件目录到 erlang 以自动查找编译好的程序。就是说,你运行 helloworld:start(). 的时候,Erlang 发现 module 'helloworld' 没加载,就在那些查找目录里找 helloworld.beam,然后加载进来。

    Error 错误处理

    常见错误码意义:

    • eacces Missing search or write permissions for the parent directories of Dir.
    • eexist 目录不是空目录;
    • enoent 目录不存在;
    • enotdir 不是目录,一些系统会返回 enoent;
    • einval 试图删除当前目录,一些系统会返回 eacces;
    • badarg 参数错误;
    • badarith 运算错误,atithmetic 运算,例如将一个整数和一个 atom 相加。
    • {badmatch, V} 模式匹配错误
    • function_clause 该错误信息表示找不到匹配的函数。例如,不到匹配的分支,会抛出 function_clause。
    • {case_clause, V} case 表达式找不到匹配的分支。一般要把 _ 加到最后的分支中,作为容错或者其它。
    • if_clause if 表达式是 case 表达式的一种特殊方式,要求至少有一个分支测试条件的结果为 true,否则会引发错误。
    • undef 调用未定义的函数或者模块时,返回该错误信息。
    • noproc 进程不存在,例如 gen_server:call 一个不存在的进程。
    • system_limit 超出系统上限,如 atom,ets,port,process 等。

    异常处理
    在开发中可使用try,catch捕获异常,同时也调用erlang:get_stacktrace(),获取栈信息,定位错误。

    try:
        %% 业务代码
        Exprs
    catch
        Calss:Reason ->
        %% 异常处理代码,
        %% Calss为异常类型,Reason为异常原因
        ok
    end.
    

    一个简单的例子:

    -module(test).
    -export([add/2]).
    
    add(A,B) ->
        try
            A + B
        catch
            Class:Reason ->
                io:format("Class:~p,Reason:~p~nstacktrace:~n~p",
                          [Class,Reason,erlang:get_stacktrace()]),
                error
        end.
    

    查询当前日志记录等级信息:

    >erl -kernel logger_level info -s init stop
    =PROGRESS REPORT==== 15-Jun-2020::17:03:16.209000 ===
        application: kernel
        started_at: nonode@nohost
    =PROGRESS REPORT==== 15-Jun-2020::17:03:16.222000 ===
        application: stdlib
        started_at: nonode@nohost
    Eshell V10.4  (abort with ^G)
    1>
    

    相关文章

      网友评论

          本文标题:Erlang/OTP OCP 面向并发编程入门

          本文链接:https://www.haomeiwen.com/subject/qlgetktx.html