Erlang趣学指南

978-7-115-43190-5
作者: 【加】Fred Hébert(弗莱德·赫伯特)
译者: 邓辉孙鸣
编辑: 杨海玲

图书目录:

详情

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

图书摘要

版权信息

书名:Erlang趣学指南

ISBN:978-7-115-43190-5

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

• 著    [加] Fred Hébert

  译    邓 辉  孙 鸣

  责任编辑 杨海玲

• 人民邮电出版社出版发行  北京市丰台区成寿寺路11号

  邮编 100164  电子邮件 315@ptpress.com.cn

  网址 http://www.ptpress.com.cn

• 读者服务热线:(010)81055410

  反盗版热线:(010)81055315


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

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


Copyright © 2013 by Fred Hébert. Title of English-language original: Learn You Some Erlang for Great Good!: A Beginner’s Guide, ISBN 978-1-59327-435-1, published by No Starch Press. Simplified Chinese-language edition copyright © 2016 by Posts and Telecom Press. All rights reserved.

本书中文简体字版由美国No Starch出版社授权人民邮电出版社出版。未经出版者书面许可,对本书任何部分不得以任何方式复制或抄袭。

版权所有,侵权必究。


作者Fred Hébert作为活跃在一线的有丰富实战经验的工程师,不仅把入门教程写得清晰易懂、深入浅出,更难能可贵的是从Erlang应用程序的完整生命周期角度把涉及设计、开发、测试、部署、调优的关键特性表现得淋漓尽致。跟着他的节奏,读者会很容易登堂入室,此外,书中配的插图幽默、诙谐、有爱,也为本书增色不少!

——余锋,阿里云研究员负责阿里云数据库


在我近20年的软件开发工作中,除了Erlang,还使用过许多其他编程语言。有工作需要的C/C++、Java,也有作为业余爱好使用的Lisp、Haskell、Scala等,其中我最喜欢的当属Erlang。除了因为我的电信软件开发背景外,还有一个很重要的原因是Erlang独特的设计哲学和解决问题方式。

大家听说Erlang,往往是因为其对高并发的良好支持。其实,Erlang的核心特征是容错,从某种程度上讲,并发只是容错这个约束下的一个副产品。容错是Erlang语言的DNA,也是和其他所有编程语言的本质区别所在。

我们知道,软件开发中最重要的一部分内容就是对错误的处理。所有其他的编程语言都把重点放在“防”上,通过强大的静态类型、静态分析工具以及大量的测试,希望在软件部署到生产环境前发现所有的错误。而Erlang的重点则在于“容”,允许软件在运行时发生错误,但是它提供了用于处理错误的机制和工具。如果把软件系统类比为人体,那么其他编程语言只关注于环境卫生,防止生病;而Erlang则提供了免疫系统,允许病毒入侵,通过和病毒的对抗,增强免疫系统,提高生存能力。

这个差别给软件开发带来的影响是根本性的。大家知道,对于大型系统的开发、维护来说,最怕的就是无法控制改动的影响。我们希望每次改动最好只影响一个地方,我们通过良好的模块化设计和抽象来做到这一点。但是如果这个更改不幸逃过了静态检查和测试,在运行时出了问题,那么即使这个改动在静态层面确实是局部的,也照样会造成整个系统的崩溃。而在Erlang中,不仅能做到静态层面的变化隔离,还可以做到运行时的错误隔离[1],让运行时的错误局部化,从而大大降低软件发布、部署的风险。另外,分布式系统中错误出现的必然性更加凸显了Erlang容错哲学的价值。

Erlang语言不仅内置了容错支持工具——完全隔离的进程、链接(link)和监控器(monitor),还提供了一套完整的系统级容错语义模型——监督树。基于这个语义模型,可以清晰、准确、声明性地表达出系统中数据状态的关键程度、系统部件之间的错误相关性和依赖关系、系统各部分之间的承诺保证等,整个系统的容错处理都固化到这个显式、一致、标准的程序结构中。用户只需编写处理正常情况的代码,这个程序结构会自动处理出现的错误。

基于这种独特的容错哲学和支持工具,用户就会以拥抱崩溃、拥抱失败、拥抱异常的态度构建自己的系统,这些原本令人恐惧的东西现在以一种受控的方式存在于系统中,它们不再是让人讨厌的破坏者,而是被转化为了一种简单、强大的工具,用来构建更大、更可靠的系统。

强大的并发支持也是Erlang的特色之一,在这一点上常常被其他语言争相模仿。不过,Erlang和模仿者之间有个根本的不同点——公平调度。为了做到公平调度,Erlang可谓“不择手段”,并做到“令人发指”的地步[2]。为什么要费劲做这些工作呢?对于一个高并发系统来讲,软实时、低延时、可响应性往往是渴求的目标,同时也是一项困难的工作。尤其是,在系统过载时,多么希望能具有一致的、可预测的服务降级能力。而公平调度则是达成这些目标的最佳手段,Erlang也是目前唯一在并发上做到公平调度的语言。

由于Erlang在容错和并发的公平调度方面的独特性,可以说,这些年来Erlang一直被模仿,但是从未被超越。

从某种意义上讲,Erlang不只是一门编程语言,更是一个系统平台。它不仅提供了开发阶段需要的支持,还提供了其他语言所没有的运行阶段的强大支持。其实,在静态检查和测试阶段发现的问题往往都是些“不那么有趣”的问题,那些逃逸出来的bug才是真正难对付的。特别是对于涉及并发和分布式的bug,往往难以通过静态检查和测试发现,并且传统的调试手段也无法奏效[3]。而Erlang则提供了强大的运行时问题诊断、调试、解决手段。使用Erlang的remote shell、tracing、自省机制以及强大的并发和容错支持,我们可以在系统工作时深入系统内部,进行问题诊断、跟踪和修正,甚至在需要时在线对其进行“高侵入性”的外科手术。一旦用户用这种方法解决过一个困难的问题,就再也离不开它了。如果要在静态类型和这项能力间进行选择,我会毫不犹豫地选择后者[4]

对于Erlang存在的问题[5],提得最多的有两个:一个是缺乏静态类型支持,另一个是性能问题。Erlang是动态类型语言,往往会被认为不适合构架大型的系统。我自己也非常喜欢静态类型。一个强大的静态类型系统不但能够大大提升代码的可读性,而且提供了一个在逻辑层面进行思考、设计的强大框架。另外,还可以让编译器、IDE等获取更深入的代码结构和语义信息,从而提供更高级的静态分析支持。

不过,在构建大型系统方面,我有些不同看法[6]。如果说互联网是目前最庞大的系统,相信没有人会反对,那么这个如此庞大的系统能构建起来的原因是什么呢?显然不是因为静态类型,根本原因在于系统的组织和交互方式。互联网中的每个部件都是彼此间隔离的实体,通过定义良好的协议相互通信,一个部件的失效不会导致其他部件出现问题。这种方式和Erlang的设计哲学是同构的。每个Erlang系统都是一个小型的互联网系统,每个进程对应一台主机,进程间的消息对应协议,一个进程的崩溃不会影响其他进程……Erlang所推崇的设计哲学是面向崩溃(crash-oriented)以及面向协议(protocol-oriented)是架构大型系统的最佳方式[7]

当然现在,鱼和熊掌可以兼得,Erlang已经支持丰富的静态类型定义和标注功能[8],并且可以通过Dialyzer工具进行一定程度的类型推导和静态检查。

再来说说性能问题。在计算密集型领域,Erlang确实性能不高[9]。因此,如果要编写的是需要大量计算的工具程序,那么Erlang是不适合的。不过,如果说涉及计算的部分只是系统中的一个局部模块,而亟需解决的是一些更困难的系统层面的设计问题——并发、分布式、伸缩、容错、短响应时间、在线升级以及调试运维等,那么Erlang则是最佳选择。此时,可以用Erlang作为工具来解决这些系统层面的难题,局部的计算热点可以用其他语言(如C语言)甚至硬件来完成。Erlang提供了多种和其他语言以及硬件集成的方法,非常方便,可以根据自己的需要(安全性、性能)进行选择。

我前段时间曾经开发过一款webRTC实时媒体网关[10],就是采用了Erlang + C的方案。其中涉及媒体处理的部分全部用C语言编写,通过NIF和Erlang交互,系统层面的难题则都交给Erlang完成。系统上线几个月,用户量就达到数百万。其间,系统运行稳定,扩容方便,处理性能也不错(尤其是高负载时的服务降级情况令人满意)。不是说使用其他语言无法做到,不过要付出的努力何止10倍[11]

前面做了这么多的铺垫,主要是为了激起读者对Erlang的兴趣。有了兴趣之后,下面当然就是要选择一本好的介绍Erlang知识的书籍进行深入、系统的学习。而读者手上拿着的这本,就是一本广受好评的关于Erlang的图书。这本书甚至力压Erlang之父Joe Armstrong的《Erlang程序设计》,被公认为是学习Erlang的一本佳作。这不是没有原因的。

首先,本书对Erlang和OTP平台进行了非常全面、详细的介绍,不仅讲解了语言的语法、常见的数据结构、基本的函数式编程和并发编程、套接字编程等内容,还深入讲解了OTP中的每个关键组件以及整个系统的发布方法。不仅如此,对于真实系统开发中会用到的关键知识内容,分别独立成章进行介绍,包括EUnit、ETS、Common Test、Mnesia、类型和Dialyzer等,甚至还用了一个专门的章节介绍分布式系统设计的核心困难所在和应对策略。

其次,对于一些关键主题,尤其是那些复杂的主题,作者并没有蜻蜓点水、一带而过,而是深入原理,辅以实例,进行了深入浅出的讲解。作者的讲解风格轻松、幽默,让读者在不知不觉中就理解了原本不那么容易理解的内容。讲解的过程中有大量的编程实例,这些例子都是以循序渐进的方式编写的。和很多其他书籍不同的是,这本书中的示例代码质量很高,有些甚至达到了产品级质量。

最后,也是最重要的一点。虽然本书中传递的知识点很多,但是并非只是讲解这些知识点是什么,而是把它们放到了具体的领域背景和时代背景中,让读者理解问题是什么,做出这些决策的原因是什么,有什么局限性。有了这些背景内容,读者可以更深刻地理解这些知识,在应用这些知识时,可以做出更准确的判断和权衡。更为难得的是,作者把自己在真实产品开发中积累的真知灼见、遇到的语言“坑”和对策也都呈现在这本书中。从某种程度上讲,本书其实是一本关于并发、容错、分布式系统设计的书,只是碰巧使用了这个领域中的DSL(也就是Erlang语言)进行设计的表达。

无论你是初学者还是Erlang老手,尤其是你想在产品系统开发中使用Erlang时,我都强烈推荐你阅读本书。你一定不会失望的!

最后,如果发现译文中有任何问题,欢迎来信指正(dhui@263.net)。祝大家阅读愉快!

邓辉

2016年8月于上海

[1] 借助操作系统的进程也可以做到运行时的错误隔离,不过粒度太大,也过于重量。

[2] 为了能够做到跨OS的高效调度,Erlang放弃了基于时间片,采用了基于reduction的方式。几乎在系统的每个地方都会进行reduction计数,达到公平调度的目的。

[3] 例如,一个和竞争有关的bug,一旦加上断点,竞争可能就不会出现了。

[4] 其实可以兼得,Erlang现在已经支持类型定义、标注和推导。

[5] 我们不讨论那些主观性太强的问题,例如,有人觉得Erlang语法怪异。

[6] 这些看法只针对Erlang。对于其他的动态类型语言,在程序规模变大后,确实有难以理解和维护的问题。在Erlang中,由于其let it crash哲学,很多动态类型语言的问题可以在很大程度上被避免。著名的AXD301就是用Erlang开发的,规模达百万行代码,历时3年。而其前身AXE-N是用具有静态类型支持的C++语言开发的,开发7年后失败了。这个例子充分说明,对于大型、复杂的系统来说,语言的语义模型是成败的关键。

[7] 目前火热的micro-service架构在某种程度上和Erlang的哲学类似。在Erlang中,微服务只是一个语言特性。

[8] 无论如何,给程序加上类型标注都是一项好的实践。

[9] 这方面的性能大概是C语言的1/7。

[10] 主要作用是完成浏览器的webRTC媒体流和IMS网络媒体流之间的互通,需要大量转码和控制。

[11] 这个是我自己对比的数字。我曾经用C++语言开发过类似的系统。


学习编程很有趣,至少应该是一件有趣的事情。如果没有趣的话,读者就不会喜欢上它。在我的程序员职业生涯中,我曾经自学了几种不同的编程语言,在这个过程中,有时也会觉得不是那么有趣。在学习一门编程语言的过程中是否觉得有趣,很大程度上取决于对这门语言的介绍方式。

开始学习一门新的编程语言时,表面上看起来,好像就是学习这门新语言本身。但是,深入思考后会发现,所要学习的其实是意义更加深远的东西——一种新的思考问题的方法。而令人兴奋的正是这种新的思考方法,不是语言中那些微小的标点符号细节,也不是这门语言和你最喜欢的编程语言在外观上的不同。

在编程领域,函数式编程一直背负着“难学”的名声(并发编程更是如此),因此,编写一本关于Erlang语言的书,并在其中同时介绍函数式编程和并发编程,想想都令人望而却步。毫无疑问,介绍函数式编程不是一件容易的事情,介绍并发编程也很困难。除非具有非常特殊的才能,否则根本无法以轻松、幽默的方式同时介绍这两方面的内容。

Fred Hebert向我们展示了他的这种特殊才能。他总能把复杂的概念以简单的方式介绍给大家。

在学习Erlang时有一个最大的障碍,这个障碍并不在于Erlang中的概念本身非常难以理解,而在于这些概念和在其他大多数语言中遇到的概念非常不同。为了学习Erlang,得先暂时忘掉在其他语言中学到的东西。Erlang中的变量不能改变。不要进行防御性编程。进程非常、非常的轻量,如果愿意的话,可以创建上千甚至上百万个进程。哦,还有就是Erlang的语法比较奇怪。Erlang和Java不同,它没有方法或者类,也没有对象……甚至等号的含义也不是“等于”——它的意思是“匹配这个模式”。

Fred完全无惧这些问题,他在处理这些内容时采用了一种巧妙的冷幽默方式,并且在教授这些复杂的主题时,他所采用的方法完全让我们感受不到复杂性的存在。

这是到目前为止第4本主要的Erlang书籍,并作为一部重量级的作品加入Erlang丛书之列。但是,这本书不仅仅是关于Erlang的。Fred在这本书中介绍的许多概念同样适用于Haskell、OCaml以及F#语言。

我希望大家能够像我一样喜欢Fred的这本书。学习Erlang的过程是一个令人愉悦、发人深省的过程,我也希望大家在学习的过程中能体会到这一点。如果在阅读本书的过程中,把书中的程序输入电脑并运行,那么你会学到更多的东西。编写程序要比阅读程序困难得多,第一步要做的就是让你的手指习惯于程序的录入,并尝试着去修正那些不可避免的语法小错误。随着阅读的深入,你会编写出对其他编程语言来说非常困难的程序——不过还好,你不会感觉到这一点。很快,你就可以编写分布式程序了。有趣的旅程就此开始了……

Fred,谢谢你写了一本好书。

Joe Armstrong    

2012年11月6日于瑞典斯德哥尔摩


本书的写作最初是在网站上开始的,现在仍然能够访问http://learnyousomeerlang.com/(感谢No Starch出版社在出版和技术素材方面的完全开放性)。从2009年公开本书第1章的内容开始,本书慢慢地从一份只有3章内容的微型教程(当时在erlang-questions邮件列表中请求大家对内容进行校正)成长为一份学习Erlang的官方推荐文档、一本书、一个我生命中的重大成就。对于这本书带给我的一切,从朋友到工作,再到2012年度Erlang User的名号,我既困惑又感恩。

当作为一个局外人从远处审视Erlang程序员时,会发现他们看起来像一个古怪的小团体,他们所信奉的原则几乎没有人愿意遵从。这些原则看起来不切实际、在应用方式上有诸多限制。更糟糕的是,Erlang社区的成员看起来很像某个宗教派别的成员,完全认为他们掌握了软件世界中心的唯一真理。这种“唯一真理”和Lisp派、Haskeller、形式化证明派中的自命不凡者、Smalltalk程序员、Forth中的栈派等语言狂热派以前所鼓吹的完全一样。没什么特别的,还是那老一套。它们都承诺一定会成功,只是方式不同,但是程序员所编写的程序仍然错误百出,仍然代价高昂,仍然难以维护。

对Erlang来说,可能是它对并发或者并行的承诺吸引读者阅读本书的。也许吸引读者的是这门语言对于分布式计算的支持,或者是其不寻常的容错机制。当然,带着怀疑来学习Erlang是一件好事。它不能解决你的所有问题——毕竟,这是你的责任。Erlang只是一个很好的工具箱,帮你解决问题。

你可能已经了解Erlang,或许还很深入。如果是这样的话,我希望本书阅读起来比较有趣,能够成为一本参考手册,或者其中的某些章可以让你更深入地了解Erlang语言的某些部分和工作环境,这些内容你以前可能不太熟悉。

也许,你对Erlang的了解在各个方面都比我好。那么,我希望本书能够拿来压压东西或者填充你的图书馆空间。

谢谢你的支持,本书的内容经过了专业的编辑,书中Erlang的版本也提升到了R15B+,希望你能喜欢。


感谢Miran Lipovača首先想到了Learn You a Language这个主意,并同意我在本书以及相关的网站中借用这个想法。

感谢Jenn(我的女朋友)设计了最初的网站,为了能够让图片适合打印,她对本书中几乎全部图片进行了重新绘制,感谢她的辛苦工作。感谢她在我花费大量时间编写本书的过程中所给予的支持和耐心。

感谢所有花时间评审本书在线版本、寻找错误并提供帮助的人们(排名不分先后):Michael Richter、OJ Reeves、Dave Pawson、Robert Virding、Richard O’Keefe、Ulf Wiger、Lukas Larsson、Dale Harvey、Richard Carlsson、Nick Fitzgerald、Brendon Hogger、Geoff Cant、Andrew Thompson、Bartosz Fabianowski、Richard Jones、Tuncer Ayaz、William King、Mahesh Paolini-Subramanya以及Malcolm Matalka。还有很多其他人也做了少量的评审工作,指出了一些拼写等方面的错误。

还要再次感谢Geoff Cant,他是本书的正式技术评审。

感谢No Starch出版社的工作团队(Keith、Alison、Leigh、Riley、Jessica、Tyler和Bill)所做出的专业工作。

最后,感谢本书在线版本的所有读者,包括那些购买本书的读者和没有购买的读者。


这是本书的开头部分。阅读本书应该是学习Erlang编程的第一步,所以,我们还是有必要说点什么。

我是在读了Miran Lipovača的Learn You a Haskell for Great Good!(LYAH)教程后,才萌生了写这本书的想法。我觉得在引发读者兴趣和提供友好的学习体验方面,他做得非常棒。因为我早就认识他,我就去问他,如果我也按照这样的方式写一本关于Erlang的书,他会觉得怎样。他很喜欢这个想法,有部分原因应该是出于他对Erlang的兴趣吧。

于是我就开始写这本书。

当然,我之所以决定写这本书还有其他原因。当我开始学习Erlang时,我发现要找到这门语言的入门书很困难(Web上的文档稀缺,书又比较贵),并且我认为如果存在类似LYAH这样的指南,对整个Erlang社区都是有好处的。此外,我还发现人们对于Erlang好坏的评价都非常的笼统。

本书的读者需要具备命令式语言(像C/C++、Java、Python、Ruby之类)编程的基础知识,但是并不要求读者具备函数式编程语言(像Haskell、Scala、Clojure、OCaml和Erlang)方面的知识。我在编写本书时尽量做到客观、诚实,对Erlang实话实说,既肯定它的强项,也不避讳它的弱点。

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

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

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

x = 5 + 1
x = 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/23
x = x
∴ 2013/10/22 = 2013/10/23

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

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

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

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

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

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

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

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

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

Erlang提供了从错误中恢复的能力,用actor来组织代码的能力,以及用分布式和并发进行伸缩的能力,这些听起来都棒极了,那我们就赶快进入下一节吧……

本书中有很多放入小框框中的文字都以这个名字为标题(阅读的过程中读者会看到)。Erlang目前之所以大受欢迎都源自于一些狂热的论点,这会导致人们对它过度信任。如果你也是一名狂热的Erlang学习者,那么下面的一些提醒有助于你保持清醒。

第一个提醒是,人们把Erlang强大的伸缩能力归结于它轻量的进程。没错,Erlang的进程的确非常轻量,你可以同时拥有数十万个进程。但是,这并不等于说,因为能这样用Erlang,所以一定要这样用。例如,在一个射击游戏中,让每颗子弹成为一个actor,就很不明智。要是按照这种方式设计一个射击游戏,能射中的就只能是自己的脚丫了。在actor之间发送消息还是存在少量开销的,如果把任务划分得太细,效率会降低不少!

在我们学到后面,真正需要关心这个问题时,我会深入进行讲解。现在只需记住,随意地对问题进行并行化是不足以提升其效率的。(别灰心,有时候,使用上百个进程既是可行的,也是管用的!)

也有观点说,Erlang的伸缩能力和计算机的核数成正比,但这通常不是真的。在某些情况下是可能的,但在绝大多数情况中,遇到的都是些无法让所有东西都同时运行的问题。

还有一件事情需要牢记,尽管Erlang在某些方面做得确实不错,但是,从技术上来说,用其他语言也是可能取得同样效果的。反之亦然。你应该仔细评估需要解决的问题,为其选择最佳工具和解决方案。Erlang不是银弹,尤其不适合开发图像和信号处理、操作系统设备驱动之类的功能。Erlang的强项在于:服务器端的大型软件(如队列中间件、Web服务器、实时竞价系统和分布式数据库),协助其他语言完成一些困难的工作,高层协议实现等。至于中间地带的部分,你自行决定。

Erlang也不是一定只能用来进行服务器端软件的开发。已经有人将Erlang用在很多意想不到的地方。一个例子就是IANO,它是Unict团队(Catania大学的Eurobot团队)制造的一个机器人,其中的人工智能部分就是用Erlang开发的。在2009年度Eurobot大赛中,IANO获得了银奖。还有一个例子是Wings 3D,这是一个开源、跨平台的3D建模工具(不是3D渲染器),也是用Erlang实现的。

学习Erlang,只要一个文本编辑器和Erlang环境就足够了。可以从Erlang官网上获取源码和Windows平台的二进制可执行文件。

对于Windows操作系统来说,下载并运行二进制文件即可。别忘了把你的Erlang目录添加到PATH系统变量中去,这样就能从命令行直接访问它了。

对于基于Debian发布的Linux操作系统来说,要用下面的命令来安装Erlang包:

$ sudo apt-get install erlang

如果使用的是Fedora系统(假设已经安装了yum),可以输入下面的命令来安装Erlang:

# yum install erlang

不过,这些库中存放的通常都是些陈旧的Erlang安装包。使用旧的版本运行本书中的样例代码可能会得到和书中不一样的结果,对有些特定的应用,还可能会出现性能下降。因此,我建议从源代码中编译出安装包。参考包中的README文件,并且使用Google查找需要的所有安装细节。

在FreeBSD中,有很多可用的选项。如果用的是portmaster,可以用下面的命令:

$ portmaster lang/erlang

如果是标准的ports系统,可以输入下面的命令:

$ cd /usr/ports/lang/erlang; make install clean

最后,如果想使用这个包,输入以下命令:

$ run pkg_add -rv erlang

如果用的是Mac OS X系统,可以用Homebrew来安装Erlang:

$ brew install erlang

如果喜欢的话,也可以用MacPorts:

$ port install erlang

注意

 
在撰写本书时,我使用的Erlang版本是R15B+,所以为获取最佳学习效果,读者应该使用这个版本或者更新的版本。不过,本书的绝大部分内容对像R13B这样老的版本也是有效的。

除了下载、安装Erlang外,读者还应当下载和本书配套的完整代码文件。其中包含有本书所有的程序和模块代码,都已经过测试。这些代码有助于修正读者自己程序中的问题。如果读者想跳着阅读本书,那么也可以把它们当作学习后面章节的基础代码。这些文件全部被打包放在一个zip包中,可以从http://learnyousomeerlang.com/static/erlang/learn-you-some-erlang.zip下载。除此之外,本书的样例代码不再有任何其他外部依赖了。

如果用的是Linux,就可以从man中找到很好的技术文档。例如,Erlang中有个lists模块(会在第1章中介绍)。要找到lists模块的文档,只需要输入以下命令:

$ erl -man lists

在Windows系统中,安装得到的文件中包括HTML文档。也可以随时从Erlang官网或其他的备选网站上下载这些文档。

如果希望把代码写得整洁些,可以从http://www.erlang.se/doc/programming_rules.shtml中学些到一些优秀的编码实践。本书中的代码也会尽量遵循这些指导原则。

有时,读者会发现仅仅了解一些技术细节并不足以解决手边的问题。此时,我主要从两个地方寻求帮助:Erlang的官方邮件列表(跟着它,就能学到不少东西)和irc.freenode.net网站上的#erlang频道。


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

在本章中,我们会介绍Erlang shell的使用方式,还会介绍一些基本的Erlang数据类型。

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

$ erl
Erlang 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 ProgramsErlang)。werl是一个专门用于Windows系统的Erlang shell,它有自己的窗口,窗口具有滚动条,还支持一些行编辑快捷键(Windows系统中的标准cmd.exe shell不支持这些快捷键)。然而,如果想重定向标准输入或者输出,又或者想使用管道,那么就只能使用erl.exe来启动shell。

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

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

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

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

至此,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
--> h
c [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作业旁有个星号(*):

--> j
1* {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: killed
1> "YESS!"

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

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

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

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

1> 2 + 15.
17
2> 49 * 100.
4900
3> 1892 - 1472.
420
4> 5 / 2.
2.5

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

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

5> 5 div 2.
2
6> 5 rem 2.
1

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

7> (50 * 100) – 4999.
1
8> -(50 * 100 – 4999).
-1
9> -50 * (100 – 4999).
244950

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

10> 2#101010.
42
11> 8#0677.
447
12> 16#AE.
174

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

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

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

1> One.
* 1: variable 'One' is unbound
2> One = 1.
1
3> Un = Uno = One = 1.
1
4> Two = One + One.
2
5> Two = 2.
2
6> Two = Two + 1.
** exception error: no match of right hand side value 3

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

7> 47 = 45 + 2.
47
8> 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极有可能不间断地运行很多年,在这期间某个给定变量名肯定会被重复使用。

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

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

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

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

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

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

保持冷静

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

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

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

注意

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

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

布尔代数非常简单:

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

注意 

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

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

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

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

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

12> 1 < 2.
true
13> 1 < 1.
false
14> 1 >= 1.
true
15> 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.
false
15> 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的一句话:“具体的顺序并不重要——重要是定义明确的全局顺序。”

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

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

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

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

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

从现在开始,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)。

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

列表的基本形式是[Element1, Element2,..., 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]).
1
12> 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.
1
17> 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列表的构建方式感到困惑。为了帮助你熟悉这个概念,仔细阅读下面的所有例子(提示:它们都是等价的):

 [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]]也会被转换成良构形式。尽管非良构列表是合乎语法的,但是除了某些用户自定义数据结构,它们的用途非常有限。

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

列表推导式的思想来自于数学中的集合表示法,所以,如果你曾经学过一门和集合论有关的数学课,那么就会觉得列表推导式很亲切。通过规定成员必须要满足的属性,集合表示法描述了如何构建一个集合。我们来看一个简单的例子:{x : 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 <- [1,2,3,4]].
[2,4,6,8]

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

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

2> [X || X <- [1,2,3,4,5,6,7,8,9,10], X rem 2 =:= 0].
[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} <- RestaurantMenu, Price >= 3, Price =< 10].
[{steak,6.409300000000001},{beer,4.2693},{poutine,3.745}]

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

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

5> [X+Y || X <- [1,2], Y <- [3,4]].
[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} <- Weather].
[london,boston] 

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

在结束本章前,我们还要再学习一个基本数据类型。它是Erlang中一个不可思议的特性,有了它,解析二进制数据将易如反掌。

和其他大多数语言不同,在用模式匹配处理二进制数据方面,Erlang提供了非常有用的抽象,不需要使用那些专门的操作符进行老套的位操作。这种做法让原始二进制数据的处理变得有趣且容易(是真的),这对于Erlang所服务的电信应用来说是非常有必要的。乍一看,会觉得位操作的语法和惯用法非常奇怪,但是如果你知道位和字节一般的工作原理,就会觉得这些语法非常合理了。(如果你对二进制操作不熟悉的话,可以略过本章后面的内容)。

在Erlang的位(bit)语法中,会把二进制数据用<<>>括起来,并把数据分隔成多个易理解的区段,区段之间用逗号分隔。一个区段就是一个二进制的位序列(不一定要在字节边界上,尽管默认情况下会这样)。

假设你想存储真彩(24位)中的一个橙色像素。如果你曾经在Photoshop或者Web CSS样式表中调测过颜色,那么就应该知道颜色的十六进制表示格式是#RRGGBB。浅色调的橙色用这个记法表示就是#F09A29,在Erlang中可以展开成如下样子:

1> Color = 16#F09A29.
15768105
2> Pixel = <<Color:24>>.
<<240,154,41>>

大概的意思是,“把二进制数值#F09A29以占用24位空间(红色占8位、绿色占8位、蓝色也占8位)的形式保存到变量Pix``e``l中”。随后可以把这个值写入文件或者套接字中。这看起来也许不太像,但是一旦被写入文件中,这个值就会变成一串不可读的字符,在正确的上下文中,会被解码成一幅图片。

这种语法非常令人愉快,因为你可以用整洁、易读的文本去表达那些实际工作起来复杂混乱的逻辑。如果没有一个好的抽象,那么代码也会是复杂混乱的。更好的是:当把这些二进制数据从文件中读取回来时,Erlang会把它们重新解释成漂亮的<<240,151,41>>格式!你可以在不同的表示方法之间自由地切换,根据需要,选择最有用的那种表示方法。

更有趣的是,还能使用模式匹配从二进制数据中提取内容:

3> Pixels = <<213,45,132,64,76,32,76,0,0,234,32,15>>.
<<213,45,132,64,76,32,76,0,0,234,32,15>>
4> <<Pix1,Pix2,Pix3,Pix4>> = Pixels.
** exception error: no match of right hand side value <<213,45,132,64,76,32,76,0,0,
234,32,15>>
5> <<Pix1:24, Pix2:24, Pix3:24, Pix4:24>> = Pixels.
<<213,45,132,64,76,32,76,0,0,234,32,15>>

在第3行中,我们定义了一个正好包含4个RGB颜色像素的二进制数据块。在第4行中,我们试图从这个二进制数据块中提取出这4个值。它抛出了异常,因为这个二进制数据中的区段个数多于4个——事实上,一共有12个区段。所以,我们要用Pix1:24Pix2:24之类的告诉Erlang,左边的每个变量都是24位的数据。然后,我们就可以把得到的第一个像素值进一步分解成单独的颜色值:

6> <<R:8, G:8, B:8>> = <<Pix1:24>>.
<<213,45,132>>
7> R.
213

“不错,非常棒。但是如果我一开始就只想要第一个颜色值该怎么办呢?难道每次都要把所有的值都提取出来吗?”别担心——为了解决此类问题,Erlang提供了许多语法糖和模式匹配方法:

8> <<R:8, Rest/binary>> = Pixels.
<<213,45,132,64,76,32,76,0,0,234,32,15>>
9> R.
213

在上面的例子中,Rest/binary是一种特别的表示法,能够让你表达无论二进制数据中还剩余什么,也无论它的长度是多少,都把它放到Rest变量中。所以,<<Pattern,Rest/binary>>对于二进制的模式匹配与[Head|Tail]对于列表的模式匹配是一样的。

很棒,对吧?上面的表示法之所以可行,是因为对于一个二进制区段的描述,Erlang允许有多种方式。下面的描述都是有效的:

Value
Value:Size
Value/TypeSpecifierList
Value:Size/TypeSpecifierList

其中,如果没有指定TypeSpecifierList,那么Size的单位永远是位。TypeSpecifierList由下面内容中的一个或者多个组成,之间用连字符(-)分隔。

类型

可能的取值为integerfloatbinarybytesbitstringbitsutf8utf16utf32。如果不指定类型,Erlang就会默认使用integer类型。

这个字段表示所使用的二进制数据的类型。注意,bytesbinary的缩写,bitsbitstring的缩写。

符号类型

可能的取值为signedunsigned。默认是unsigned。只有当类型为integer时,这个字段才有意义。

字节序

可能的取值为big、littlenative。字节序的默认值是big,因为它是网络协议编码中使用的标准字节序。

只有当类型为integer、utf16、utf32或者float时,字节序才有意义。这个选项和系统如何读取二进制数据有关。例如,BMP图片文件的头格式中用4个字节的整数来表示文件大小。如果一个文件的大小是72个字节,那么一个小字节序(little-endian)系统会把它表示成<<72,0,0,0>>,而一个大字节序(big-endian)系统会把它表示成<<0,0,0,72>>。在Erlang中,前者会解释成72,后者则会被解释成1 207 959 552,所以,要确保使用了正确的字节序。

还可以使用选项native,会在运行时根据本地CPU使用的是小字节序还是大字节序进行选择。

单位

写成unit:Integer这样的形式。

单位指的是每个区段的大小。取值范围为1~256。对于integerfloatbitstring类型来说,单位默认设置为1;对于binary类型,单位默认设置为8。类型utf8utf16utf32无需定义unit。Size和单位的乘积等于要提取的区段中的位数,并且必须能被8整除。单位的大小通常用来确保字节对齐。

数据类型的默认长度可以通过组合不同的二进制区段描述加以改变。例如,<<25:4/unit:8>>会把数值25编码成一个4字节的整数,其形象的表示就是<<0,0,0,25>><<25:2/unit:16>>会得到同样的结果,<<25:1/unit:32>>也一样。一般来讲,Erlang会接受 <<25:Size/unit:Unit>>这样的形式,并用Size乘以Unit算出表示这个值需要的空间。同样,空间的大小要能被8整除。

我们来看一些例子,帮助你理解刚才的这些定义:

10> <<X1/unsigned>> = <<-44>>.
<<"Ô">>
11> X1.
212
12> <<X2/signed>> = <<-44>>.
<<"Ô">>
13> X2.
-44
14> <<X2/integer-signed-little>> = <<-44>>.
<<"Ô">>
15> X2.
-44
16> <<N:8/unit:1>> = <<72>>.
<<"H">>
17> N.
72
18> <<N/integer>> = <<72>>.
<<"H">>
19> <<Y:4/little-unit:8>> = <<72,0,0,0>>.
<<72,0,0,0>>
20> Y.
72

可以看出,对于二进制数据的读取、存储和解释,存在多种方法。这有些令人困惑,不过与大多数语言中提供的常见工具相比,还是简单多了。

Erlang中也提供了标准的二进制操作(向左移位、向右移位以及二进制的andorxornot)。相应的操作符是bsl(按位左移)、bsr(按位右移)、bandborbxor以及bnot

2#00100 = 2#00010 bsl 1.
2#00001 = 2#00010 bsr 1.
2#10101 = 2#10001 bor 2#00101.

使用这个表示法和通用的位语法,可以非常容易地对二进制数据进行解析和模式匹配。例如,可以使用如下代码解析TCP报文:

<<SourcePort:16, DestinationPort:16,AckNumber:32,
DataOffset:4, _Reserved:4, Flags:8, WindowSize:16,
CheckSum: 16, UrgentPointer:16,
Payload/binary>> = SomeBinary.

如果SomeBinary确实是网络处理代码发送过来的TCP报文,就可以用类似的方式提取出其中的内容。所有值都以位为单位(除了Payload,它可以任意长),并且都是按照标准严格定义的。无论程序需要使用报文的哪个部分,直接使用相应的那个变量即可。

对于其他二进制数据,完全可以使用同样的匹配逻辑:视频编码、图像、其他协议实现等。

保持冷静

与C或者C++相比,Erlang要慢一些。除非你非常有耐心(或者是个天才),否则用Erlang去实现视频或者图像变换是不明智的,即使Erlang的二进制语法让这件工作变得异常有趣。Erlang历来都不擅长数值密集型计算。

不过,请注意,对于那些不需要数值计算的应用,如事件响应、消息传递(使用原子时,会特别轻量)等,Eralng通常是非常快的。它可以在毫秒(ms)内完成事件的处理,因此,它是构建软实时应用的极佳选择。

还有另一个全新的二进制表示法:二进制字符串(binary string)。二进制字符串在语言中的实现方式,和用列表实现字符串使用的方式完全一样,只是在空间使用上更加高效。这是因为,普通列表与链表(一个字符一个“结点”,还有一个指向列表其余部分的引用)类似,而二进制字符串更像是C语言中的数组(一种紧凑填充的内存块)。

二进制字符串的语法是<<"this is abinary string!">>。与列表相比,二进制字符串的缺点是,它的模式匹配和操作方法没有列表那么简单。因此,通常当所存储的文本无需频繁操作或者空间效率是个实际问题时,才会使用二进制字符串。

注意

 尽管二进制字符串非常轻量,还是不要用它们去标记数据。例如,使用字符串常量去表达{<<"temperature">>,50}是很有吸引力的,不过,对于这种情况,应该永远使用原子。如果使用原子,那么在对不同的值进行比较时几乎没有开销,并且不管原子多长,比较总可以在常量时间完成,而二进制字符串的比较则是线性时间的。另一方面,也不要因为原子更轻量,就用原子取代字符串。字符串是可以运算的(分隔、正则表达式等),而原子除了可以比较,什么都不能做。

二进制推导式(binary comprehension)之于位语法就等同于列表推导式之于列表:在处理二进制数据时,可以让代码短小精悍。一般来讲,它的使用方式也和列表推导式完全一样:

1> << <<X>> || <<X>> <= <<1,2,3,4,5>>, X rem 2 == 0>>.
<<2,4>>

语法层面唯一的变化是:标准列表推导式中的<-变成了<=(用于二进制生成器),使用的是二进制数据(<<>>),而不是列表数据([])。

在本章的前面,你曾经看到过一个使用模式匹配从表示大量像素数据的二进制数据中提取RGB值的例子。当时采用的技术在那个例子中是恰当的,但是对于更大型的结构,那样的做法会让代码变得难以理解和维护。使用二进制推导式,只需一行代码就可以完成同样的功能,更加整洁:

2> Pixels = <<213,45,132,64,76,32,76,0,0,234,32,15>>.
<<213,45,132,64,76,32,76,0,0,234,32,15>>
3> RGB = [ {R,G,B} || <<R:8,G:8,B:8>> <= Pixels ].
[{213,45,132},{64,76,32},{76,0,0},{234,32,15}] 

<-换成<=,我们就能把二进制数据当成生成器使用。上面的整个二进制推导式就是把二进制数据转换成元组中的整数。还可以通过另外一个二进制推导式来完成正好相反的功能:

4> << <<R:8, G:8, B:8>> || {R,G,B} >.
<<213,45,132,64,76,32,76,0,0,234,32,15>> 

这样做时要非常小心,因为如果生成器返回的是二进制数,那么就必须要对结果二进制数中的元素给出明确的二进制类型定义。

5> << <<Bin>> || Bin >] >>.
** exception error: bad argument
6> << <<Bin/binary>> || Bin >] >>.
<<3,7,5,4,7>>

默认情况下,Erlang会认为放入二进制数中以及从二进制数中提取的值都是整数(8位无符号)。当写出<<Bin>>时,实际上是在说,我们想要一个二进制数,里面包含了一个存储在变量Bin中的整数。问题是,Bin中保存的是另外一个二进制数,这对于Erlang来说完全不合理。我们承诺会提供一个整数,结果提供了一个二进制数。通过指定Bin的类型是binary(见第6行),Erlang就能够处理这个模式了,因为我们对Bin的描述和Bin中当前包含的完全吻合了。

还可以在二进制推导式中使用二进制生成器:

7> << <<(X+1)/integer>> || <<X>> <= <<3,7,5,4,7>> >>.
<<4,8,6,5,8>>

注意,在本例中把类型指定为integer是多余的,因为integer是Erlang中的默认类型。

在本书中,我不会再介绍有关二进制数和二进制推导式的更多细节内容。如果你想更全面地了解位语法,可以去阅读位语法的规范定义白皮书,参见http://user.it.uu.se/~pergu/papers/erlang05.pdf


使用交互式shell是动态语言编程中至关重要的一个部分。在交互式shell中,可以方便地测试代码和程序。在第1章中,我们甚至都没有使用文本编辑器或者保存文件,直接在交互式shell中就学习了Erlang语言的大部分基本数据类型。虽然你可以现在就放下这本书,跑出去打打球,并就此结束你的Erlang学习生涯,不过这样做会让你成为一个非常糟糕的Erlang程序员。代码当然需要存在某个地方才能被使用!在本章中,你将会看到,这就是模块要负责的事情。

模块(module)是一个具有名字的文件,其中包含一组函数。Erlang中的所有函数都必须定义在模块中。你之前已经使用过模块了,不过可能没有意识到。在第1章中提到的那些BIF函数,如hdtl,实际上是属于erlang模块的。所有的算术、逻辑和布尔操作函数也都在erlang模块中。

erlang模块中BIF函数和其他函数不同,因为在启动Erlang时,它们会被自动引入。模块中的其他所有函数都必须用Module:Function(Arguments)这样的形式进行调用,如下例所示:

1> erlang:element(2, {a,b,c}).
b
2> element(2, {a,b,c}).
b
3> lists:seq(1,4).
[1,2,3,4]
4> seq(1,4).
** exception error: undefined shell command seq/2

其中,lists模块中的seq函数不会被自动引入,而element函数会被自动引入。之所以出现“undefined shell command”这条出错消息,是因为shell会像查找f()这样的shell命令一样去查找seq,但是没有找到。erlang模块中有些函数也没有被自动引入,不过它们使用的不是很频繁。

从逻辑上来说,应该把处理类似事情的函数放在同一个模块中。列表的通用操作函数被放在lists模块中,处理输入和输出的函数(如向终端或者文件写入东西)被组织在io模块和file模块中。唯一违反这个原则的是erlang模块,其中包含有数学计算、类型转换、多进程处理、虚拟机设置等函数。除了都是BIF函数,它们之间没有任何共同点。避免创建类似erlang这样的模块,要关注于整洁、有逻辑的模块划分。

编写模块时,可以定义两种东西:函数(function)和属性(attribute)。属性是元数据,用来描述模块自身,如模块的名字、外部可见的函数、模块的作者等。这类元数据很有用,因为它既可以为编译器提供工作提示,也可以让程序员从编译过的代码中获取信息,无需去查看源码。

目前,世界各地的程序员在Erlang代码中所使用的属性种类非常繁多。事实上,只要你愿意,甚至可以声明自己的专有属性。不过,有些预定义的模块属性在代码中的使用频率要远高于其他属性。

所有模块属性都采用-Name(Attribute).的形式。要让你的模块能够编译,下面的模块属性必须定义:

-module(Name).

这个属性永远是文件中第一个属性(也是第一条语句),理由很充分:它是当前这个模块的名字,其中Name是一个原子。你调用其他模块中的函数时,使用的就是这个名字。函数调用采用的形式是M:F(A),其中M是模块名,F是函数名,A是参数。

注意,-module属性中定义的模块名必须和模块文件的名字一致。例如,如果模块名是unimaginative_mame,那么文件名必须是unimaginative_mame.erl(.erl是标准的Erlang源文件扩展名)。如果名字不一致,那么模块将无法编译。

一切就绪,可以编码了!我们要写的第一个模块非常简单,也没啥用处。打开文本编辑器,输入以下内容,然后把文件保存为useless.erl。

-module(useless).

只要有了这一行就是一个合法的模块。的确如此!当然,因为没有任何函数,所以也没什么用处。我们先来决定一下要从useless模块中导出(export)哪些函数。要想导出函数,需要使用另一个属性:

-export([Function1/Arity, Function2/Arity,`...`, FunctionN/Arity]).

这个属性用来定义模块中的哪些函数可以被其他模块调用。它接收一个带有各自元数(arity)的函数名列表。函数的元数是个整数,表示这个函数可以接收的参数个数。这是一个关键的信息,因为在同一个模块中定义的不同函数,只要它们的元数不同,就可以使用相同的函数名字。函数add(X,Y)add(X,Y,Z)就是不同的函数,分别表示成add/2add/3

注意

导出的函数相当于模块的接口。在定义接口时,尽量只暴露使用这个模块所必需的最少的信息,这一点非常重要。这样,你就可以随意更改内部实现细节,而不会影响那些依赖于你的模块的代码。

我们先让useless模块导出一个名为add的函数,这个函数有两个参数。在模块定义后面增加如下的-export属性:

-export([add/2]).

接着,我们来实现这个函数:

add(A,B) ->
    A + B.

函数定义的语法遵循Name(Args)->Body.这样的形式,Name必须是一个原子,Body可以是一个或者多个用逗号分隔的Erlang表达式。函数以一个句点结束。注意,和许多命令式语言不同,Erlang中没有return关键字。return毫无用处!无需显式说明,函数中最后一个表达式的执行结果会被自动作为返回值传递给调用者。

接下来,在文件中增加下面的函数。(是的,每个教程都需要一个“Hello,World”例子!)不要忘了把这个函数也加到-export属性中(现在,-export属性应该是这样:-export([add/2, hello/0]).)。

%%  显示欢迎语
%%  io:format/1是标准的文本输出函数
hello() ->
    io:format("Hello, world!~n").

先来讲讲上面代码中的注释。在Erlang中,注释只能是单行的,以%符号起始。(在本例中使用了%%,这纯粹是风格问题)。hello/0函数展示了如何在自己的模块中调用其他模块的函数。在本例中,正如注释写的那样,io:format/1是一个标准的文本输出函数。

注意

在Erlang社区中,习惯于在模块的概括性注释(模块是做什么的,许可证等)以及模块的区段分隔注释(公有代码、私有代码、辅助函数等)中使用3个百分号(%%%)。在所有其他需要放置在独立行中的注释使用2个百分号(%%),并和周边的代码采用同样的缩进。放在代码之后的行内注释,使用单个%。

我们来给这个模块增加最后一个函数,它同时使用了add/2hello/0函数:

greet_and_add_two(X) ->    
    hello(),
    add(X,2).

同样,不要忘了把greet_and_add_two/1添加到导出函数列表中。对hello/0add/2的调用不需要在前面加上模块名,因为这两个函数就是定义在这个模块内的。

如果想用和add/2或者其他定义在当前模块中的函数一样的方式去调用io:format/1,可以在文件的前面增加一个模块属性:-import(io,[format/1]).。然后,就可以直接调用format("Hello,World!~n").了。-import属性定义的一般形式如下:

-import(Module, [Function1/Arity, `...`, FunctionN/Arity]).

因为引入函数的做法会降低代码的可读性,所以大多数程序员都强烈反对在代码中使用-import属性,尽管它确实能带来一些便捷。例如,以函数io:format/2为例,这个函数有一个同名函数,不过位于另外一个库io_lib:format/2中。此时,如果函数是被引入的,那么要判断出代码中使用的到底是哪个函数,就要到文件的最前面去查看这个函数是从哪个模块中引入的。因此,在函数调用中包含模块名被认为是一项好的实践,对于那些喜欢使用grep在工程间进行搜索的Erlang程序员来说,这种做法也是有帮助的。通常,只有lists模块中的函数会被引入,因为lists模块中函数的使用频率要远高于其他大多数模块中的函数。

现在,useless模块的内容应该如下所示:

-module(useless).
-export([add/2, hello/0, greet_and_add_two/1]).

add(A,B) ->
    A + B.

%%  显示欢迎语
%%  io:format/1是标准的文本输出函数
hello() ->
    io:format("Hello, world!~n").

greet_and_add_two(X) ->
    hello(),
    add(X,2).

useless模块编写完成了。保存useless.erl文件,接下来可以尝试编译它了。

Erlang代码会被编译成字节码,这样VM就能执行它了。调用编译器有多种方法。最常用的一种是在命令行中调用它,例如:

$ erlc flags file.erl

如果是在shell或者模块中,可以像这样编译代码:

compile:file(Filename)

还有一种方法,在开发代码时经常使用,就是在shell中编译:

c()

好了,是时候编译运行useless模块了。但是,先要告诉Erlang shell在哪里能找到我们的模块。打开Erlang shell,输入下面的命令,要填上文件存放目录的全路径。

1> cd("/path/to/where/you/saved/the-module/").
"Path Name to the directory you are in"
ok
```

默认情况下,shell只会在它的启动目录和标准库中去查找文件。cd/1函数专用于Erlang shell,可以更换shell的当前目录,这样寻找文件就会方便一些。

接下来,输入以下命令:

2> c(useless).
{ok,useless}

如果你看到的信息和上面不同——出现了像userless.erl:Line: Some Error Message之类的显示——请确认一下文件名是否正确,文件的路径是否正确,模块中是否有括号不匹配、遗漏了结束符(.)之类的错误,等等。

代码被成功编译后,你会发现在工作目录中,紧挨着useless.erl之后,多了一个useless.beam文件。这就是编译好的模块文件。

注意

文件扩展名.beam是Bogdan/Björn’s Erlang Abstract Machine的缩写,也是虚拟机的名字。还有一些其他种类的Erlang虚拟机,但是基本上都不再使用了。例如,灵感来自于Prolog的WAM的Joe’s Abstract Machine (JAM)以及试图先把Erlang编译成C,然后再编译成本地码的旧版BEAM。基准测试表明这种做法收效甚微,所以就被放弃了。最近,还有人尝试把Erlang移植到JVM上,并发明了Erjang语言。尽管结果令人印象深刻,但是鲜有开发者把Erlang的开发工作迁移到Java平台上。

现在,调用一下我们的函数吧!

3> useless:add(7,2).
9
4> useless:hello().
Hello, world!
ok
5> useless:greet_and_add_two(-3).
Hello, world!
-1
6> useless:not_a_real_function().
** exception error: undefined function useless:not_a_real_function/0

函数的表现符合预期:add/2对数字相加,hello/0输出“Hello,world!”,greet_and_add_two/1做了上面两件事。当然,你可能会问为什么hello/0在输出文本后返回了原子ok。这是因为Erlang中的函数和表达式必须要有返回值,虽然在其他语言中可能并不需要。正因为如此,io:format/1函数返回ok表示情况正常:没有错误发生。

第6行的调用抛出了一个异常,因为我们调用的函数并不存在于模块中。如果忘了把一个函数导出,那么当试图去调用它时,也会看到此类出错消息。

Erlang提供了许多编译选项,用来对一个模块的编译方式进行控制。你可以在Erlang文档中看到所有的编译选项。下面列出的是一些最常用的选项。

-debug_info

像调试器、代码覆盖率统计以及静态分析之类的Erlang工具都是使用模块中的调试信息来完成工作的。一般来说,建议这个编译选项一直开启。比起不开启这个选项所节省的那一点点字节码空间来说,你可能更需要这个选项所带来的好处。

-{outdir,Dir}

默认情况下,Erlang编译器会将生成的.beam文件放置到当前目录下。可以用这个选项指定编译文件的存放路径。

-export_all

这个选项会让编译器忽略文件中已定义的-export模块属性,把文件中的所有函数都导出。这个选项在测试和开发新代码时特别有用,但在产品代码中严禁使用。

-{d,Macro}和{d,Macro,Value}

这个选项定义了一个可以在模块中使用的宏,其中Macro是个原子。这个选项在单元测试中用得最多,因为它能确保模块中的测试函数只在明确需要时才会被创建和导出。如果元组中没有定义第三个元素,Value会被默认设置为true

在编译useless模块时,如果想使用编译选项,可以通过如下两种方式:

7> compile:file(useless, [debug_info, export_all]).
{ok,useless}
8> c(useless, [debug_info, export_all]).
{ok,useless}

还可以在模块内部通过模块属性来定义编译选项。要达到与前面第7行和第8行一样的编译效果,可以在模块中增加如下内容:

-compile([debug_info, export_all]).

注意

还有一个可以把Erlang代码编译成本地码的编译选项。并不是所有平台和操作系统中都能进行到本地码的编译,只能在支持这个特性的平台或者操作系统中进行,编译成本地码会让程序运行得更快(坊间传闻,大概能提速20%)。要编译成本地码,需要使用hipe模块,然后调用hipe:c(Module,OptionsList)来编译。也可以在shell中调用c(Module,[native]). 达到同样的效果。注意,这样编译出来的.beam文件就不能跨平台了。一般来讲,在提升CPU密集型操作的性能时,用hipe进行编译往往都是最后的选择。

Erlang的宏和C语言的#define语句类似,主要用来定义简短的函数和常量。在代码被编译成供VM使用的字节码之前,它们会被替换成所代表的文本表达式。这些宏主要用来避免四散在模块代码中的“魔数”。例如,如果你看到一段比较某个变量和一个硬编码数3 600的代码,就不清楚这个数字代表的是1 h(3 600 s)还是60 h(3 600 min),还是金钱数额之类的东西。然而,如果你看到的是?HOUR这样一个Erlang宏,那么立即就能明白是什么意思。更好的是,如果你最后把这个表示从秒(3 600)更改为毫秒(3 600 000),那么只需要更改这个宏定义,代码中所有使用该宏的地方都会自动得到更新。

Erlang中的宏是通过模块属性来定义的,如下所示:

-define(MACRO, some_value).

然后,你就可以在模块的任意函数中使用宏?MACRO了,这个宏在代码编译前会被替换成some_value。对于前面的小时例子,可以这样定义宏:

-define(HOUR, 3600).  % 单位是秒

“函数”宏的定义方法类似。下面是一个简单的函数宏,进行两个数的减法:

-define(sub(X,Y), X-Y).

要使用这个宏,像调用其他宏那样调用它就行了。例如,如果调用?sub(23,47),这个调用会被编译器替换成23-47

Erlang中有一些预定义的宏,下面列出了其中的一部分:

你还可以检测一下某个特定的宏是否已在代码中定义,并且根据这个结果有条件地定义其他宏。使用属性-ifdef(MACRO).、-else.-endif.能够完成上述逻辑,如下例所示:

-ifdef(DEBUGMODE).
-define(DEBUG(S), io:format("dbg: "++S)).
-else.
-define(DEBUG(S), ok).
-endif.

在代码中使用时,这个宏看起来像这样:?DEBUG("entering some function"),只有在编译这个模块时定义了DEBUGMODE宏,才会打印出信息。否则,会被替换成原子ok,什么都不做。

我们再来看一个例子,在这个例子中定义了一个只有在某个测试宏被定义时才会存在的测试函数:

-ifdef(TEST).
my_test_function() ->
    run_some_tests().
-endif.

然后,使用前面介绍的编译选项,我们可以选择是否定义DEBUGMODE或者TEST,就像这样:c(Module, [{d,'TEST'},{d,'DEBUGMODE'}]).

接下来,我们会编写一些功能更强大的函数,不会再编写一些无用的代码,不过在此之前,我们先来了解和模块有关的其他一些杂项知识,以后也许会用得上。

在本章前面曾经提到过,模块属性是描述模块自身的元数据。如果不能访问源代码,那么在哪里才能找到这些元数据呢?嗯,编译器已经帮我们搞定了——在编译一个模块时,编译器会提取出大部分模块属性并把它们(还有其他一些信息)保存在module_info/0函数中。

可以用如下方式查看useless模块的元数据:

9> useless:module_info().
[{exports,[{add,2},
             {hello,0},
             {greet_and_add_two,1},
             {module_info,0},
             {module_info,1}]},
 {imports,[]},
 {attributes,[{vsn,[174839656007867314473085021121413256129]}]},
 {compile,[{options,[]},
 {version,"4.8"},
 {time,{2013,2,13,2,56,32}},
 {source,"/home/ferd/learn-you-some-erlang/useless.erl"}]}]
10> useless:module_info(attributes).
[{vsn,[174839656007867314473085021121413256129]}]

上面的示例中展示了模块另外一个函数module_info/1,用它来获取一些特定信息。在上面的信息中,可以看到有导出的函数、引入的函数(本例中没有)、属性(自定义的元数据就在其中)以及一些编译选项和信息。如果你在module中增加了-author("An Erlang Champ").,那么它会出现在和vsn同样的区段中。

注意

vsn是一个自动生成的唯一值,用来区分代码的不同版本,生成这个值时注释内容除外。它通常用在代码热加载(对运行中的应用进行升级,无需停止它)以及某些发布管理工具中。也可以通过在模块中增加-vsn(VersionNumber)属性来自行指定一个vsn值。

对于产品代码来说,模块属性的作用很有限,不过通过一些小技巧,它们也能用来解决问题。例如,在我为本书写的测试脚本中,就用它们来标注那些在单元测试时需要更严格对待的函数。脚本会查看模块属性,找出被标记的函数,并显示出关于它们的警告信息。如果你对这个脚本感兴趣,可以从http://learnyousomeerlang.com/static/erlang/tester.erl找到它。

关于模块设计,还有一点要牢记:一定要避免环形依赖。如果模块B调用了模块A,那么模块A就不应当再去调用模块B。这样的依赖关系最终会导致代码难以维护。

事实上,如果代码依赖于太多的模块——即使它们之间并不构成环形依赖——也会让代码变得难以维护。你肯定不希望哪天半夜醒来,发现一个疯狂的软件工程师正试图戳瞎你的眼睛,就因为你写的代码太烂了。

好了,说教的话到此为止。在第3章中,我们将继续Erlang的探索之旅,学习与函数有关的内容。


相关图书

JUnit实战(第3版)
JUnit实战(第3版)
Kafka实战
Kafka实战
Rust实战
Rust实战
PyQt编程快速上手
PyQt编程快速上手
Elasticsearch数据搜索与分析实战
Elasticsearch数据搜索与分析实战
玩转Scratch少儿趣味编程
玩转Scratch少儿趣味编程

相关文章

相关课程