Erlang也可以这么学!

Posted 异步图书

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Erlang也可以这么学!相关的知识,希望对你有一定的参考价值。

Erlang是什么

Erlang是一门函数式编程语言。如果你曾经用过命令式语言,那么像i++这样的语句对你来说再普通不过了,但是在函数式编程中,却不能这样使用。事实上,改变任何变量的值都是绝对不允许的。乍一听这似乎很奇怪,但是想想上过的数学课,你学到的内容是这样的:

y = 2x = y + 3x = 2 + 3x = 5

如果我把以下内容加进去,你一定会觉得困惑。

x = 5 + 1x = x 5 = 6

函数式编程认识到了这一点。如果我说x5,从逻辑上,我就不能说它也等于6!这属于欺诈。这也是为什么每次用同样的参数去调用函数时,它都应该返回相同的值:

x = add_two_to(3) = 5 x = 5

对于同样的参数,函数永远要返回同样的值,这个概念称为引用透明性(referential transparency)。正是因为这一点,才能够把add_two_to(3)替换成 5, 因为3+2的结果就是5。这意味着,为了解决更为复杂的问题,我们可以将很多函数粘合在一起,还能保证不破坏任何逻辑。既合乎逻辑,又整洁,不是吗?不过还有一个问题:

x = today() = 2013/10/22    -- 等待一天后 -- x = today() = 2013/10/23x = x 2013/10/22 = 2013/10/23

哦不!我美丽的等式!它们突然间全部出错了!为什么我的函数每天返回的值都不同呢?

显然,在某些情况下,不遵循引用透明性是有用的。Erlang在函数式编程方面采用了一种非常注重实效的策略:遵守最纯粹的函数式编程原则(引用透明性、避免可变数据等),但是在遇到现实问题时,就打破这些原则。

Erlang是一门函数式编程语言,它同时也非常重视并发和高可靠性。为了让几十个任务能同时执行,Erlang采用了actor模型,每个actor都是虚拟机中的一个独立进程。简而言之,如果你是Erlang世界中的一个actor,你将会是一个孤独的人,独自坐在一个没有窗户的黑屋子里,在你的邮箱旁等待着消息。当你收到一条消息时,会用特定的方式来响应这条消息:收到账单就要进行支付;收到生日卡,就回一封感谢信;对于不理解的消息,就完全忽略。

可以把Erlang的actor模型想象成这样一个世界,其中每个人都独自坐在屋子里,可以执行一些不同的任务。人和人之间只能通过写信进行交流,就是这样。这样的生活虽然听起来很乏味(但却是邮政服务的新时代),但这意味着,你可以要求很多人为你分担不同的任务,他们做错了事或者犯了错误绝对不会对其他人的工作造成任何影响。除了你之外,他们甚至不知道还有其他人的存在(这真是太棒了)。

事实上,在Erlang中,只能创建出相互之间完全没有共享、只能通过消息通信的actor(进程)。每次消息交互都是显式的、可追踪的和安全的。

Erlang不仅仅是一门语言,同时也是一个完整的开发环境。代码被编译成字节码,字节码运行在虚拟机中。所以,Erlang很像Java,也像患有多动症的孩子,在任何地方都能运行。下面是Erlang标准发布中的一些组件:

  • 开发工具(编译器,调试器,性能分析器以及测试框架和可选的类型分析器);

  • 开放电信平台(OTP)框架;

  • Web服务器;

  • 高级跟踪工具;

  • Mnesia数据库(一个键/值存储系统,能够在多台服务器上复制数据,支持嵌套事务,并且可以存储任何类型的Erlang数据)。

Erlang虚拟机和库还能让用户在不中断任何程序的情况下升级运行系统的代码,能轻易地将代码分布在多台计算机上,还能用一种简单但强大的方式去管理错误和故障。

Erlang也可以这么学!

在本书中,我们会对其中绝大部分工具的使用方法进行介绍,也会讲解如何实现安全的系统。

谈到安全性,你应该知道Erlang中与之相关的一个总方针——任其崩溃(let it crash),这说的可不是一架崩溃后会导致大量乘客死亡的飞机,它指的更像是在下方铺有安全网络的钢丝上的行走者。尽管应该避免犯错,但是也不用时刻去检查每一种可能的错误情况。

Erlang提供了从错误中恢复的能力,用actor来组织代码的能力,以及用分布式和并发进行伸缩的能力,这些听起来都棒极了……

使用Erlang语言,可以在仿真器(emulator)中对绝大多数代码进行测试。在代码脚本被编译和部署后,不仅可以在仿真器中运行它们,还可以现场对它们进行修改。

下面我们会介绍Erlang shell的使用方式以及Erlang的基础知识。

1 使用Erlang shell

要在Linux或者Mac OS X系统上启动Erlang shell,首先需要打开一个终端,输入erl。如果之前已经正确安装了Erlang,就会看到如下显示:

$ erlErlang R15B (erts-5.9) [source] [64-bit] [smp:4:4] [async-threads:0] [hipe][kernel-poll:false]Eshell V5.9 (abort with ^G)

祝贺,你成功运行了Erlang shell!

如果你是Windows用户,那么可以在命令行提示符中执行erl.exe启动Erlang shell,但是一般建议使用werl.exe,它存在于开始菜单中(选择All Programs → Erlang)。werl是一个专门用于Windows系统的Erlang shell,它有自己的窗口,窗口具有滚动条,还支持一些行编辑快捷键(Windows系统中的标准cmd.exe shell不支持这些快捷键)。然而,如果想重定向标准输入或者输出,又或者想使用管道,那么就只能使用erl.exe来启动shell。

现在,我们可以在shell中输入代码,并在仿真器中运行它们。但是,我们先要熟悉一下Erlang shell。

1.1 输入shell命令

Erlang shell中内置了一个基于Emacs的功能子集构建的行编辑器,Emacs是一款流行的文本编辑器,从20世纪70年代使用至今。如果你熟悉Emacs,那就再好不过了。即便你对Emacs一无所知,也无妨。

首先,在Erlang shell中输入一些文本,然后,按下Ctrl+A(^A)。光标会移至该行的开头。同样地,按下Ctrl+E(^E),光标会移至该行的末尾。你还可以使用左右箭头键让光标前后移动,用上下箭头键在之前写好的代码行之间来回切换。

Erlang也可以这么学!

我们来试一些其他输入。输入li,然后按下Tab键。shell会自动把刚才输入的li自动补全为lists:。再次按下Tab键,shell中会列举出lists模块下所有可用的函数信息。你可能会觉得这种表示法比较奇怪,不过放心,很快你就会熟悉了(第2章会对模块进行更多的介绍)。

1.2 退出shell

至此,Elang shell的大部分基本功能特性都已经介绍过了,除了一件非常重要的事情:我们还不知道如何退出shell呢!还好,有一种快速的寻求帮助的方法:在shell中输入help().并按下Enter键。你会看到一系列命令的相关信息,包括进程查看函数、shell工作方式控制函数等。其中大部分的命令都会在本书中用到,不过现在,我们最感兴趣的是下面这行描述:

q() -- quit - shorthand for init:stop()

嗯,这是退出shell的一种方法(实际上,一共有两种)。但是,当shell被冻结住时,是无法输入这条命令的!

如果刚才启动shell时,你留意观察过,可能会看到一句“aborting with ^G.”字样的注解。那就按下Ctrl+G,然后再输入h寻求帮助。

User switch command--> hc [nn]              - connect to job i [nn]              - interrupt job k [nn]              - kill job j                   - list all jobs s [shell]          - start local shell r [node [shell]]   - start remote shell q                    - quit erlang? | h               - this message-->

如果你还带着单片眼镜,是时候扔掉它了!和其他语言中的简单shell完全不同,Erlang shell是一组shell实例,每个实例运行着不同的作业(job)。此外,你可以像管理操作系统中的进程一样管理这些shell实例。如果输入k <em>N,N是作业编号,就会终止掉承担这个作业的shell以及它运行的所有代码。如果不想杀死shell,只想停止shell中运行的代码,可以使用i N命令。你还可以输入s创建一个新的shell实例,用j列举出所有的shell实例,以及用c <em>N来连上一个shell实例。

尝试这些命令时,你可能会看到某个shell作业旁有个星号(*):

--> j1* {shell,start,[init]}

*表明这个shell是你最近一次使用的shell实例。如果在使用命令c、i或者k时,后面不带任何数字,那么命令会默认作用到这个最近使用的shell实例上。

如果shell冻结住了,一个快捷的解冻指令序列是:按下Ctrl+G、输入i、按下Enter键、输入c、按下Enter键(^G i ENTER c ENTER)。这个指令序列会先进入shell管理界面,中断当前shell作业,然后再重新建立连接。

Eshell V5.9 (abort with ^G)1> "OH NO THIS SHELL IS UNRESPONSIVE!!! *hits ctrl+G*"User switch command  --> i  --> c** exception exit: killed1> "YESS!"

在向shell中输入有“实际意义”的内容之前,有件重要的事情要说明一下:表达式序列必须要以点号结尾,后面是空白符(换行符、空格之类),否则,表达式不会被执行。你可以用逗号来分隔表达式,但是只会显示最后一个表达式的执行结果(其他表达式也都执行了)。大多数人都会觉得这个语法很奇怪,这是因为Erlang最初是直接在Prolog中实现的。Prolog是一门逻辑编程语言。

现在,我们来做一些实践。就从学习Erlang的基本数据类型以及如何在shell中写一点Erlang程序开始吧。

2 Erlang基础知识

虽然你刚刚看到的管理不同作业和shell会话的机制非常高级,但是Erlang仍然被认为是一门相对小巧、简单的语言(这里简单的含义与C比C++简单中的含义相同)。Erlang语言中只有少量内置的数据类型(相关的语法也不多)。我们先来看看数值类型。

2.1 数值类型

按照前面讲过的方法,打开Erlang shell,并输入如下内容:

1> 2 + 15.172> 49 * 100.49003> 1892 - 1472.4204> 5 / 2.2.5

Erlang也可以这么学!

可以看到,Erlang并不关心输入的是浮点数还是整数。算术运算对两种类型都支持。

注意,如果你希望整数除法演算的结果还是整数,而不是浮点数,必须要使用div。要获取整数除法的余数(模),可以使用rem(remainder,余数)。

5> 5 div 2.26> 5 rem 2.1

在一个表达式中,可以使用多个操作符,算术计算遵循标准的优先级规则:

7> (50 * 100)  4999.18> -(50 * 100  4999).-19> -50 * (100  4999).244950

如果想以其他不为10的基数表示整数,只需以Base#Value这种形式输入数值即可(Base必须在236之间),像这样:

10> 2#101010.4211> 8#0677.44712> 16#AE.174

在上面的例子中,我们把二进制数、八进制数、十六进制数转换成十进制数。太棒了!虽然语法有些奇怪,但是Erlang的计算能力完全可以和桌面角落里摆放的计算器媲美。真令人兴奋!

2.2 不变的变量

能做算术运算固然不错,但是如果不能保存计算结果的话,也没多大用处。你可以使用变量来存放结果。如果读过本书的引言部分,应该知道在函数式编程中,变量是不会变化的。

在Erlang中,变量必须以大写字符开始。下面的6个表达式展示了变量的基本用法:

1> One.* 1: variable 'One' is unbound2> One = 1.13> Un = Uno = One = 1.14> Two = One + One.25> Two = 2.26> Two = Two + 1.** exception error: no match of right hand side value 3

从这些命令的执行中,我们首先看出的是,一个变量只能被赋值一次。之后,可以“假装”给一个变量再赋一次值,只要这个值和变量已有的值完全一样。如果这两个值不同,Erlang会报错。现象是这样,但是解释要略微复杂一些,和=操作符有关。负责比较操作并且在不相等时报错的是=操作符(不是变量)。如果相同,Erlang会返回这个值:

7> 47 = 45 + 2.478> 47 = 45 + 3.** exception error: no match of right hand side value 48

如果等号两边都是变量,并且左边是个未绑定的变量(没有任何值和它关联),Erlang会自动把右边变量的值绑定到左边的变量上。这样,两个变量就有了相同的值。因此,两边的值比较,肯定相等,并且左边的变量会一直把这个值保存在内存中。

下面是另外一个例子:

9> two = 2.** exception error: no match of right hand side value 2

这个命令失败了,因为单词two是以小写字符起始的。

注意

  严格来说,变量名还可以以下划线(_)起始,但是依照惯例,这种变量仅用于不想关心其值的情况。

=操作符的这种行为表现是模式匹配(pattern matching)的基础,许多函数式编程语言中都有这个特性,只是通常认为,Erlang中的模式匹配要比其他语言中的更灵活、更完备。在本章的后面介绍其他数据类型时,我会更详细地介绍模式匹配。在后续的几章中,也会看到如何在函数中使用模式匹配。

注意,在shell中做测试时,如果不小心给某个变量赋错了值,可以使用f(Variable).函数把这个变量“删除”。如果想清除所有的变量,可以使用f().。这两个函数是专门用来辅助代码实验的,只能在shell中使用。当编写真实程序时,是不能用这种方法来销毁值的。如果你考虑到Erlang要适用于工业场景,就会觉得这个限制是合理的。一个shell极有可能不间断地运行很多年,在这期间某个给定变量名肯定会被重复使用。

2.3 原子

变量名不能以小写字符开头有一个原因:原子(atom)。原子是字面量,这意味着原子是常量,唯一的值就是自己的名字。换句话说,你看到的就是你能得到的——别想得到更多。原子cat代表“cat”,就这样。你不能操作它、不能改变它、也不能把它分成几部分。它就是cat。只能接受它。

以小写字符开头的单词只是原子的一种写法,还有其他写法:

1> atom.atom2> atoms_rule.atoms_rule3> atoms_rule@erlang.atoms_rule@erlang4> 'Atoms can be cheated!'.'Atoms can be cheated!'5> atom = 'atom'.atom

Erlang也可以这么学!

如果原子不以小写字符开头或者其中包含有除字母、数字、下划线以及@符号之外的其他字符,那么必须被放到两个单引号(')之间。第5行表明,原子加上单引号后和原来的原子相等。

我把原子比作名字,就是其值的常量。你以前编写过的代码中可能用过常量。例如,假设你用一些值来表示眼睛的颜色:1代表蓝色(blue),2代表棕色(brown),3代表绿色(green),4代表其他(other)。这些常量的名字必须要和底层的值进行匹配。但是,原子能够让你无须考虑那些底层的值。眼睛的颜色就是蓝色、棕色、绿色和其他。这些颜色可以用在代码的任何地方。底层的值绝对不会冲突,这种常量也绝不会出现没有定义的情况!(在第2章中,我们会介绍具有关联值的常量)。

因此,原子主要用来表达或者修饰和其放置在一起的数据,通常是在一个元组(tuple)(在1.2.5节进行介绍)中。有时(并不常见),单独使用原子也是有意义的。这也是为什么我们没有在此花过多篇幅介绍它的原因。在后面的例子中,会看到原子和其他类型数据的联合使用。

保持冷静

  在消息发送和常量表示方面,原子确实是个不错的选择。然而,在其他许多方面,使用原子会带来一些问题。每个原子都会被引用到一个原子表中,原子表会消耗内存(在32位系统中,每个原子需要4字节;在64位系统中,每个原子需要8字节)。原子表不被垃圾回收,所以原子会一直累积,直到系统因为内存耗尽或者创建的原子个数超过了1 048 577而发生崩溃。

这意味着不能动态创建原子。如果系统需要可靠保证,但是用户输入部分动态创建了原子,那你就遇上大麻烦了,因为只要让其不停地创建原子就可以随意让系统崩溃。

原子应该被当作开发者使用的工具,坦白说,它本来就是用作此目的的。再重申一遍:日常编码中,只要原子都是你亲手输入的,那么完全可以安全地使用它们。只有动态生成原子才会存在风险。

注意

  有些原子是保留字,这些原子不能使用,因为Erlang语言的设计者已经在一些特定的函数名、操作符、表达式等中使用了它们。这些保留字是afterandandalsobandbeginbnotborbslbsrbxorcasecatchconddivendfunifletnotofororelsequeryreceiveremtrywhenxor

2.4 布尔代数和比较操作符

如果无法区分出大小或者真假,那就有大麻烦了。和其他语言一样,Erlang也有布尔操作和比较操作。

布尔代数非常简单:

1> true and false.false2> false or true.true3> true xor false.true4> not false.true5> not (true and true).false

Erlang也可以这么学!

注意 

布尔操作符andor对操作符两边的参数都会去求值。如果你想要的是一个短路操作符(只在有必要时,才去求值右边的参数),可以使用andalsoorelse

相等性测试和不等性测试也很简单,只不过使用的符号和其他语言中稍微有些不同:

6> 5 =:= 5.true7> 1 =:= 0.false8> 1 =/= 0.true9> 5 =:= 5.0.false10> 5 == 5.0.true11> 5 /= 5.0.false

在其他常见的语言中,通常使用==!=来做相等性和不等性比较,但在Erlang中,使用的是=:==/=。最后3个表达式(第9~11行)展示了一个陷阱:在做算术计算时,Erlang并不区分浮点数和整数,但是在做比较时,却会区分。不过别担心,如果你不想区分,可以使用==/=操作符。因此,关键在于要想清楚是否需要精确的相等性。根据经验,你应该始终都用=:==/=,只有在明确知道确实不需要精确相等性时才换成==/=。当数值的期望类型和实际类型不一致时,这种做法有助于避免一些错误的比较。

还有其他一些用于比较的操作符:<(小于)、>(大于)、>=(大于等于)和=<(小于等于)。最后一个是倒着写的(我的观点),我的代码中的许多语法错误都是由它产生的。请多留意一下这个=<

12> 1 < 2.true13> 1 < 1.false14> 1 >= 1.true15> 1 =< 1.true

如果输入5 + llama或者5 =:= true会怎样呢?要想知道答案,最好的办法当然是去试验一下,然后被错误结果吓一跳!

12> 5 + llama. ** exception error: bad argument in an arithmetic expression      in operator +/2          called as 5 + llama

看来,Erlang确实不喜欢我们将它的基本类型用错。仿真器返回一条出错消息,表明它不喜欢出现在+操作符两边的某个参数。

不过,对于用错类型,Erlang也并不总是会生气:

13> 5 =:= true.false

为什么在有些操作中拒绝接受不同的类型,而在另一些操作中却又允许呢?尽管Erlang不允许把两个不同类型的操作数加在一起,但是却允许对它们进行比较。这是因为Erlang语言的发明者把实用性的优先级排在理论前面,觉得如果能简单地写出像通用排序算法那样的程序,可以对任意数据排序,岂不是很棒!做出这个决定是为了简化编程工作,事实也基本上证实了这一点。

在进行布尔代数和比较操作时,还有最后一点要铭记在心:

14> 0 == false.false15> 1 < false.true

如果你原来熟悉的是过程式语言或者流行的面向对象语言,那么很可能要抓狂了。第14行应当求值为true,而第15行应当求值为false!毕竟,false代表0,而true代表除0以外的任何东西!在Erlang中不是这样。因为我对你撒了谎。是的,我确实撒谎了。很惭愧。

Erlang中并没有布尔值truefalse之类的东西。数据项truefalse都是原子,只要你始终认为falsetrue只能表示字面意思,那么它们其实和语言融合得是非常好的,不会带来问题。

注意

比较操作中,数据类型之间的大小顺序是:number < atom < reference < fun < port < pid < tuple < list < bit string。其中有些类型你现在可能还不熟悉,随着对本书的学习,你会逐渐了解的。现在,只需要记住,正是因为有了这个顺序,才可以在任意数据类型之间进行比较。引用Erlang语言的发明者之一Joe Armstrong的一句话:“具体的顺序并不重要——重要是定义明确的全局顺序。”

2.5 元组

使用元组可以把一组数据项组织到一起。在Erlang中,元组的书写形式为{Element1, Element2</em>, ...,<em> ElementN}。例如,如果你想告诉我笛卡儿坐标系中一个点的位置,就可以提供出它的坐标(xy)。这个点可以被表示成一个具有两个数据项的元组:

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

在这个例子中,一个点就是两个数据项。这样,就不用到哪儿都带着变量XY,只要带着这一个元组就可以了。不过,如果拿到一个点,但是只想知道它的x坐标时,该怎么办呢?获取这个信息也没什么困难的。记住,在给变量赋值时,如果新旧值相同,Erlang就不会报错。

我们来利用这个特性。(在输入下面的例子前,需要用f()清除以前设置的变量。)

3> Point = {4,5}.{4,5}4> {X,Y} = Point.{4,5}5> X.46> {X,_} = Point.{4,5}

Erlang也可以这么学!

从现在开始,X就是元组中第一个元素的值了。是怎么做到的呢?一开始,XY没有值,所以都是未绑定的变量。当把它们放在=操作符左边的元组{X,Y}中时,=操作符会去比较两边的值:{X,Y}{4,5}。Erlang非常聪明,它会把值从元组中取出,并把它们分配给左边的未绑定变量。接下来,就变成{4,5}={4,5}的比较了,显然是成立的。还有很多其他种类的模式匹配形式,这只是其中的一种。

注意,第6行使用了无需关心型变量()。它的用法正如其名:把本来应该放置在这个位置的值丢弃掉,因为我们不会用到这个值。通常,变量会一直被当作未绑定的,在模式匹配中充当通配符的作用。只要两边元组中元素的个数(元组的长度)相等,就可以通过模式匹配来提取元组中的元素。

7> {_,_} = {4,5}.{4,5}8> {_,_} = {4,5,6}.** exception error: no match of right hand side value {4,5,6}

元组还可以配合单个值使用。例如,假设我们想保存下面的温度值:

9> Temperature = 23.213.23.213

哇,这个温度太适合去海滩玩耍了!但是,等等,这个温度是开氏、摄氏还是华氏?我们可以用元组把温度值和单位放到一起:

10> PreciseTemperature = {celsius, 23.213}.{celsius,23.213}11> {kelvin, T} = PreciseTemperature.** exception error: no match of right hand side value {celsius,23.213}

抛出异常了,不过这正是我们期望的。这同样是模式匹配所导致的。=操作符比较{kelvin, T}{celsius, 23.213},虽然变量T是未绑定的,但是Erlang发现了原子celsius和原子kelvin是不同的。因此产生了一个异常,并停止了代码执行。所以,期望开氏温度的程序代码是不能处理摄氏温度的。这样,程序员就能方便地知道传入的是何种数据,并且还能用作一种辅助的调试手段。

如果一个元组中包含一个原子,原子后面只跟随着一个元素,那么这样的元组就称为带标记的元组(tagged tuple)。这种元组中的元素可以是任意类型,甚至是另外一个元组:

12> {point, {X,Y}}.{point,{4,5}}

但是,如果想处理多个点时该怎么办呢?此时,可以使用列表(list)。

2.6 列表

列表是很多函数式语言的重要组成部分。列表可以用来解决各种各样的问题,毋庸置疑,列表是Erlang中使用最广泛的数据结构。列表中可以包含任何东西——数值、原子、元组以及其他列表——所有能想象得到的东西都可以放到这个数据结构中。

列表的基本形式是[Element1Element2,...ElementN],在列表中,可以混合放入多种数据类型:

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

很简单,对吧?再试另外一个:

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

这是Erlang中最令人讨厌的东西之一:字符串。字符串就是列表,它们的表示方法完全一样。那为什么令人讨厌呢?原因如下:

3> [97,98,99,4,5,6].[97,98,99,4,5,6]4> [233]."é"

在打印数值列表时,只有当其中至少有一个数字代表的不是字符时,Erlang才会把它们作为数值打印出来。在Erlang中,根本就没有真正的字符串!毫无疑问,你将来肯定会为此而困扰,并会因此怨恨这门语言。不过,别失望,因为还有其他书写字符串的方法,我们会在1.3.3节中进行介绍。

保持冷静

这也是为什么会有程序员说Erlang不擅长字符串处理的原因:Erlang中不具有其他大多数语言中的内置字符串类型。缺乏的原因是因为Erlang最初是由电信公司创建和使用的,他们从不(或者很少)使用字符串,所以没有把字符串作为一种独立类型正式增加到语言中。不过,随着时间的流逝,这个问题正逐步得到解决。现在,Erlang虚拟机(VM)已经部分支持Unicode,在字符串处理上也一直在提速。还可以把字符串存成一个二进制数据结构,更加的轻量,处理速度也更快。我们会在1.3.3节进行介绍。

总之,标准库中字符串相关的函数还是不够多的。虽然用Erlang做字符串处理是可行的,但是如果要大量处理字符串时,可以选择更适合的语言,如Perl和Python。

++操作符可以把列表粘合起来。--操作符可以从列表中删除元素。

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,2,3] -- [1,2] -- [3].[3]10> [1,2,3] -- [1,2] -- [2].[2,3]

在第一个例子中,从右向左,首先从[1,2]中移除[3],剩下[1,2]。接着从[1,2,3]中移除[1,2],只剩下[3]。在第二个例子中,首先从[1,2]中移除[2],得到[1],接着从[1,2,3]中拿走[1],产生最后的结果[2,3]

我们继续。列表中的第一个元素称为头(head),剩余的部分称为尾(tail)。可以用内建函数(BIF)获取它们:

11> hd([1,2,3,4]).112> tl([1,2,3,4]).[2,3,4]

注意

  BIF通常是一些不能用纯Erlang实现的函数,因此会用C或者任何其他实现Erlang的语言编写(在20世纪80年代是Prolog)。也有一些BIF,本可以用Erlang实现,但是为了让这些常用的操作性能更高,就用C来实现了。length(List)函数就是一个例子,它返回参数列表的长度(你一定猜到了)。

访问列表的头元素或者给列表增加头元素都非常快捷高效。几乎所有需要处理列表的应用都是先操作列表头元素的。因为这种操作模式使用得非常频繁,所以Erlang通过模式匹配提供了一种简单的方式来分离列表的头和尾:[Head|Tail]。下例展示的是如何给列表增加一个新的头元素:

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

在处理列表时,如果有某种快捷的方法可以保存列表尾,以便以后继续处理,就非常方便了。如果你还记得元组的概念以及我们是如何使用模式匹配从点({X,Y})中提取值的,那么你就能够理解如何以类似的方式切下并得到列表中的第一个元素(头元素):

15> [Head|Tail] = NewList.[1,2,3,4]16> Head.117> Tail.[2,3,4]18> [NewHead|NewTail] = Tail.[2,3,4]19> NewHead.2

这里使用的|操作符称为cons操作符(构造器)。事实上,仅凭cons操作符和值就能构建出任何列表:

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

换句话说,任何列表都可以用下面这个公式构建:[Term1| [Term2| [... | [TermN]]]]。所以,可以把列表递归地定义成一个头元素,后面跟着一个尾,而尾本身又是一个头元素后面跟着更多的头。从这个意义上说,可以把列表想象成是一条蚯蚓,你可以把它切成两半,然后,你就有了两条蚯蚓。

Erlang也可以这么学!

对那些没见过类似构造器的人来说,可能会对Erlang列表的构建方式感到困惑。为了帮助你熟悉这个概念,仔细阅读下面的所有例子(提示:它们都是等价的):

 [a, b, c, d][a, b, c, d | []][a, b | [c, d]][a, b | [c | [d]]][a | [b | [c | [d]]]][a | [b | [c | [d | []]]]]

理解了这一点,下一节中介绍的列表推导式(list comprehension)应该就好理解了。

注意

  以[1|2]这种形式构建出来的列表称为非良构列表(improper list)。非良构列表可以用于[Head|Tail]这种形式的模式匹配,但是在Erlang的标准函数(即使是length())中使用时会失败。这是因为Erlang期望的是良构列表(proper list)。良构列表的最后一个元素是空列表。当定义一个像[2]这样的数据项时,这个列表会被自动地转换成良构形式。同样,[1|[2]]也会被转换成良构形式。尽管非良构列表是合乎语法的,但是除了某些用户自定义数据结构,它们的用途非常有限。

2.7 列表推导式

列表推导式(list comprehension)可以用来构建或者修改列表。和其他列表操作方式相比,它们会让程序既短小又易于理解。一开始,可能觉得这个概念不太好理解,但是,它们是值得花时间学习的。不要犹豫,请不断试验本节中的例子,直到理解为止!

列表推导式的思想来自于数学中的集合表示法,所以,如果你曾经学过一门和集合论有关的数学课,那么就会觉得列表推导式很亲切。通过规定成员必须要满足的属性,集合表示法描述了如何构建一个集合。我们来看一个简单的例子:{xErlang也可以这么学! : x=x2}。它定义了一个集合,其中包含所有平方与自身相等的实数(这个结果集合是{0, 1})。{x :x>0}是一个更简单的集合表示法示例。它定义了一个包含所有大于0的数的集合。

和集合表示法类似,列表推导式也是基于其他集合构建集合的。例如,考虑这样一个集合{2n : nL},其中L 是列表[1, 2, 3, 4],我们可以这样理解:“对列表[1, 2, 3, 4]中的每个n,构建元素n*2”。基于此构建出来的集合是[2,4,6,8]。对于这个集合,Erlang的实现如下:

1> [2*N || N [2,4,6,8]

对比一下数学表示法和Erlang表示法,会发现并没有大的区别:大括号({})变成了中括号([]),冒号 (:)变成了两个管道符(||),操作符∈变成了箭头(<-)。换句话说,只更改了符号,但是保留了同样的逻辑。在本例中,列表[1, 2, 3, 4]中的每个值会依次模式匹配到N。箭头的作用和=操作符完全一样,只是它不会在不匹配时抛出异常。

还可以在列表推导式中使用具有布尔返回值的操作来增加约束条件。如果想得到1到10之间的所有偶数的集合,可以这样实现:

2> [X || X [2,4,6,8,10]

其中,X rem 2 =:= 0用来检测一个数是不是偶数。

Erlang中列表推导式的用法如下:

NewList = [Expression || Pattern List, Condition1, Condition2, ... ConditionN]

其中的Pattern<-List部分称为生成器表达式(generator expression)。

当想对列表中的每个元素都应用一个函数,要求它们都遵守某些约束时,列表推导式会很有用。例如,假设你有一家饭店,来了一个客人,看了菜单,问是否能够告诉他价格在3美元和10美元之间的所有菜品,消费税(大概百分之7)先不包含在内。

3> RestaurantMenu = [{steak, 5.99}, {beer, 3.99}, {poutine, 3.50}, {kitten, 20.99}, {water, 0.00}].[{steak,5.99},{beer,3.99},{poutine,3.5},{kitten,20.99},{water,0.0}]4> [{Item, Price*1.07} || {Item,Price} = 3, Price =< 10].[{steak,6.409300000000001},{beer,4.2693},{poutine,3.745}]

上面的小数点没做舍入,不太易读,不过,你应该知道这个例子想表达的要点。

列表推导式还有另外一个不错的地方,那就是可以同时使用多个生成器表达式,如下例所示:

5> [X+Y || X [4,5,5,6]

上面的表达式会执行1+3、1+4、2+3、2+4这4个操作。因此,如果想把列表推导式的用法描述得更通用些,可以写成这样:

NewList = [Expression || GeneratorExp1, GeneratorExp2, ..., GeneratorExpN,Condition1, Condition2, ... ConditionM]

注意,也可以把生成器表达式和模式匹配放在一起当成过滤器使用:

6> Weather = [{toronto, rain}, {montreal, storms}, {london, fog},6>              {paris, sun}, {boston, fog}, {vancouver, snow}].[{toronto,rain},   {montreal,storms}, {london,fog}, {paris,sun}, {boston,fog}, {vancouver,snow}]7> FoggyPlaces = [X || {X, fog} [london,boston]

如果列表Weather中的一个元素不能匹配{X, fog}这个模式,在列表推导式中会被直接忽略,在=操作符中则会抛出异常。

正文结束

又轻松、又有趣、又可以学编程的内容来自哪里?就是这本书啦,学习Erlang编程的第一步从这本书开始肯定没错的。在哪里能买得到?点击【阅读原文】去人民邮电出版社

Erlang也可以这么学!

这是一本讲解Erlang编程语言的入门指南,内容通俗易懂,插图生动幽默,示例短小清晰,结构安排合理。书中从Erlang的基础知识讲起,融汇所有的基本概念和语法。内容涉及模块、函数、类型、递归、错误和异常、常用数据结构、并行编程、多处理、OTP、事件处理,以及所有Erlang的重要特性和强大功能。

本书适合对Erlang编程语言感兴趣的开发人员阅读。


福利 |


以上是关于Erlang也可以这么学!的主要内容,如果未能解决你的问题,请参考以下文章

如果可以,我也不想学设计模式!

Go语言能在中国这么火是因为啥?

linux就该这么学 第五课

iOS学习之代码块(Block)

erlang下lists模块sort(排序)方法源码解析

新书推荐—《Erlang趣学指南》