[关闭]
@khan-lau 2016-01-08T10:40:47.000000Z 字数 48030 阅读 3103

Erlang程序设计

Erlang


Erlang程序设计

Programming Erlang Software for a Concurrent World

                                        [瑞典]Joe armstrong 著
                                           赵东炜  金尹 译

                                           人民邮电出版社
                                               北 京

                                             内容提要

  本书是讲述下一代编程语言 Erlang 的权威著作, 主要涵盖顺序型编程 异常处理 编译和运行代码 并发编程 并发编程中的错误处理 分布式编程 多核编程等内容. 本书将帮助读者在消息传递的基础上构建分布式的并发系统, 免驱锁与互斥技术的羁绊, 是程序在多核 CPU 上高效运行. 本书讲述的各种设计方法和行为将成为设计容错与分布式系统中的利器. 

                                             版权声明  

  Copyright © 2007 armstrongonsoftware. Original English language edition, entitled Programming Erlang: Software for a Concurrent World.
  Simplified Chinese-language edition copyright © 2008 by Posts & Telecom Press. All rights reserved.
  本书简体中文版由 The Pragmatic Programmers, LLC  授权人民邮电出版社独家出版, 未经出版者书面许可,不得以任何方式复制或抄袭本书内容。
  版权所有,侵权必究。

  这个世界是并行的。
  如果希望将程序的行为设计得与真实世界物体的行为相一致。那么程序就应具有并发结构。
  使用专门为并发应用设计的语言, 开发变得极为简便。
  Erlang程序模拟了我们如何思考,如何交互。
                                                         —— Joe Armstrong

                                          推荐序 

  Erlang算不上是一种”大众流行”的程序设计语言,而且即使是Erlang的支持者, 大多数也对于Erlang成为“主流语言”并不持乐观态度。然而,自从2006年以来,Erlang语言确实在国内外一批精英程序员中暗流涌动, 光我所认识和听说的, 就有不少于一打技术高手像着了魔一样迷上了这种已经有二十多年历史的老牌语言。这是一件相当奇怪的事情。因为就年龄而言, Erlang 大约与Perl同年, 比C++年轻四岁, 长Java差不多十岁, 但Java早已经是工业主流语言, C++和Perl 甚至已经进入其生命周期的下降阶段。按理说, 一个被扔在角落里二十多年无人理睬的老家伙合理的命运就是坐以待毙, 沒想到Erlang却像是突然吃了返老迹童丹似的在二十多岑的”高龄”又火了一把, 不但对它感兴趣的人数量激增, 而且还成立了一些组织, 开发实施了一些非常有影响力的软件项目。这是怎么回事呢?

  根本原因在于Erlang天赋异禀恰好适应了计算环境变革的大趋势: CPU的多核化与云计算。 

  自2005年 C++ 标准委员会主席Herb Sutter在Dr. Dobb’s Journal 发布<免费的午餐已经结束>一 文以来, 人们已经确凿无疑地认识到, 如果未来不能有效地以并行化的软件并充分利用并行化的硬件资源, 我们的计算效率就会永远停滞在仅仅略高于当前的水平上,而不得动弹。因此, 未来的计算必然是并行的。Herb Sutter本人曾表示, 如果一个语言不能够以优雅可靠的方式处理并行计算的向題,那它就失去了在21世紀的生存权。”主流语言”当然不想真的丧失掉这个生存权,是纷纷以不同的方式解决井行计算的问题。就C/C++而言, 除了标准委员会致力于以标准库的方式来提高并行计算库之外, 标准化的OpenMP 和 MPI, 以及 Inter 的 Threading Building Blocks也都是可信赖的解決方案; Java在5.0版中引入了意又重大的concurrency库, 得到Java社区的一致推崇; 而微软更是采用了多种手段来应对这一问题: 先是在NET中引入APM, 随后又在Robotics Studio 中提供了CCR库, 最近又友布了Parrallel FX和MPLNET, 可谓不遗余力. 然而,送些手法都可以视为亡羊补牢 ,因为这些语言和基础设施在创造时都没有把并行化的问题放到优先的位置来考虑。与它们相反, Erlang从其构思的时候起, 就把“并行”放到了中心位置, 其语言机制和细节的设计无不从并行角度出发和考虑, 并且在长达二十年的发展完善中不断成熟。今天,Erlang可以说是为数不多的天然适应多核的可靠计算环境,这不能不说是一种历史的机缘。

  另一个可能更加迫切的变革, 就是云计算。Google的实践表明, 用廉价的服务器组成的服务器集群, 在运算能力、可靠性等方面能够达到价格昂贵的大型计算机的水准, 毫无疑问, 这是是大型、 超大型网站和网络应用梦寐以求的境界。然而, 要到达这个境界井不容易。目前一般的网站为了达成较好的好的可延展性和运行效率, 需要聘请有经验的架构师和系统管理人员, 手工配置网络服务端架构, 并且常备一个高水准的系统运维部门, 随时准备处理各种意外情况, 可以说, 虽然大多数 Web 企业只不过是想在这些基础设施上运行应用而已, 但仅仅为了让基础设施正常运转, 企业就必須投入巨大的资源和精力。現在甚至可以说, 这方面的能力成了大型和超大型网站的核心竞争力。这与操作系統成熟之前人们自己动手设置硬件井且编写驱动程序的情形类似——做应用的人要精通底层细节 。这种格局的不合理性一望便知, 而解決的思路也是一目了然 ———— 建立网络服务端计算的操作系统, 也就是类似Google已经建立起來的”云计算"那祥的平台。所谓"云计算",  指的是结果, 而当前的关键不是这个结果, 而是作为手段的“计算云”。计算云实际上就是控制大型网络服务器集群计算资源的操作系统, 它不但可以自动将计算任务井行化, 充分调动大型服务器集群的计算能力, 而且还可以自动应对大多数系统故障, 实现高水平的自主管理。计算云技术是网络计算时代的操作系統, 是绝对的核心技木, 也正因此, 很多赫赫有名的中外大型IT企业都在不惜投入巨资研发计算云 。包括我在內的很多人都相信, 云计算将不仅从根本上改变我们的计算环境, 而且将从根本上改变 IT 产业的盈利模式, 是真正几十年一遇的重大变革, 对于一些企业和技木人员来说是重大的历史机遇。恰恰在这个主题上, Erlang又具有先天的优势, 这当然也是归结于其与生俱来的并行计算能力, 使得开发计算用系统对于 Erlang来说格外轻松容易。現在 Erlang社区已经开发了一些在实践中被证明非常有效的云计算系统, 学习Erlang和这些系统是迅速进入这个领域并且提高水平的捷径。

  由此可见, Erlang虽然目前还不是主流语言,但是有可能会在未来一段时间发挥重要的作用, 因此, 对于那些愿意领略技术前沿风景的”先锋派”程序员来说, 了解和学习Eriang可能是非常有价值的投资。即使你未来不打算使用Erlang, 也非常有可能从Erlang的设计和Erlang社区的智慧中得到启发, 从而能够在其他语言的項目中更好地完成井行计算和云计算相关的实际和实现任务。再退一步说,就算只是从开启思路、全面认识计算本质和并行计算特性的角度出发,Erlang 也值得了解。所以我很希望这本中在中国程序员社区中不要遭到冷遇。

  本书是由Erlang创造者Joe Amstrong亲自执笔撰写的Erlang语言权威参考书,原作以轻松引导的方式帮助读者在实践中理解Erlang的深刻设计思路, 并掌握以Erlang开发并行程序的技术, 在技木图书中属于难得的佳作。 两位译者我都认识, 他们都是技木精湛而且思想深刻的”先锋派", 对Erlang有着极高的热情, 因此翻译质量相当高, 阅读起来流畅通顺, 为此书中译本增色不少。有兴趣的读者集中一段时间按图索骥, 完全有可能就此踏上理解Erlang、应用Erlang的大路。


                                孟岩
                CSDN 首席分析师兼<程序员>杂志技术主编
                              2008年10月


                                   译者序 


  另辟蹊径 ———— 从容面对容错 分布 并发 多核的挑战.

  作为一名程序员, 随着工作经验的增长, 如果足够幸运的话, 终有一日, 我们都会直面大型系统的挑战. 最初的手忙脚乱总是难免的, 经历过最初的迷茫之后, 你会惊讶的发现这是一个完全不同的”生态系统”.  要在这样的环境中生存, 我们的代码需要具备一些之前我们相当陌生或者闻所未闻的”生存技能”.  容错 分布 负载均衡, 这些词会频繁出现在搜索列表之中.  经历过几轮各种方案的轮番上阵之后, 我们会开始反思这一系列问题的来龙去脉, 重新审视整个系统架构, 寻找瓶颈所在. 你可能会和我一样, 最终将目光停留在那些之前被认为无懈可击的优美代码上. 开始琢磨: 究竟是什么让它们在新的环境中”水土不服”, 妨碍其更加有效的利用越来越膨胀的计算资源. 

  其实, 早在多年以前硬件领域上的革命就已经开始, 现在这个浪潮终于从高端应用波及常规计算领域—多核芯片已经量产, 单核芯片正在下线—这场革命正在我们的桌面上演. 时代已经改变, 程序员们再也不能继续稳坐家中装作什么事也没发生, 问题已经自己找上门来了. 由单核 CPU 频率提升带来软件自动加速的时代已经终止, 性能的免费大餐已经结束, “生态环境的变化”迫使代码也必须”同步进化”.  锁 同步 线程 信号量 这些之前只是在教科书中顺带提及的概念, 越来越多的出现在我们日常的编程中, 接踵而来的各类问题也开始折磨我们的神经. 死锁 竟态 越来越多的锁带来了越来越复杂的问题. 

  在多核 CPU 系统上, 程序的性能不增反降, 或者暴露出隐藏的错误?
  在更强的硬件平台上, 程序并发处理能力却没有得到提升, 瓶颈在哪里?
  在分布式计算网络中, 不得不对程序结构进行重大调整, 才能适应新的运行环境?
  在系统的关键应用上, 不得不为软件故障或代码升级, 进行代价高昂的停机维护?
    ……
  这一系列问题, 归根结底, 都是因为驻留技术体系在基本模型上存在着与并发计算相冲突的设计. 换句话说, 问题广泛的存在于我们所写的每一行代码中. 在大厦初具规模时, 却猛然发现每一块砖石都不牢固, 这听起来有一些耸人听闻, 但这种事并不罕见. 
  比如: x = x + n, 即使是这个再常见不过的语句, 也暗藏着烦恼的根源(你也可以把它称为共享内存陷阱). 从机器指令的角度来思考, 这个语句可能做了这么几件事(仅仅从概念上). 
  (1) mov ax, [bp+x];  //将寄存器 ax 赋值为变量 x 所指示的内存中的数据. 
  (2) mov bx, n;       //将寄存器 bx 赋值为 n. 
  (3) add ax, bx;      //将寄存器 ax 加 n. 
  (4) mov [bp+x], ax;  //将变量 x 所指示的内存赋值为寄存器 ax 中的数据. 
  理论上, 这是一个原子操作, 在单核 cpu 下情况也确实如此, 但在多核 cpu 下, 就完全不同了. 如果每个核心上都有一个正在执行上述代码的进程(也就是试图对这个代码执行并行计算), 问题就出现了. 这里的 x 十在两个进程之间共享内存的. 很明显, 在(1)到(4)之间, 需要某种机制来保证”某一时刻”只有一个进程正在执行, 否则就会破坏数据的完整性. 这也就意味着, 这也的代码无法充分利用多核 CPU 的运算能力. 也罢, 咱们不加速就是了, 但更糟糕的是, 为了保证不出错, 还需要引入锁的机制来避免数据被破坏. 现在的主流技术体系全都建立在共享内存的模型之上, 像 x = x + n 这也的代码几乎无处不在, 但在多核环境下, 每一处这也的代码(逻辑上或者事实上的) 都需要小心的处理锁, 更糟糕的是, 大量的锁彼此互相影响优惠导致更为复杂的问题, 这迫使程序员们在实现复杂的功能之余, 还要拿出极度的耐心和娴熟的技巧来处理好这些沉闷和易错的锁, 切不说是否可能, 但这至少也是一个极为繁重的额外负担. 

  Erlang 为了并发而生. 20多年前, 它的创建者们就已经意识到这一问题, 转而选择了一条与主流语言完全不同的路(还有为数不多的另外几种语言也是如此). 它采用的十消息模型, 进程之间并不共享任何数据, 因而也就完全避免了引入锁的必要. 对于多核系统而言, 完全无锁, 就意味着相同的代码在更多的 CPU 上会容易具有更高的性能, 而对于分布式系统, 则意味着尽可能的避免了顺序瓶颈, 可以把更多的机器无缝的加入到计算网络中来. 甩掉了锁的桎梏, 无疑是对程序员们的解放.  

  不仅如此,Erlang在编程模型上走得更远。它在语言级别提供了一系列的并发原语, 通过这些原语, 我们可以用远程 + 消息的模型来建模现实世界中多人协作的场景。一个进程表示一个人, 人与人之间并不存在任何共享的内存, 彼此之间的协作完全通过消息(说话、打手势、做表情, 等等)交互采完成。这正是我们每一个人生而知之的并发模式! 软件模拟现实世界协作和交互的场景——这也就是所谓的COP(面向并发编程)思想。

  在错误处理上, Erlang也有与众不同的设计决策,  这使得实现”容错系统"不再遥不可及。 COP假设进程难免会出错 ———— 不像其他某些语言一样假设程序不会出错。它假设程序随时可能会出错, 如果发生出错的情况, 则不要尝试自行处理, 而是直接退出, 交给更高级的进程来对这种情况进行处理。通过引入”速错”和”进程監控"的概念,  我们将错误分层, 井由更高层的进程来妥善处理(比如, 重启进程, 或重启一系列进程)。有了这样的概念作为支撑,  构造”容错系统”就会变得易如反掌。在这样的系統之下,软件错误不会导致整个系统的瘫痪, 发现错误也无需停机就可直接更新代码, 在配置了备份硬件之后,硬件的错误也不会影响服务的正常运行。这么做的结果相当惊人, 使用Erlang的电信关键产品,达到了传说中的999999999%可用性(即9个9的最高可用性标准)。

  Erlang采用虚拟机技术实现, 用它编写的程序可以不经修改直接运行在从手机到大型机几乎所有的平台。这是一项有着20多年所史的成熟技木, 有着相当多的成熟库(OTP)和开源软件, 这些资产使得它有极高的实用价值。Erlang本身也是开源软件, 这扫清了对于语言本身生命力的疑惑。Erlang还是一个充满活力的语言, 在它的社区, 常常能够见到Joe Armstrong等语言的创建者在回答问題, 这一点尤其宝贵。在熟悉了Erlang的思维方法和社区之后, 很多人都发出了相见恨晚的感概。

  虽说对于并发而言, Erlang确实是非常好的选择, 但这么多年以来, 业界对于并发预料之中的增长却一直没有真正发生。此前, 这类应用更多的局限在一些相对高端的领域, 而Erlang身上浓厚的电信背景, 又使得第一眼看来它似乎只适用于电信行业(实际情況远非如此)。长期以来Erlang的使用群体仅局限在一个狭小的技术圈子之内, 它处于”非主流语言”的边缘位置已经很久了。这种情况直到最近才有所改观, 最近两年, Google的成功使得其引为核心的大规模分布式应用模式广为人知, 而多核CPU进入桌面也迫使”工业主流”开始认真对待并发计算。直到此时, 解决这类问题最为成熟的Erlang技术, 才因为其难以忽视的优势而引起人们的广泛关注。

  从历史的眼光来看, 在计算机语言的荣誉堂内, 上演着一代又一代程序设计语言的繁荣与更替, 潮来潮往让人难以捉摸。这与其说是技木, 还不如说是时尚。对于Erlang这种有些怪异的小众语言来说, 是否真的会成为“下一个Java”? 实难预测, 而且也不重要。但是有一点已经毫无疑问, 那就是“下一代语言”至少也要像Erlang一样, 处理好与并发相关的一系列问题(或者做得更好)。也许将来的X++ (或X#)语言在吸收了它的精髓之后, 又会成为新的工业主流语言。 但即便如此,先跟随本书作者开辟的小径信步浏览这些饶有趣味的问题肯定也会大有帮助。

  对于想要学习Erlang的读者,虽说语言本身相当简单, 但想要运用自如也有一些难度。比如, 在适应COP之后会变得非常自然, 但对于有OOP背景的程序员而言, 从固有的思维习惯转换到 COP和FP上(主要是和自己的思维惯性较劲)需要有一个过程。此外OTP和其他Erlang社区多年积累的财富 (这些好比JDK、EJB之于Java)也需要一些时间才能被充分地理解和吸收。但这些有价值的资料大多零星地散落于邮件列表和独立的文档之中, 给学习造成了很多不必要的麻烦。 现在好了, 有了Erlang 创建者Joe Armstrong为我们撰写”官方教程", 这些问题都已经迎刃而解。

  一般而言, 由语言的创建者亲自撰写的教程, 常常都是杰作。在翻译的过程中,译者也常常会发出这种赞叹。在本书中, Joe Armstrong不仅全面地讲述了Erlang语言本身, 还详细交代了这些语言特性的来龙去脉。除了掌握语言本身之外,能有幸窥见大师精微思辨的轨迹, 也是难得的机缘。书中的例子, 还会将你为之惊异Erlang特性一一解密。通常是从一个不起眼的小问题开始, 从宏观分析到围观实现, 层层深入细细道来。问题是什么? 要如何建模, 该怎么重构, 各个版本之同的精微演化全然呈現, 但这些微小的改造,最终演化出了那些让人惊喜的特性, 整个过程可谓相当精彩。

  本书由Erlang中文社区(eriangchina.org)组织翻译。其中,第1章到第14章由金尹翻译,第15章到附录F由赵东炜翻译, 全书有赵东炜统稿润色和审校。
  由于时间仓促,加之译者水平有限,译文难免会有不足之处,欢迎读者批评指正。


                                              赵东炜
                                         2008年3月于北京

目录


第1章 引言

  哦, 不! 别再来一种编程语言了! 我一定要在学另外一种吗? 现在的这些难道还不够吗? 我能理解你的反应. 程序语言浩如烟海, 在学一种的理由何在?
  学习 Erlang 的理由, 可列出如下5条
  1. 希望编写能在多核计算机上运行更快的程序.
  2. 希望编写不停机即可修改的可容错性程序.
  3. 希望尝试传闻中的"函数式语言"是否切实可行
  4. 希望使用一种语言, 它既可以在大规模工业产品中经过实战检验, 又不乏优秀的类库与活跃的社区.
  5. 不希望在冗长繁琐的代码中耗费时间

  我们能如愿以偿吗? 在20.3节中, 我们会看到一些能在32核计算机中以线性增速运行的程序. 在第18章中, 我们将会关注如何构造可以经历数年全天候运行的高可靠性系统. 在16.1节中, 我们还将讨论编写服务器的技术, 这些服务器可以在不停机的情况下更新软件.
  很多时候, 我们会不断的话要函数式语言的各种长处. 函数式语言禁止代码具有"副作用", 副作用与并发水火不容. 你要么编写有副作用的顺序代码, 要么编写无副作用的并发代码. 在这两者之间你必须选择, 没有中间情况.
  Erlang 的并发特性原子语言本身, 而非操作系统. 它把现实世界模拟成一系列的进程, 其间仅靠交互消息进行互动, 由此 Erlang 简化了并行编程. 在 Erlang 世界中, 存在并行进程但是没有锁, 没有同步方法, 也不存在共享内存污染的可能, 因为 Erlang 根本没有共享内存.
  Erlang 程序可以由几百万个超轻量级的进程组成. 这些进程可以运行在单处理器 多核处理器或者处理器网络上.

1.1 路线图

  第2章 让你能对 Erlang 快速起步
  第3章 是有关顺序型编程的第1章. 它介绍了模式匹配和非破坏性赋值的概念.
  第4章 是异常处理. 程序总免不了出错. 该章讲述了 Erlang 顺序型编程中的错误侦测和处理.
  第5章 是有关顺序型编程的第2章, 它从一些高级的主题开始, 涵盖了顺序型编程的其他所有细节.
  第6章 主要讲述了编译和运行程序的几种不同方法.
  第7章 开始新话题. 这是一个非技术型章节, 主要讨论两个问题: "这种编程方式背后的思想是什么" 以及 "如何用 Erlang 的视角来看待世界".
  第8章 主要讲述并发性. 如何创建 Erlang 中的并行进程? 如何处理进程间通信? 创建一个并行进程有多快?
  第9章 讨论了并行程序中的错误. 当一个进程错误退出时会发生什么? 如何检查进程错误, 又该如何处理?
  第10章 开始讲述分布式编程. 在这一章我们会写几个小型分布式程序, 来展示如何在 Erlang 集群节点或者使用基于套接字分布模型的独立主机上运行它们.
  第11章 是一个纯应用的章节. 我们会把并发和基于套接字分布的主题与我们第一个不平凡的应用精力联系起来, 构造一个 mini 的类 IRC 客户/服务器程序.
  第12章 介绍如何将 Erlang 与外部程序语言衔接起来.
  第13章 给出了几个与文件编程有关的样例.
  第14章 展现如何使用套接字进行编程. 我们会看到如何在 Erlang 中构建顺序和并行的服务器. 该章的最后给出了第2个颇具规模的应用: SHOUTcast 服务器. 这是一个流媒体服务器, 它可以使用 SHOUTcast 协议来流化 MP3数据.
  第15章 描述了 ets 和 dets 两个底层模块. ets 模块主要用于快速 可更改 内存散列表操作, dets 则专门针对低级磁盘存储.
  第16章 专门介绍 OTP. OTP 是一批 Erlang 库和操作过程的总称, 用来构建具有工业规模的 Erlang 应用. 该章节介绍了行为(behavior, 一种 OTP 的核心概念)思想. 通过行为我们可以着眼于模块的功能性部分, 而让框架去处理问题的非功能性部分. 比如说, 框架关心应用的容错和可伸缩性, 而行为的回调函数则几种处理问题的细节. 该章首先概要论述了如何构建你自己的行为, 然后进一步的深入描述作为 Erlang 标准库一部分的 gen_server 行为.
  第17章 讨论了 Erlang 数据库管理系统(DBMS)Mnesia. Mnesia 是一个内建的集成数据库管理系统, 它极为快速且拥有软实时的响应速度. 可以通过配置来让它在数个分散的物理节点上复制数据, 从而提供容错功能.
  第18章 再次讨论 OTP. 它涵盖了拼接 OTP 应用所涉及的各个实践性层面. 实际应用程序中充斥了大量零散的细节, 它们必须以一种相互协调的方式来启动和关闭. 如果它们自身或者它们的子模块崩溃, 就必须重新启动. 我们需要依靠错误日志来找出在崩溃前后到底发生了什么. 这一章讲述了打造一个真正成熟的 OTP 应用程序所需的实质性细节.
  第19章 简要介绍了为什么说 Erlang 是为多核计算机编程量身定做的. 我们将概略的讨论共享内存并发和消息传递并发, 并探讨为何我们坚信无可变状态且支持并发的语言是契合多核编程的理想选择.
  第20章 讲述多核编程. 我们会讨论确保在多核计算机上使得 Erlang程序高效运行的各种技术. 为在多核计算机上加速顺序型程序, 我们会引入一系列与之相关的抽象原则. 最后我们会对优化的效果执行一些测量, 然后开发第3个主要程序: 一个全文检索引擎. 为此我们会首先实现 mapreduce 函数, 这是一个高阶函数, 可以对一群处理元素进行并行化计算.
  附录A 讲述了用于撰写 Erlang 函数文档的类型系统
  附录B 讲述了如何在 Windows 系统上建立 Erlang(以及如何在所有操作系统上配置 Emacs).
  附录C 列出了 Erlang 资源列表
  附录D 讲述了 lib_chan, 一个用于编写基于套接字分布的库.
  附录E 关注于代码的分析 优化 调试和跟踪
  附录F 简要的介绍了 Erlang 标准库中最常用的模块
  

1.2 正式起航

  从前, 一名程序员偶然读到了一本古怪的程序语言图书. 相等其实不是相等, 变量实际上不能改变, 它的语法一切都那么陌生. 更糟糕的是, 它甚至都不是面向对象的. 这些程序, 呃, 实在是太另类了......
  另类的不仅仅是程序, 编程的教学步骤也特立独行. 它的作者一直喋喋不休的教授并发 分布和容错, 不断的唠叨着一种叫 COP(Concurrency Oriented Programming, 面向并发编程)的方法 -----管它叫什么呢.
  不过有些例子看上去很好玩. 那天夜里, 这个程序员注视着那个聊天程序的小例子. 它是多么的小巧可爱而又通俗易懂, 只是那些语法有那么一丁点儿古怪, 但它确实简单到不能再简单了.
  开始编程并不困难, 用不了几行代码. 文件共享和加密通信便跃然纸上. 于是这个程序员开始敲起了他的键盘......
  
  

这是一本什么样的书

  这是一本讲述并发的书, 也是讲述分布式的书, 既是一本讲述容错的书, 也是讲述函数编程的书. 这本书会帮助你在消息传递的基础上构建分布式的并发系统, 免去锁与互斥技术的羁绊. 它会让你的程序在多核 CPU 上风驰电掣, 亦会展示分布式互动程序的所有设计蓝图. 这本书传授的各种设计方法和行为将成为设计容错与分布式系统的利器. 它所蕴含的模型和并发思想, 及从模型映射到代码的过程, 则被成为面向并发编程.
  我独乐撰此书, 亦望众乐读此书.
  幸望诸君, 学而时习, 乐其乐也.

1.3 致谢

  在撰写本书时, 很多人提供了帮助, 我想在这里感谢所有人.
  首先感谢的是我的编辑 Dave Thomas. Dave 一直以来都在教导我写作, 用没玩没了的问题来轰炸我. 为什么这样? 为什么那样? 我开始写书时, Dave 认为我的写作风格近似于"站在石头上布道". 他说:"我希望你是在和读者交流, 而不是布道. " 这本书在这方面大有改观. 谢谢 Dave.
  接下来感谢的是我身后的一群语言专家组成的智囊团. 他们协助我决定内容取舍, 帮我澄清一些难以解释的小问题. 在这里一并感谢他们(排名不分先后): Bjom Gustavsson, Robert Virding, Kostis Sagonas, Kenneth Lundin, Richard Carlsson和 U1f Wiger.
  感谢 Claes Vikstrom, 他提供了关于 Mnesia 很有价值的建议, 感谢 Rickard Green 在 SMP Erlang 上的帮忙, 也为在全文检索中使用的词干分析算法感谢 Hans Nilsson.
  Sean Hinde 和 U1f Wiger 帮我弄懂了如何使用不同的 OTP 内部构件, Serge Aleynikov 向我解释活动套接字, 我才能清楚的了解它们.
  Helen Taylor(我的妻子) 帮我校对了好几章, 还要感谢她那数百杯香茶. 它们总在我最需要的时候出现在我手边. 更重要的是, 她忍受了我长达7个月之久的忘我工作. 感谢 Thomas 和 Claire, 感谢 Bach, Handel, Zorro, Daisy 和 Doris(5只可爱的小家伙), 抚摸它们的时候为我欢叫, 帮我保持清醒, 为我找到正确的方向.
  最后, 要感谢所有填写勘误表的初稿阅读者. 我对他们既恨又爱. 当第一版草稿释出时, 我没有料到他们可以在两天内读完, 他们的批注把每一页改得体无完肤. 但这个过程最终催生了一本比我预想中还要优秀的书. 当一堆人对我说"我看不到这页"时(发生过不止一次), 我就被迫回头审视他们关心的问题, 然后推倒重来. 感谢所有人给予的帮助.
  
                                        Joe Armstrong
                                        2007年5月


第2章 入门

2.1 概览

  同任何其他学习经历一样, 在掌握 Erlang 的过程中你也将会经历若干个阶段. 下面就来看看, 这本书里会经历那些阶段, 而在这些阶段之中我们又能学到什么.

2.1.1 阶段1: 茫然无绪

  作为一个初学者, 你能学到如何启动系统, 在 shell 里运行命令, 编译简单的程序, 然后逐渐的熟悉 Erlang(Erlang 是一门小巧的语言, 这用不了多久).
  
  让我们再细化一下. 作为一个初学者, 需要做下面这些事情:
    . 确保你计算机上的 Erlang 系统能正常工作.
    . 学习启动和关闭 Erlang shell.
    . 了解如何在 Erlang shell 里输入表达式, 对其求值, 并且弄懂返回结果的意思
    . 了解如何在你惯用的文本编辑器中创建和修改程序.
    . 练习一下如何在 Erlang shell 中编译和运行程序.

2.1.1 阶段2: 初窥门径

  到这个阶段你已经具备了初步的知识来运用这个语言. 如果继续深入学习语言而碰到难点, 正好可以运用这些背景知识去弄懂第5章.
  
  在这个阶段中我们已经熟悉 Erlang, 因此可以转到更多有趣的主题.
    . 你会发掘出 Erlang shell 更多的高级功能. 与你初次接触它时所学的那些功能相比, Erlang shell 可以做到的事情远远超乎我们想象. (比如说, 你可以重新调用之前录入的表达式并对它们进行编辑, 这个话题包含在 6.5节中.)
    . 你会开始逐步学习各种库(Erlang 中称为模块). 我所撰写的大多数程序中都会用到5个模块: lists io file dictgen_tcp, 而且, 在本书中我们会频繁的使用这些模块.
    . 随着代码规模的逐渐膨胀, 你有必要学会如何去自动编译和执行它们. make 是进行这些工作的必备工具. 我们会学到如何运用 makefile 来控制编译和执行, 这个话题包含在6.4节中.
    . Erlang 编程更高的层次是能灵活的运用一批扩展库----OTP(Open Telecom Platform 开放电信平台). 随着 Erlang 使用经验的积累, 你会意识到, 通晓 OTP 将能事半功倍. 毕竟, 如果有人已经写好了所需的功能, 又何必另起炉灶? 我们会学习 OTP 主要的行为(behavior), 特别是 gen_server, 这个话题包含在16.2节中.
    . Erlang 主要用来编写分布式的应用程序, 这个阶段正式开始尝试的好时机. 你可以从第10章的样例程序开始, 而后尽情的扩展它们.

2.1.2 阶段2.5: 观其大略, 不求甚解

  第一次通读本书时, 不必苛求每一章都能学得面面俱到.
  你先前可能接触过很多语言, Erlang 与它们中的大部分都不相同, 它是一种并发编程语言, 这使得它可以和分布式 多核/ SMP(Symmetric multiprocessing, 对称多处理器)编程结合得天衣无缝. 大多数 Erlang 程序在多核或 SMP 机器上会运行得更快.
  用 Erlang 编程总是与一种编程范式形影不离, 我称之为 COP(面向并发编程).
  使用 COP, 你可以分解问题, 识别出它本身的并发模式. 这正是面向并发编程实质性的第一步.

2.1.4 阶段3: 运用自如

  至此, 你已然可以灵活的运用这一语言, 编写一些有用的分布式程序. 但要达到游刃有余的境界, 你还需要学习更多知识.
    . Mnesia. Erlang 的发行版包含一个内置的, 完整的 快速的 可复制的数据库 ----Mnesia. 它原本用于那些性能和容错都至关重要的电信应用, 现在也广泛的用于很多非电信场合.
    . 如何和其他语言编写的代码进行接口, 如何使用内联的驱动, 这些内容包含在12.4节中.
    . 利用各种 OTP 行为(behavior)来构建监控树(supervision tree)和启动脚本(start script)等, 这些内容包含在第18章中.
    . 如何在多核计算机上运行和优化你的程序, 这些内容包含在第20章中.
  

2.1.5 重中之重

  在通读此书时, 有一条原则你必须铭记于心: 编程乐在其中. 就我个人看来, 与传统顺序型应用程序相比, 编写诸如聊天 即时消息这样的分布式程序有着更多的乐趣. 如果说在一台计算机上你能做的事情往往会被其能力限制, 那么, 构建于网络之上的计算机集群则是海阔凭鱼跃, 天高任鸟飞的另一番天地. Erlang 提供了一个非常理想的环境, 在它之中你可以尝试网络化的应用程序, 构建产品级的系统.
  为了让你能再这些领域里大展拳脚, 我在那些技术性的章节里揉和了一些真实世界中的应用. 毫无疑问, 你可以将这些应用作为进一步实践的基石. 接受并修改它们, 把它们应用到超乎我所能想象的领域, 而卧将会为此感到万分欣慰.

2.2 Erlang 安装

  在动手做任何事之前, 你得先确保系统上装有一个可运行的 Erlang 版本. 转到命令行提示符界面, 输入 erl:

  1.   $ erl
  2.   Erlang (BEAM) emulator version 5.5.2 [source] ... [kernel-poll:false]
  3.   Eshell V5.5.2 (abort with ^G)
  4.   1>

  在 Windows 系统中, 必须先安装 Erlang, 并修改环境变量 PATH 以包含 Erlang 所在的目录, 之后, erl 命令才能正确执行. 如果你是按照标准方式安装的程序, 还可以通过菜单开始-->所有程序--> Erlang OTP 来启动 Erlang. 在附录B中, 我们会谈到如何配置 Erlang 以便让它与 MinGW 和 MSYS 一起运行.
  

  说明
  
  我只会偶尔展示("Erlang (BEAM) ... (abort with ^G)")这个提示. 这个提示只是在你需要提交一个 Erlang 的 bug 时有用. 我们在这里展示它仅仅是为了让你安心, 不会因为你看到它却不理解而感到担心. 在大多数例子中它们都不再出现, 除非是某些特定情况

  如果看到上述的 shell 提示, 那就表示你已经成功的安装了 Erlang. 在提示符>后面, 按下 Ctrl+G, 输入字母 Q, 然后回车就可以退出 shell. 你现在可以跳过下面的内容直接阅读2.3节.
  如若不然, 你得到了 erl 是一个未知命令的错误信息, 就需要先在你的机器上安装好 Erlang. 这通常意味着你需要做一个决定 --是用一个预先做好的二进制发布版呢, 还是一个打包好的发布版(仅支持 OS X)? 或者从源代码中编译 Erlang? 此外还可以使用 CEAN(Comprehensive Erlang Archive Network, Erlang 综合档案网络).
  

2.2.1 二进制发布版

  只有 Windows 和基于 Linux 的操作系统才可以使用 Erlang 的二进制发布版. 二进制发布版的具体安装步骤要视不同系统而定. 因此, 我们会逐个将这些系统上的安装方法介绍一遍.
    1. Windows
    你可以从http://www.erlang.org/download.html中找到各个版本的下载列表. 选择最新版本的条目, 单击 Windows 二进制文件的链接, 它指向你下载的 Windows 可执行文件, 单击链接, 按照提示执行安装步骤. 它是标准的 Windows 安装程序, 因此你无须担心任何问题.
    2. Linux
    只有基于 Debian 的系统才有相应的二进制包. 在基于 Debian 的系统中, 输入如下命令:

  1. $ apt-get install erlang

    3. 在 Mac OS X 上的安装
    Mac 用户可以使用 MacPorts 系统来安装 Erlang 的预打包版本, 也可以通过编译源码来进行安装. 相比之下, 使用 MacPorts 极为方便, 而且它可以一直保持软件更新. 但 MacPorts 的 Erlang 发布总要落后几版. 在本书撰写的最初一段时间里, MacPorts 的 Erlang 落后当时的官方发布大约两个版本. 这种情况下, 我建议你还是咬咬牙, 按照下一节的描述, 啃下源码编译这块硬骨头. 为了完成这项工作, 你需要确保机器上已经安装好了开发工具(它们一般都在随机附带的软件 DVD 上).
    

2.2.2 从源码创建 Erlang

  除了二进制安装外, 另一个方法就是从源代码来编译 Erlang, Windows 上每一个新的发布版都附带了完整的二进制代码和源代码, 所以对于 Windows 来说, 这种方式没有什么特别的好处. 但对于 Mac 和 Linux 平台就不同了, 在新的 Erlang 发布版和可用的二进制安装包之间总存在着一些延时. 对于所有的类 Unix 操作系统, 从源代码安装的步骤都是相同的.
    (1) 获取最新的 Erlang 源码, 代码会打包在一个文件之中, 它的名字类似 otp_src_R11B-4.tar.gz(意思是这个文件包含了 Erlang 第11版的第4个维护发布版).
    (2) 按照下面的步骤进行解包 配置 编译 以及安装

  1. $ tar -xzf otp_src_R11B-4.tar.gz
  2. $ cd otp_src_R11B-4
  3. $ ./configure
  4. $ make
  5. $ sudo make install
  说明
  
  在编译系统之前, 你可以使用命令 ./configure --help 来查看可用的配置选项

2.2.3 使用 CEAN

  CEAN 视图将所有主流的 Erlang 应用集中起来, 统一在一个通用的安装界面上. CEAN 的特点在于不仅可以管理基本的 Erlang 系统, 还能管理大量使用 Erlang 编写的程序包. 这也就意味着你自己编写的程序包也能像最基本的 Erlang 系统那样得到持续的更新和维护.
  CEAN 针对不同的操作系统和处理器架构预编译了各种不同的二进制版本. 想通过 CEAN 来安装系统. 那么就前往http://cean.process-one.net/download/ , 按照上面的指示步骤安装[注意, 有些读者反映 CEAN 可能不会安装 Erlang 编译器, 如果你恰好碰上了这个问题, 那么, 启动 Erlang shell 并输入命令cean:install(compiler). 这样就能装好编译器了].

2.3 本书代码

  我们所展示的大多数代码片段都来自完整的, 可运行的样例程序. 这些程序你也能通过网络下载(可从http://pragmaticprogrammer.com/titles/jaerlang/code.html下载). 为了便于查找, 如果本书的代码清单也包含在下载文件中, 那么在该代码片段上会有一个条目(就像下面的这个样子):

  1. shop1.erl
  2. -module(shop1).
  3. total([{What, N}|T]) -> shop:cost(What) * N + total(T);
  4. total([]) -> 0.

  这个条目表明了代码在下载点中的路径. 如果你读的是本书的 PDF 版, 而且你的 PDF 阅读器也支持超链接, 那么在点击这个条目之后, 相关代码就会在浏览器中显示出来

2.4 启动 Shell

  现在我们正式开始. shell 是一个交互工具, 我们常用它来完成与 Erlang 的互动. 启动 shell 之后, 我们可以输入表达式, 然后 shell 会返回这些表达式的值.
  如果你已经装好了 Erlang(参考2.2节所述), 那么 Erlang shell --erl 也就同时安装好了. 要运行它, 请开启一个传统的操作系统命令行界面(Windows 上市 cmd, 而类 Unix 系统上则是 bash 这样的 shell 程序). 在命令行提示符下, 输入 erl 来启动 Erlang shell:

  1. $ erl //1
  2.   Erlang (BEAM) emulator version 5.5.2 [source] ... [kernel-poll:false]
  3.   Eshell V5.5.2 (abort with ^G)
  4.   1> % I'm going to enter some expressions in the shell .. //2
  5.   1> 20 + 30. //3
  6.   50
  7.   2> //4

  让我们看看刚才做的动作.
  1 这是在 Unix 名下启动 Erlang shell. shell 返回了一个提示, 告诉你正在运行的是哪个版本的 Erlang.
  2 shell 显示了提示符1>, 然后我们输入了一串注释, 百分号(%)表示一个注释的开始. 从百分号开始到这行结束的所有文本都被看做是注释, 它会被 shell 和 Erlang 编译器忽略.
  3 由于我们没有输入一个完整的命令, 所以 shell 重复显示1> . 在此时, 我们输入表达式20+30, 后面紧跟一个句点和一个回车(初学者往往会忘记输入句点. 没有句点, Erlang 就认为我没还没有输完整个表达式, 我们也不会看到显示的结果).
  4 shell 打印出另外一个提示符, 这次显示的命令数为2(因为命令数会随着每次新命令的输入而增加).
  
  有没有在你的系统上试着运行 shell, 如果没有. 那么现在就放下书去试一下. 可能你会认为自己对于前面的内容了如指掌, 但是如果一味的死读书而不去加以实践, 那就会眼高手低, 不能将知识融入实践. 如果将编程比作运动的话, 那么它绝对不是表演项目, 而是竞赛项目. 因此, 你也应该像一个勤奋的运动员那样, 不断的区练习.
  照着书上的样子, 输入例子中的表达式, 用这些例子做些试验, 对它们做些修改. 如果出错了. 那就停下来, 研究一下出错的原因. 即便是经验老道的 Erlang 程序员也会花上大把时间去和 shell 打交道.
  随着经验的累积, 你会发现 shell 其实是一个非常强大的工具. 之前录入 shell 的命令可以用 Ctrl+p 和 Ctrl+n 来找回, 也能用类似 Emacs 的编辑命令来编辑它们. 这些话题我们会留到6.5节中继续讨论. 更妙的是, 当开始编写分布式的程序时, 一个集群内会有许多正在运行着 Erlang 系统的节点, 你将发现可以将 shell 随意的附着到它们中的任何一个上. 你甚至可以用安全 shell(ssh)向一个运行着 Erlang 系统的原创计算机发起一个直接连接. 通过这种方法, 在 Erlang 节点集群中, 你能与其中的任何一个节点上的任意一个程序打交道.

  警告
  
  本书之中, 也不是所有的东西都能输入 shell. 特别要注意的是, 你不能往 shell 里输入 Erlang 文件当中的代码. .erl 文件中的句法形式不是表达式, 它不能被 shell 所接受. shell 仅仅能够对 Erlang 表达式求值, 除此以外的其他事情, 它都做不了. 另外需要特别注意的是, 你不能在 shell 中输入模块注释, 这些注释一连字号开始(比如 -module  -export 等). 

  本章剩余的部分依然会采用这种"与 Erlang shell 进行数次短小对话"的形式. 为了不破坏行文, 很多时候, 我不会解释全部细节, 这些内容会在5.4节中补充.
  
  

2.5 简单的整数运算

  先计算几个算数表达式:

  1. 1> 2 + 3 * 4.
  2. 14
  3. 2> (2 + 3) * 4.
  4. 20
  要点
  
  你会注意到这段对话以命令行数1开始(也就是说 shell 打印了1). 这表明我们开启了一个新的 shell, 如若不然, 则意味着 shell 继续上一个样例的会话. 为了正确的再现本书中的样例, 你不许在看到对话从1>开始时重新启动 shell, 反之则不必. 

  shell 没有任何响应
  你输入命令后, 如果 shell 不响应, 那么大概就是因为你忘记了用句点加回车[或者叫做点-空行(dot-whitespace)]来结束命令.
  另一个可能会出错的地方是, 你用单引号或者双引号开始一段文字, 但是没有用匹配的引号去结束它. 
  如果上述的任何一种情况发生, 最好的办法就是把遗漏的符号补上.
  如果已经到了无可挽回的地步, 系统不再有任何响应, 那么就按 Ctrl+C (Windows 上是 Ctrl + Break). 你会看到下面这行提示:
  BREAK: (a)bort   (c)ontinue   (p)roc info   (i)nfo   (l)oaded   (v)ersion   (k)ill   (D)b-tables   (d)istribution
  这个时候, 按 A 就可以中止当前 Erlang 会话. 

  进阶
  
  你可以开启关闭数个 shell, 参见6.7节中的详细说明

  说明
  
  照着书中的例子依葫芦画瓢, 不失为一种绝佳的学习方式. 如果你整准备这么做, 最好先浏览一下6.5节

  此外, Erlang 遵守标准算术表达式的法则, 因此, 2+3*4应该是2+(3*4)而不是(2+3)*4.
  Erlang 采用不定长的整数来进行整数的算术演算. 在 Erlang 中, 整数运算没有误差, 因此你不用担心运算溢出, 也不用为了一个固定字长容纳不下一个大整数而伤脑筋.

  变量记号
  
  我们经常需要讨论特定变量的值, 为此我会用 Var --> Value 这种记号, 例如, A --> 42就表示变量 A 的值为42. 如果有多个变量的话, 我会这样写{A --> 42, B --> true ...}, 意思是 A 为42, B 为 true, 以此类推. 

  为何不尝试一下这个特性? 超大整数的计算一定会给你的朋友们留下深刻的印象

  1. 3> 123456789 * 987654321 * 112233445566778899 * 998877665544332211.
  2. 13669560260321809985966198898925761696613427909935341

  你还可以用很多种不同的方式来输入整数, 下面这个例子就是用了16进制和32进制记号:

  1. 4> 16#cafe * 32#sugar.
  2. 1577682511434

2.6 变量

  怎样才能吧一个命令的结果保存起来, 以供后面使用呢? 这就是变量的职责所在. 下面是一个例子

  1. 1> X = 123456789.
  2. 123456789

  这是什么意思呢? 首先, 我向变量 X 赋了一个值, 然后 shell 打印出了变量的值.

  说明
  
  所有变量都必须以大写字母开头.

  如果你要查看一个变量的值, 那么只需要输入变量名字即可.

  1. 2> X.
  2. 123456789

  现在 X 有了值, 你可以这样使用它:

  1. 3> X*X*X*X
  2. 232305722798259244150093798251441
  代数式的单一赋值
  
  在我们上初中时, 数学老师就告诉过我 "如果在同一个方程的不同地方都有 X, 那么这些 X 指的都是同一个东西". 解方程就靠它了, 比如, 我们有 X+Y=10和 X-Y=2, 那么根据这两个方程可得: X=6, Y=4.
  但是当我学习第一门程序语言时, 缺看到老师在黑板上写出这样的式子:
  X=X+1
  大家都蒙了, "这是个无解的等式". 但老师却说, 我们错了, 我们应该忘了在数学课上学到的东西. X 不是一个数学变量, 它就像一个鸟笼......
  而在 Erlang 中, 变量恢复了它在数学中的涵义. 当我们把一个变量和值关联在一起时, 你其实就做出了一项断言, 也就是对一个事实的陈述, 这个变量的值是多少, 仅此而已. 

  然而, 你要是想要尝试着给变量 X 赋上一个另外的值, 那么系统就会无情的抛给你一个错误信息.

  1. 4> x = 1234.
  2. =ERROR REPORT==== 11-Sep_2006::20:32:49 ===
  3. Error in process <0.31.0> with exit value:
  4. {{badmatch, 1234}, [{erl_eval, expr, 3}]}
  5. ** exited: {badmatch, 1234}, [{erl_eval, expr, 3}]} **

  这到底是怎么回事? 嗯, 要解释它, 我们需要破除对于表达式 X=1234的两种错误想法, 如下:
    首先, X 不是一个变量, 至少不是你在 Java 或者 C 当中碰到的那种变量.
    其次, =不是一个赋值操作符.
  对于 Erlang 新手来说, 这可能是最让人犯晕的地方之一, 为此, 很有必要来深入的探讨一下这个问题.

2.6.1 变量不变

  Erlang 的变量是一个单一赋值变量. 恰如其名, 单一赋值变量的值只能一次性给定. 一个变量一旦被赋值, 你想再次改变它, 就会得到一个错误(实际上, 我们刚刚得到的是一个匹配失败的错误). 一个变量如果含有一个被赋予的值, 就称为绑定变量, 否则, 则被称作自由变量. 一开始, 所有变量都是自由的.
  当 Erlang 遇上语句 X=1234时, 它就将值1234绑定到 X. 而在被绑定之前, X 可以接受任何值, 它就像是一个需要被填满的空洞. 但是, 一旦它得到了某个值, 那么它就永远保持这个值.
  
  讲到这儿, 你有可能会问, 既然如此, 使用变量这个术语的意义又在哪里. 主要有两个原因.
    它们的确也是变量, 只不过它们的值只能改变一次(也就是, 它们从自由变量变成了绑定变量).
    它们看上去与传统编程语言中的变量很相似, 因此当遇到一行这样的代码:

  1. X =...

    我们心里就会开始想, "嗯, 这个我知道, X 是一个变量, =是一个赋值操作符". 实际上, 我们的这些想法也没有什么大错, X 近似于一个变量, =也近似于一个赋值操作符.
    说明, 出现在Erlang 代码之中的省略号(...)意味着"此处省略掉了一些代码".
    就其本质而言, =是一个模式匹配运算符, 当 X 是一个自由变量时, 它的行为与赋值一致.
    最后, 定义一个变量的词法单元就是这个变量的作用域, 因此, 如果在一个函数语句的范围内使用 X, 那么 X 的值就不能"跳出"语句之外. 在同一个函数的不同子句中, 彼此之间也不存在全局或者共享的私有变量. 如果 X 出现在许多个不同的函数当中, 那么这些 X 的值都是各自独立的.
    

2.6.2 模式匹配

  在大多数的语言中, =都表示赋值语句. 然而, 在 Erlang 中, =表示一个模式匹配操作. Lhs=Rhs 实际上是这样一个过程, 对右端求职(Rhs), 然后将结果于左端(Lhs)进行模式匹配.
  一个变量, 比如 X, 就是一种最简单的模式, 如前所述, 变量只能被赋值一次. 当我们第一次输入 X=SomeExpression 时, Erlang 会问自己, "要怎么做才能让这个语句的值变为 true?(Erlang 的每一个语句都会有值)", 由于 X 没有被赋值, 因此可以把 SomeExpression 的结果绑定到 X 上, 同时也使得语句有效, 于是皆大欢喜.
  但是, 如果随后又输入X=AnotherExpression, 那么只有当 SomeExpression与AnotherExpress一致时, 这个语句才会成立. 下面是一个例子

  1. 1> X = (2 + 4).
  2. 6
  3. 2> Y = 10.
  4. 10
  5. 3> X = 6.
  6. 6
  7. 4> X = Y.
  8. =ERROR REPORT==== 27-Oct-2006::17:25:25 ===
  9. Error in process <0.32.0> with exit value:
  10.   {{badmatch, 10}, [{erl_eval, expr, 3}]}
  11. 5> Y = 10.
  12. 10
  13. 6> Y = 4.
  14. =ERROR REPORT==== 27-Oct-2006::17:25:46 ===
  15. Error in process <0.32.0> with exit value:
  16.   {{badmatch, 4}, [{erl_eval, expr, 3}]}
  17. 7> Y = X.
  18. =ERROR REPORT==== 27-Oct-2006::17:25:57 ===
  19. Error in process <0.32.0> with exit value:
  20.   {{badmatch, 6}, [{erl_eval, expr, 3}]}

  在上面这个例子中, 在第1行, 系统对表达式2+4求值, 得到答案6, 在第一行之后, Shell 就维护了这样一张绑定表{X --> 6}. 在第2行被求值后, 绑定表就变成了这样{X --> 6, Y --> 10 }.
  现在到了第3行. 由于之前的运算, 现在有X --> 6, 于是 X=6这个匹配成立.
  但当运行到第4行X=Y 时, 现在的绑定表是{X --> 6, Y --> 10 }, 因此匹配失败, 然后打印了一个错误信息.
  这些表达式的成功与失败都取决于 X 与 Y 的值. 继续下面的话题之前, 你应该反复的吻戏这些知识, 以确保真的弄懂了它们.
  到目前为止, 你可能会觉得我们在匹配模式上所着的笔墨似乎有点儿多. 这是因为目前所遇到的情况, 在等号的左边, 无论是绑定的还是自由的, 这些模式都只是变量. 但后续的内容中, 我们会看到, 可以让=去匹配任意复杂得的模式. 在回到这一主题之前, 我们还会介绍元组(tuple)列表(list), 它们通常都用于存储符合数据.

2.6.3 单一赋值为何有益于编写质量更高的代码

  Erlang 里面的变量仅是对值的一个引用, 就具体实现而言, 一个绑定变量就是一个指针, 这个指针指向存放那个值的存储区域. 而那个值是无法改变的.
  不能改变一个变量的值是极为重要的事实, 因为这与 C Java 这与的命令式语言中的变量是不同的.
  现在来看看, 如果允许改变变量, 又会发生什么情况. 先定义一个变量 X:

  1. 1> X = 23.
  2. 23

  现在用 X 进行运算:

  1. 2> Y = 4 * X + 3.
  2. 95

  假定我们可以修改变量 X 的值(好可怕):

  1. 3> X = 19.

  幸运的是, Erlang 不允许这么做, shell 会神经质的报告:

  1. =ERROR REPORT==== 27-Oct-2006::17:36:24 ===
  2. Error in process <0.32.0> with exit value:
  3.   {{badmatch, 19}, [{erl_eval, expr, 3}]}

  也就是说我们已经给 X 赋了23, 它就不能是19.
  但是, 假设我们可以这么做, 那么 Y 的当前值就是不正确的, 我们就不能把语句2看做是一个等式, 此外, 如果在程序中不同的地方允许 X 多次改变自己的值, 那么一旦某些部分出错了, 我们会很难确定具体是哪个 X 值引起的, 也就是说, 会很难精确找出错误语句.
  在 Erlang 中, 变量一旦被赋值就不能再改变的特性还可以简化调试. 要知其所以然, 我们就要想一想, 错误是什么? 怎么才能发现错误?
  在代码错误的排查中相当常见的一种错误就是, 变量被赋予了一个错误的值. 在这种情况下, 你需要找出程序从哪儿获得的错误值. 如果变量在程序的不同地方多次修改了值, 那么要找出哪些修改是错误的比较困难.

  抛弃"副作用"意味着我们的程序可以并行化

  用术语来说, 我们把可修改的内存区域称为可变状态(Mutable state). Erlang 是一个函数式语言, 不存在可变状态. 
  本书的后续章节, 将会关注如何在多核 CPU 上编写程序. 当多核编程来临的时候, 采用不可变状态所带来的好处是难以估量的. 
  如果你用 C Java 这样的传统编程语言为多核 CPU 编写程序, 就不得不应付共享内存带来的问题. 你要想不破坏共享内存, 就必须在访问时对其加锁, 程序还要保证在操纵共享内存是不会崩溃. 
  而 Erlang 没有可变状态, 也就没有共享内存, 更没有锁, 这一切都有利于并行化程序的编写. 

  在 Erlang 中就不存在这个问题, 变量赋值一次之后永不再变. 一旦发现某个变量出错, 我们就能立刻确定程序之中绑定这个变量的代码, 它就是错误产生之处.
  那么, 你可能又会问, 没有了变量该怎样去编程? 在 Erlang 中要如何去描述 X = X + 1这样的表达式呢? 答案也很简单, 声明一个新变量, 它的名字之前没有被用过, 比如说 X1, 然后写 X1 = X + 1.

2.7 浮点数

  让我们用浮点数做一些运算:

  1. 1> 5/3.
  2. 1.66667
  3. 2> 4/2.
  4. 2.00000
  5. 3> 5 div 3.
  6. 1
  7. 4> 5 rem 3.
  8. 2
  9. 5> 4 div 2.
  10. 2
  11. 6> Pi = 3.14159
  12. 3.14159
  13. 7> R = 5.
  14. 5
  15. 8> Pi * R * R.
  16. 78.5397

  这里有件事情不要混淆, 第1行末尾是整数3. 句点代表表达式的结束而不是小数点. 如果要表示一个浮点数, 那么可以写成3.0.
  "/"永远返回浮点数, 因此 4/2计算结果(在 shell 中)就是2.0000. N div MN rem M 是用于整数除取余数, 因此5 div 3是1, 5 rem 3是2.
  浮点数必须含有小数点且小数点后至少有一位十进制数. 当你用"/"来除两个整数的时候, 其结果会自动转换为浮点数.

2.8 原子

  在 Erlang 中, 原子用来表示不同的非数字常量值.
  如果以前使用过 C 或Java 中的枚举类型, 不管是否意识到了. 你都是在使用和原子非常类似的东西.
  使用符号常量来增加代码的可读性, C 程序员对此大多非常熟悉. 一个典型的 C 程序员会在包含文件中定义一大堆全局常量, 比如文件 glob.h 可能包括这些:

  1. #define OP_READ 1
  2. #define OP_WRITE 2
  3. #define OP_SEEK 3
  4. ...
  5. #define RET_SUCCESS 223
  6. ...

  而典型的 C 代码会像下面这样使用这些符号常量:

  1. #include "glob.h"
  2. int ret;
  3. ret = file_operation(OP_READ, buff);
  4. if (ret == RET_SUCCESS){...}

  在 C 程序中, 这些常量具体的值并不重要, 重要的是它们的值都不相同, 而且它们之间可以进行比较.
  与之邓家的 Erlang 代码会是这样:

  1. Ret = file_operation(op_read, Buff).
  2. if
  3. Ret == ret_success ->
  4. ...

  Erlang中的原子是全局有效的, 而且无需使用宏定义或者包含文件.
  如果想编写一个涉及日期和星期的程序. 该如何在 Erlang 中表示某一天呢? 显然, 最好用 monday tuesday 这样的一些原子.
  原子是一串以小写字母开头, 后跟数字 字母下划线(_)邮件符号(@)的字符[你可能会发现句点(.)也能在原子中使用, 但这并不是一个正规的 Erlang 扩展]. 例如, red december cat meters yards joe@somehost 以及 a_long_name 等.
  使用单引号引起来的字符也是原子. 使用这种形式, 我们就能使得原子可以用大写字母作为开头或者包含非数字字符. 例如, 'Monday' 'Tuesday' '+' '*' 'an atom with spaces'. 你还可以将原本不需要使用引号的原子引起来, 'a'实际上等同于 a.
  一个原子的值就是原子自身. 因此, 如果输入的命令只有原子, 那么 Erlang shell 就会打印那个原子的值

  1. 1> hello.
  2. hello

  讨论原子的值或整数的值, 这听上去多少有些奇怪. 但因为 Erlang 是一个函数式语言, 每一个表达式都必须有值, 整数和原子这些特别简单的表达式也不例外.
  

2.9 元组

  你若想将一定数量的项组成单一的尸体, 那么就可以使用元组(tuple). 将若干个以逗号分隔的值用一堆花括号括起来, 就形成一个元组. 例如, 想要描述某个人的名字和他的身高, 你可以用{joe, 1.82}. 这个元素包括了一个原子和一个浮点值.
  元组类似于 C 语言中的结构, 除了元组是匿名的之外, 它们之间相差无几. 在 C 语言中要定义类型 Point 的变量 p, 要这么做:

  1. struct Point{
  2. int x;
  3. int y;
  4. } p;

  在 C 语言的结构中, 通过点操作符来访问一个结构的字段. 比如, 要设置 Point 中的 x, y 的值, 你可能会这么写:

  1.   p.x = 10; p.y = 45;

  Erlang 中并没有类型声明, 因此创建一个"Point"则会是这个样子:

  1.   P = {10, 45}

  这条语句创建了一个元组并将其绑定到变量 P. 和 C 语言不同的是, 元组中的字段没有名字. 因为这个元组只包含了两个整数, 所以我们必须记住其用处. 为了方便记忆, 通常可以使用一个原子作为元组的第一个元素来标明这个元组锁代表的含义. 隐藏我们可以使用{pint, 10, 45}来代替{10, 45}, 这能使得程序更为清晰易懂.
  元组可以嵌套. 你若想表达一个人信息的某些方面, 如他们的名字 高度 鞋码和眼睛颜色, 我们可以这样做:

  1. Person = {person,
  2. {name, joe},
  3. {height, 1.82},
  4. {footsize, 42},
  5. {eyecolour, brown}}.

  注意观察我们是如何使用原子作为元组的标签, 同时给字段赋值(在 name 和 eyecolour 这些地方).
  
  

2.9.1 创建元组

  在声明元组时, 就自动创建了元组, 不再使用它们时, 元组也随之销毁. Erlang 使用垃圾搜集器去回收没有使用的内存, 因此我们不用担心内存分配的问题.
  如果你创建的一个新元组引用了一个已绑定的变量, 那么新元组就会享有这个变量所引用的数据结构. 下面就是一个例子:

  1. 2> F = {firstName, joe}.
  2. {firstName, joe}
  3. 3> L = {lastName, armstrong}.
  4. {lastName, armstrong}
  5. 4> P = {person, F, L}.
  6. {person, {firstName, joe}, {lastName, armstrong}}

  而若在创建数据结构时试图引用一个未定义的变量, 系统就会给出一个错误. 比如, 下面这一行, 常识使用未定义的变量 Q, 会得到一个错误消息.

  1. 5> {true, Q, 23, costs}.
  2. ** 1: variable 'Q' is unbound **

  这就意味着变量 Q 未被定义.
  
  

2.9.2 从元组提取字段值

  正如前文所述, =看似赋值语句, 实乃模式匹配操作符. 呵呵, 这么啰嗦, 你大概要开始嘀咕为什么我们这么迂腐? 这么说吧, 模式匹配作为 Erlang 的基础, 用来完成很多不同的任务: 可以用它从数据结构中提取字段值, 在函数中进行流程控制, 或者当你想一个进程发送消息时, 从并行程序中筛选那些需要处理的消息.
  当想从元组中提取一些字段值时, 就会用到模式匹配操作符=
  让我们先回到用元组表示点的例子:

  1. 1> Point = {pint, 10, 45}.
  2. {pint, 10, 45}

  若想从 Point 中提取字段然后存放于 X, Y 两个变量, 可以这么做:

  1. 2> {pint, X, Y} = Point.
  2. {pint, 10, 45}
  3. 3> X.
  4. 10
  5. 4> Y.
  6. 45

  在命令2中, X 被绑定到10, Y 被绑定到45. 这里 Lhs=Rhs 表达式锁定义的值是 Rhs, 因此 shell 打印{pint, 10, 45}.
  如你所见, 位于等号两边的元组必须含有相同数量的元素. 两边相对应的元素必须绑定相同的值.
  现在如果输入这样的命令:

  1. 5> {pint, C, C} = Point.
  2. =ERROR REPORT==== 28-Oct-2006::17:17:00 ===
  3. Error in process <0.32.0> with exit value:
  4.   {{badmatch, {pint, 10, 45}}, [{erl_eval, expr, 3}]}

  会发生什么呢? 模式{point, C, C} 与 {pint, 10, 45}不能匹配, 因为 C 不可能同时等于10和45. 因此, 模式匹配失败了(注: 致熟悉 Prolog 的读者: Erlang 将匹配失败当做错误处理, 也不会在匹配中回溯), 系统会打印一个错误信息.
  如果你有一个复杂的元组, 那么可以使用相同结构的模式来提取索要的字段值, 并且只要在需要提取的字段位置上使用未绑定变量(注:这种使用模式匹配来提取变量的方法称为 unification, 很多函数式编程语言和逻辑式编程语言都使用这种方法).
  作为演示, 首先要定义一个含有复杂数据结构的 Person 变量:

  1. 1> Person = {person, {name, {firstName, joe}, {lastName, armstrong}}, {footsize, 42}}.
  2. {person, {name, {firstName, joe}, {lastName, armstrong}}, {footsize, 42}}

  现在, 编写一个模式去提取这个人的姓:

  1. 2> {_, {_, {_, Who}, _}, _}.
  2. {person, {name, {firstName, joe}, {lastName, armstrong}}, {footsize, 42}}

  最后打印出 Who 的值

  1. 3> Who.
  2. joe
  注意
  
  在前面的样例中, 将_作为占位符, 表示那些我们不关心的变量. 符号_称为匿名变量, 与常规变量不同, 在同一个模式中的不同地方, 各个_所绑定的值不必相同. 

2.10 列表

  我们用列表存储数目可变的东西, 如在商店里买到的商品 行星的名字 从因式分解函数中返回的素数, 等等.
  将若干个以逗号分隔的值使用一对方括号括起来, 就形成了一个列表. 下面的例子就演示了如何创建一个购物清单:

  1. 1> ThingsToBuy = [{apples, 10}, {pears, 6}, {milk, 3}].
  2. [{apples, 10}, {pears, 6}, {milk, 3}]

  列表之中的各个元素可有有各自不同的类型, 比如, 可以这样写:

  1. 2> [1+7, hello, 2-2, {cost, apple, 30-20}, 3].
  2. [8, hello, 0, {cost, apple, 10}, 3]

2.10.1 术语

  列表的第一个元素称为列表的头(head). 那么你可以想象一下, 如果从列表中移除头, 所剩下的东西就称为列表的尾(tail).
  例如, 如果有列表[1, 2, 3, 4, 5], 那么列表的头就是整数1, 它的尾为[2, 3, 4, 5]. 注意, 列表的头可以是任何东西, 但是列表的尾通常还是一个列表.
  访问列表的头式一个非常高效的操作, 因此, 实际上所有的列表处理函数都是从提取列表头开始的, 先对头进行处理, 然后继续处理列表的尾.

2.10.2 定义列表

  如果 T 是一个列表, 那么[H|T]也是一个列表(注: LISP 程序员注意:[H|T]其实是一个带有 CAR H 和 CDR T 的 CONS 单元. 在模式中, 这个语法可以分解成 CAR 和 CDR. 在表达式中, 它构造了一个 CONS 单元), 这个列表以 H 为头, 以 T 为尾. 竖线符号(|)可以将列表的分隔开来, 而[]则是空列表.
  无论何时, 当我们用[...|T]来构造一个列表时, 都应该保证 T 是一个列表, 如果 T 是一个列表, 那么新的列表就是"正规形式"的, 反之, 新列表就被称为"非正规列表". 大多数的库函数都假定列表是正规的, 它们不能正确的处理非正规列表.
  可以用[E1, E2, ..., En|T]这种形式向 T 的起始处加入多个元素. 例如:

  1. 3> ThingsToBuy1 = [{oranges, 4}, {newspaper, 1} | ThingsToBuy].
  2. [{oranges, 4}, {newspaper, 1}, {apples, 10}, {pears, 6}, {milk, 3}]

2.10.3 从列表中提取元素

  我们可以用模式匹配操作从一个列表中提取元素, 假定现在有一个非空的列表 L, 那么表达式[X|Y]=L (这里 X, Y 都是自由变量), 可以把列表的头提取到 X, 将列表的尾提取到 Y.
  如果我们在商店, 并且有一个购物清单 ThingsToBuy1, 首先要做的十把列表分解成头和尾:

  1. 4> [Buy1 | ThingsToBuy2] = ThingsToBuy1.
  2. [{oranges, 4}, {newspaper, 1}, {apples, 10}, {pears, 6}, {milk, 3}]

  这个匹配的结果是: Buy1 --> {oranges, 4} 和 ThingsToBuy2 --> [{newspaper, 1}, {apples, 10}, {pears, 6}, {milk, 3}].
  我们根据 Buy1去买橘子, 然后再来继续提取下面两个元素:

  1. 5> [Buy2, Buy3 | ThingsToBuy3] = ThingsToBuy2.
  2. [{newspaper, 1}, {apples, 10}, {pears, 6}, {milk, 3}]

  得到了 Buy2 --> {newspaper, 1}, Buy3 --> {apples, 10} 和 ThingsToBuy3 --> [{pears, 6}, {milk, 3}]
  
  

2.11 字符串

  严格来讲, Erlang 中并没有字符串, 字符串实际上就是一个整数列表, 用双引号(")将一串字符括起来就是一个字符串, 比如, 我们可以这样写:

  1. 1> Name = "Hello".
  2. "Hello"
  说明
  
  在某些编程语言中, 字符串既可以使用单引号也可以使用双引号, 而在 Erlang 中, 必须使用双引号. 

  这里的"Hello"仅仅是一个速记形式, 实际上它意味着一个整数列表, 列表中每个元素都是相应字符的整数值.
  shell 打印一串列表值时, 只有列表中的所有整数都是可打印字符, 它才把这个列表当做字符串来打印:

  1. 2> [1, 2, 3].
  2. [1, 2, 3]
  3. 3> [83, 117, 114, 112, 114, 105, 115, 101].
  4. "Surprise"
  5. 4> [1, 83, 117, 114, 112, 114, 105, 115, 101].
  6. [1, 83, 117, 114, 112, 114, 105, 115, 101]

  表达式2的列表[1, 2, 3]被原封不动的打印出来, 这是因为1 2 3 并不是可打印字符.
  表达式3的列表中, 所有的项都是可打印字符, 因此列表就打印成字符串的形式.
  表达式4和表达式3大体相同, 但列表的开始元素为1, 并非可打印字符, 因此这个列表被原封不动的打印.
  我们无须死记硬背哪一个整数表示哪一个特定字符(ASCII 码表), 可以使用$符号来表示字符的整数值. 例如, $a 实际上是一个整数, 表示字符 a, 比如说:

  1. 5> I = $s.
  2. 115
  3. 6> [I-32, $u, $r, $p, $r, $i, $s, $e].
  4. "Surprise"

字符串中使用的字符集

  字符串中的字符是 Latin-1(ISO-8859-1)编码的字符.

  说明 
  
  如果在shell 中将[72, 229, 107, 97, 110]作为表达式输入, 你可能不会看到想要的结果:
  1> [72, 229, 107, 97, 110].
  "H\345kan"
  怎么面目全非了? 这实际上是显示终端的字符集和区域设定有问题, 在这类问题上Erlang束手无策. 

  Erlang 所关心的, 只是以某种编码方式编码的遗传整数值列表. 如果碰巧是在 Latin-1编码下, 那么它们应该可以正确显示(如果终端显示设定无误的话).

2.12 再论匹配模式

  在本章接近尾声的时候, 我们要再一次回到模式匹配的话题.
  表2-1列出了一些模式和它们锁对应的值(注:值就是 Erlang 的数据结构). 在表2-1第3列结果栏中的内容说明了这个模式是否匹配对应的例子, 如果是, 那么显示变量的绑定情形. 逐行研究这些样例, 确保真正明白了它们的含义.


表2-1 (由于制表符的关系, [H I T] 表示 [H|T])

模式 结果
{X, abc} {123, abc} 成功, X --> 123
{X, Y, Z} {222, def, "cat"} 成功, X --> 222, Y --> def, Z --> "cat"
{X, Y} {333, ghi, "cat"} 失败, 元组结构不同
X true 成功, X --> true
{X, Y, X} {{abc, 12}, 42, {abc, 12}} 成功, X --> {abc, 12}, Y --> 42
{X, Y, X} {{abc, 12}, 42, true} 失败, X不能同时为{abc, 12} 和 true
[H I T] [1, 2, 3, 4, 5] 成功, H --> 1, T --> [2, 3, 4, 5]
[H I T] "cat" 成功, H --> 99, T --> "at"
[A, B, C I T] [a, b, c, d, e, f] 成功, A --> a, B --> b, C --> c, T --> [d, e f]

  如果你对其中的任何一点仍然心存疑惑, 那么应该试着在 shell 中输入表达式 Pattern = Term 来查看具体的运行结果.
  例如:

  1. 1> {X, abc} = {123, abc}.
  2. {123, abc}
  3. 2> X.
  4. 123
  5. 3> f()
  6. ok
  7. 4> {X, Y, Z} = {222, def, "cat"}.
  8. {222, def, "cat"}
  9. 5> X.
  10. 222
  11. 6> Y.
  12. def
  13. ...
  说明
  
  命令 f() 会让 shell 释放它所绑定过的所有变量. 执行这个命令后, 所有的变量都变成自由变量, 因此第4行的 X 与第1行和和第2行的X 也就没有任何关系. 

  现在, 我们对基本的数据类型已经非常熟悉了, 对单一赋值和模式匹配也有了初步的了解. 因此可以加快步伐进入新的一章, 学习如何定义函数和模块.


第3章 顺序型编程

  本章中, 我们会学到如何用 Erlang 来编写简单的顺序程序. 3.1节主要会讨论模块和函数. 而在学习函数的过程中, 还会进一步的了解第2章中谈到的匹配模式, 它们如何发挥更多的作用.
  此外, 我们还会继续第2章中提到过的购物清单的例子. 这一次, 要编写一些代码来计算购物清单中的价格总和.
  随着学习的深入, 我们还会循序渐进的改良之前的代码. 经过这样的过程, 就能学习如何去演化一个原始的创意, 将其变成优美的代码, 而不是一堆知其然而不知其所以然的死知识. 通过剖析其中的每一个步骤, 你将会领悟到一些可以应用到自己编程实践中的理念, 这会让你受益匪浅.
  再进一步, 我们会讨论高阶函数(称为 fun), 学习如何用他创建自己的控制抽象. 最后, 我们还会谈及断言(guard) 记录(record) case语句if语句.
  好了, 继续上路......

3.1 模块

  模块是 Erlang 中代码的基本单元, 我们编写的所有函数都存于模块之中. 模块文件通常存放在以.erl为扩展名的文件中.
  要运行一个模块, 首先需要编译它, 编译成功之后的模块文件其扩展名是.beam[注:beam 是 Bogdan's Erlang abstract Machine(Bogdan的 Erlang 抽象机)的缩写. Bogumil(Bogdan) Hausman在1993年实现了一个 Erlang 编译器, 并且设计了一套新的 Erlang 指令集].
  在我们动手编写第一个模块之前, 先来回想一下有关模式匹配的知识. 下面的内容中, 我们会创建两个数据结构, 分别用来表示矩形和圆. 之后再来解析这些数据结构, 从矩形中取边长, 从圆中取半径, 下面是其实现:

  1. 1> Rectangle = {rectangle, 10, 5}.
  2. {rectangle, 10, 5}
  3. 2> Circle = {circle, 2.4}.
  4. {circle, 2.40000}
  5. 3> {rectangle, Width, Ht} = Rectangle.
  6. {rectangle, 10, 5}
  7. 4> Width.
  8. 10
  9. 5> Ht.
  10. 5
  11. 6> {circle, R} = Circle.
  12. {circle, 2.40000}
  13. 7> R.
  14. 2.40000

  在第1行和第2行分别创建了矩形和圆. 第3行和第6行用模式匹配分别提取了矩形和圆中的字段. 在第4行 第5行和第7行, 打印这些通过模式匹配获得的值. 运行到第7行之后, shell 中的便利绑定是这样的:{Width --> 10, Ht -->5, R --> 2.4}.
  将模式匹配从 shell 挪到函数中只需稍加改变. 首先, 我们来创建一个名为 area 的函数, 用它来计算矩形和圆形的面积. 我们把这个函数放在 gemoetry 模块中, 并把这个模块存到 gemoetry.erl 文件中. 下面就是这个模块的完整内容:

  1. geometry.erl
  2. -module(geometry).
  3. -export([area/1]).
  4. area( {rectangle, Width, Ht} ) -> Width * Ht;
  5. area( {circle, R} ) -> 3.14159 * R * R.

  先别理会-module 和-export 声明(我们稍后会再讨论它们), 现在我们只关注 area 函数.
  area 函数由两个子句构成, 子句间以分号分隔, 最后一条子句的后面以句点作为结束符. 每一个子句都有一个函数头和一个函数体, 函数头由函数名和随后的以括号括起来的模式组成, 函数体则由一系列表达式组成, 如果函数头中的模式与调用参数匹配成功的话, 其对应的表达式就会进行运算. 模式将按照它们出现的函数定义中的先后顺序进行匹配.
  注意, 形如{rectangle, Width, Ht}的模式是 area 函数定义的第一部分, 每个模式都明确的和一个子句对应. 下面看看 area 函数的第一个子句:

  1. area( {rectangle, Width, Ht} ) -> Width * ht;

  这是一条计算矩形面积的规则. 当我们的调用是 geometry:area( {rectangle, 10, 5} ) 时, 最前面那个模式被匹配, 绑定变量{Width --> 10, Ht --> 5}. 匹配完之后, ->之后的代码会被执行. 这里是 Width * Ht, 即 10 * 5, 结果为50.
  现在, 编译并运行它:

  1. 1> c(geometry).
  2. {ok, geometry}
  3. 2> geometry:area( {rectangle, 10, 5} ).
  4. 50
  5. 3> geometry:area( {circle, 1.4} ).
  6. 6.15752

  上面的演示是在做什么呢? 在第1行, 我们输入了命令 c(geometry), 编译 geometry.erl 文件中的源代码. 编译器返回{ok, geometry}, 意味着编译成功, 模块 geometry 已经被编译并加载. 第2行和第3行是在 geometry 模块之中调用定义的函数. 注意, 如何同时使用模块名和函数名以精确定位希望调用的函数.
  

扩展这个程序

  假如现在我们要扩展这个程序, 假如对正方形这种几何对象的支持, 可以这么做:

  1. area( {rectangle, Width, Ht} ) -> Width * ht;
  2. area( {circle, R} ) -> 3.14159 * R * R;
  3. area( {square, X} ) -> X * X .

  或者这么写:

  1. area( {rectangle, Width, Ht} ) -> Width * ht;
  2. area( {square, X} ) -> X * X ;
  3. area( {circle, R} ) -> 3.14159 * R * R.

  在这个例子中, 子句的顺序并不重要, 物理这些子句的顺序如何, 对程序来说, 运行的效果都是一样的. 这是因为(这个例子中)个个子句的模式彼此互不相干. 这使编写和扩展程序变得很简单, 只须添加新的模式就行了. 不过, 通常来说, 子句的顺序还是有点关系的, 因为进入一个函数的时候, 调用是按照模式在文件中的顺序依次进行匹配的.
  继续深入之前, 小结一下, 对于 area 函数的编写方式, 我们应该注意以下两点.
  (1) area 函数由若干个不同的子句构成. 当调用这个函数时, 对齐调用参数的匹配过程从第一个子句开始依次向下进行.
  (2) 函数不能处理模式匹配失败的情形, 此时程序会失败并抛出一个运行时错误. 这一点是故意为之.
  很多编程语言, 比如 C 语言, 每个函数只有一个入口点. 如果用 C 语言来写这个程序的话, 代码可能会使这个样子:

  1. enum ShpeType { Rectangle, Circle, Square };
  2. struct Shape {
  3. enum ShapeType kind;
  4. union {
  5. struct { int width, height; } rectangleData;
  6. struct { int radius; } circleData;
  7. struct { int side; } squareData;
  8. } shapeData;
  9. };
  10. double area(struct Shape *s){
  11. if (s->kind == Rectangle) {
  12. int width, ht;
  13. width = s->shapeData.rectangleData.width;
  14. ht = s->shapeData.rectangleData.ht;
  15. return width * ht;
  16. } else if (s->kind == Circle) {
  17. ....
  18. }
  代码要放哪儿
  
  如果你下载本书的样例代码或想要编写自己的示例程序, 在进入 shell 运行编译器之前, 为确保系统能找到代码, 需要确认当期的工作目录是否正确. 
  在尝试编译示例程序之前, 如果你在使用操作系统的命令 shell, 那么需要先把目录切换到代码所在的目录. 
  如果你运行的是 Windows 上的 Erlang 标准发布版, 也需要将目录切换到存储代码的目录上, Erlang shell 中油两个命令可以帮你切换到正确的目录. 如果你现在不知道当期位于哪个目录, pwd() 可以打印当期的工作目录. cd(Dir)则可以将当期目录切换到 Dir 所在的目录. 在 shell 中你应该使用正斜杠来分隔目录名.
  
  1> cd("c:/work").
  c:/work
  
  给 Windows 用户的一个小技巧. 创建一个名为"C:/Program Files/erl5.4.12/.erlang"的文件(根据实际的安装路径进行调整), 然后在文件中加入如下内容:
  
  io:format("consulting .erlang in ~p~n", [ element( 2, file:get_cwd() ) ] ).
  %% Edit to the directory where you store your code
  c:cd("c:/work").
  io:format("Now in:~p~n", [enement( 2, file:get_cwd() ) ] ).
  
  保存之后, 每次启动 Erlang 时, 它都能自动切换到目录"c/work". 

  C代码想我没原原本本的战士了参数和汗水进行模式匹配的过程. 在 C 语言中, 程序员必须自己编写模式匹配代码, 并保证他们正确无误.
  在 Erlang 中, 做相同的事, 只需要编写模式, Erlang 编译器会自动生成优化的模式匹配代码, 帮助程序找到正确的入口点.
  我们还可以再来看看 Java 中的等价代码:

  1. abstract class Shape{
  2. abstract double area();
  3. }
  4. class Circle extends Shape {
  5. final double radius;
  6. Cirecle(double radius) {this.radius = radius;}
  7. double area() {return Math.PI * radius * radius;}
  8. }
  9. class Rectangle extends Shape {
  10. final double ht;
  11. final double width;
  12. Rectangle(double width, double height){
  13. this.ht = height;
  14. this.width = width;
  15. }
  16. double area {return width * ht;}
  17. }
  18. class Square extends Shape{
  19. final double side;
  20. Square(double side){ this.side = side; }
  21. double area() {return side * side; }
  22. }

  比较 Erlang 和 Java 代码, 你会发现, Java 代码中的 area 函数分布在3个不同的地方, 而在 Erlang 的代码中, area 所有的代码都在一起.
  
  

3.2 购物系统 ———— 进阶篇

  回顾一下之前我们讨论过的购物清单, 他大致是这个样子的

  1. [{oranges, 4}, {newspaper, 1}, {apples, 10}, {pears, 6}, {milk, 3}]

  假如现在还想知道所购物品的价格, 我们必须知道购物清单中各个商品的单价. 我们假定这个计算总和的功能由一个名为 shop 的模块来实现. 那么, 我们现在就开始动手, 打开你惯用的编辑器, 将如下内容输入到一个名为 shop.erl 的文件中:

  1. shop.erl
  2. -module(shop).
  3. -export([cost/1]).
  4. cost(oranges) -> 5;
  5. cost(newspaper) -> 8;
  6. cost(apples) -> 2;
  7. cost(pears) -> 9;
  8. cost(milk) -> 7.

  函数 cost/1 (注:符号 Name/N 表示一个带有 N 个参数的名为 Name 的函数. N 称为函数的运算目)由5个子句组成, 每个子句的头部都包括了一个模式(本例之中的模式非常简单, 只是一个原子而已). 当我们对 shop:cost(X)求值时, 系统会用 X 对这些子句的每一个模式进行匹配. 如果匹配了某个模式, 那么紧跟在->之后的代码就会执行.
  cost/1函数必须从模块之中导出, 如果你想从模块的外部调用它, 这是必须的. (注: 即用-compile(export_all), 来导出模块之中的所有函数).
  我们来测试一下. 在 Erlang shell 中编译和运行这个程序:

  1. 1> c(shop).
  2. {ok, shop}
  3. 2> shop:cost(apples).
  4. 2
  5. 3> shop:cost(oranges).
  6. 5
  7. 4> shop:cost(socks).
  8. =ERROR REPORT==== 30-Oct-2006::20:45:10 ===
  9. Error in process <0.34.0> with exit value:
  10. {function_clause, [{shop, cost, [socks]},
  11. {erl_eval, do_apply, 5},
  12. {shell, exprs, 6},
  13. {shell, eval_loop, 3}]}

  第1行编译了 shop.erl 文件中的模块, 第2行 第3行得到了 apples 和 oranges 的价格(结果分别是2个和5个货币单位), 第4行想得到 socks 的价格, 但因为没有任何子句可以与之匹配, 所以得到了一个模式匹配失败的错误, 系统打印一条错误消息(错误消息中的"function_clause"部分表明由于没有可匹配参数的子句, 从而导致函数调用失败).
  回到购物清单, 假设我们的购物清单是这样的:

  1. 1> Buy = [{oranges, 4}, {newspaper, 1}, {apples, 10}, {pears, 6}, {milk, 3}].
  2. [{oranges, 4}, {newspaper, 1}, {apples, 10}, {pears, 6}, {milk, 3}]

  若想计算清单之中所有物品的总价格, 可以用下面这种方式达到目的:

  1. shop1.erl
  2. -module(shop1).
  3. -export([total/1]).
  4. total( [ {What, N} | T ] ) -> shop:cost(What) * N + total(T);
  5. total( [] ) -> 0.

  让我们试试一下代码:

  1. 2> c(shop1).
  2. {ok, shop1}
  3. 3> shop1:total([]).
  4. 0

  这个结果为什么会是0? 这是因为 total/1的第2个子句是 total( [] ) -> 0;

  1. 4> shop1:total( [{milk, 3}] ).
  2. 21

  调用时, total( [ {milk, 3} ] )匹配了子句 total([{What, N}|T]), T = [] (注:这是因为[X]就是[X|[]]的缩写). 匹配之后, 此时的变量绑定为{What --> mile, N --> 3, T --> []}. 然后, 进入函数体(shop:cost(What) * N + total(T)). 函数体重的所有变量都被替换成绑定标记的值. 那么, 函数体的结果就是表达式 shop:cost(milk) * 3 + total([]).
  shop:cost(milk)的值为7, total([])的值为0, 因此, 函数体的结算结果也就是7*3+0=21.
  如果换上一个更加复杂的参数, 结果又会如何?

  1. 5> shop1:total([{pears, 6}, {milk, 3}]).
  2. 75
  在哪里使用分号
  
  我们在Erlang 中会遇到3种表达符号. 
  逗号(,) 用来分隔函数调用 数据构造器以及模式中的参数.
  句号(.) (后面跟一个空白符号) 用来在 shell 中分隔完整的函数和表达式.
  分号(;) 用来分隔子句, 在这几种情况下都会用到子句: 分段的函数定义 case子句 if语句 try...catch语句 以及 receive表达式. 
  无论何时, 我们只要看到一组后面根由表达式的模式, 就会使用分号进行分隔. 
  
  Pattern1  ->
        Expressions1;
  Pattern2  ->
        expressions2;
  ...

  此次, total 函数第1个子句的匹配情况是{What --> pears, N --> 6, T --> [{milk, 3}] }. 相对应的结果是shop:cost(pears) * 6 + total([{milk, 3}]), 也就是 9 * 6 + total([{milk, 3}]).
  上次我们已经计算过total([{milk, 3}])的值是21, 因此, 最终的值也就是: 9*6+21 = 75.
  最后:

  1. 6> shop1:total(Buy).
  2. 123

  在我们结束本节的内容之前, 应该更加细致的观察 total 函数, total(L)通过分析参数 L 的各种不同条件来工作. 这里的 L 存在两种不同情况, L 是一个非空列表, 或者是一个空列表. 对于这里的每一种情况, 我们都写了一个子句来进行处理:

  1. total([Head|Tail]) ->
  2. some_function_of(head) + total(Tail);
  3. total([]) ->
  4. 0.

  在这个例子中, Head 是一个模式{What, N}. 当第1个子句匹配一个非空列表时, 他会从列表中选出头部, 对齐进行一些处理, 然后调用自身去处理列表的尾部. 当列表被缩减至空列表时([]), 就会匹配到第2个子句.
  函数 total/1实际上做了两件事情. 第一, 它查找列表中每件物品的价格, 然后把他们的价格加起来. 我们可以对其进行重写, 把查找单个物品与价格和对价格进行汇总这两个部分分开, 代码将会更加清晰易懂. 为此我们需要设计两个小的列表处理函数, 分别是 sum 呵 map, 但在此之前, 我们需要先了解 fun 的概念. 在此之后, 我们将会编写 sum 和 map 函数, 以及 total 的改进版本.

3.3 同名不同目的函数

  函数的目(arity) 就是它所拥有的参数数量. 在 Erlang 中, 同一模块的两个函数, 如果它们同名但是并不相同, 这样的两个函数被认为是完全不相同的. 他们之前除了名字恰巧相同之外, 彼此之间再无其他关联.
  为了方便起见, 同名但不同目的函数经常被 Erlang 程序员用来作为辅助函数. 下面就是一个例子:

  1. lib_misc.erl
  2. sum(L) -> sum(L, 0).
  3. sum([], N) -> N;
  4. sum([H|T], N) -> sum(T, H+N).

  函数 sum(L)用于计算列表 L 之中所有元素的总和. 它使用了一个辅助函数 sum/2, 这个辅助函数其实可以叫任何其他的名字, 你甚至可以把它叫做辅助函数 hedgehog/2, 而程序的含义保持不变. 显然, 作为一个命名, sum/2是一个不错的选择, 因为它能给程序的读者一点提示, 告诉他们函数在干些什么, 与此同时你也不用去为此发明一个新的名字(大部分时候, 这都不是什么轻松的活儿).

3.4 fun

  fun 就是匿名函数. 被称为匿名函数, 是因为它并没有名字. 我们来做点儿试验, 先定义一个 fun, 然后把它赋给一个变量 Z:

  1. 1> Z = fun(X) -> 2 * X end.
  2. #Fun<erl_eval.6.56006484>

  当定义一个 fun 时, Erlang shell 会打印# fun<...>, 这里的...常常是一些奇怪的数字. 不过现在不用管它.
  我们可以将一个参数应用到一个 fun 上, 对于 fun 来说, 这是我们唯一能做的事, 像这样:

  1. 2> Z(2).
  2. 4

  对这个 fun 来说, Z 显然不是一个好名字, 叫做 Double 可能更好一些, 这恰好表述了 fun 的功能:

  1. 3> Double = Z.
  2. #fun<erl_eval.6.10732646>
  3. 4> Double(4).
  4. 8

  fun可以拥有任意数量的参数. 我们可以像下面一样, 编个函数来计算直角三角形的斜边:

  1. 5> Hypot = fun(X, Y) -> math:sqrt(X*X + Y*Y) end.
  2. #func<erl_eval.12.115169474>
  3. 6> Hypot(3, 4).
  4. 5.00000

  如果调用参数的个数不正确, 会得到一个错误:

  1. 7> Hypot(3).
  2. ** exited: {{badarity, {#Fun<erl_eval.12.115169474>, [3]}},
  3. [{erl_eval, expr, 3}]} **

  这个错误为何被称作 badarity? 回忆一下arity(目)的含义 ----一个函数可以接受的参数个数. badarity表明在 Erlang 中找不到由所调用的函数名及其给定的参数数量所表明的函数. 在这个例子中, 给出了函数 Hypot, 它需要两个参数, 但我们只传了一个.
  fun 也可以有若干个不同的子句. 下面是在华氏气温与摄氏气温之间进行转换的函数:

  1. 8> TempConvert = fun({c, C}) -> {f, 32 + C*9/5};
  2. 8> ({f, F}) -> {c, (F-32)*5/9}
  3. 8> end.
  4. #Fun<erl_eval.6.5.56006484>
  5. 9> TempConvert({c, 100}).
  6. {f, 212.000}
  7. 10> TempConvert({f, 212}).
  8. {c, 100.100}
  9. 11> TempConvert({c, 0}).
  10. {f, 32.0000}
  说明
  
  第8行的表达式跨越了好几行. 在输入这个表达式时, 没输入一个新行 shell 都会重复打印"8>", 这意味着这个表达式并未结束, shell 还在等待后续的输入.

  Erlang 是一种函数式的编程语言, 也就是说, 除了极个别情况外, fun 既可以作为函数的参数, 也可以作为函数(或者 fun)的结果.
  这些能够返回 fun 或接受 fun 作为参数的函数, 都被称作高阶函数(high-order function). 在下一节中, 我们会看到一些与此有关的例子.
  现在这些内容看上去好像没什么特别, 这只是因为我们目前还未领略到 fun 的强大威力. 虽然到目前为止, fun 中的代码与模块之中其他的常规函数的代码看起来没有区别, 但实际上, 距离真相我们仅有一步之遥. 高阶函数式函数式语言的灵魂所在, 它能使程序脱胎换骨. 一旦掌握了 fun 的用法, 你就会爱上他们. 后面我们还会看到更多用到 fun 的地方.
  

3.4.1 以 fun 为参数的函数

  list 是标准库中的一个模块, 从中导出的很多函数都是以 fun 作为参数的. 其中, 最有用的十 lists:map(F, L). 这个函数将 fun F 应用到列表 L 的每一个元素上, 并返回一个新的列表.

  1. 12> L = [1, 2, 3, 4].
  2. [1, 2, 3, 4]
  3. 13> lists:map(Double, L).
  4. [2, 4, 6, 8]

  另一个常用的函数式 lists:filter(P, L), 它返回一个新的列表, 新列表由列表 L 中每一个能满足 P(E)为 true 的元素组成.
  让我们定义一个函数 Event(X), 当 x 为奇数则返回 true.

  1. 14> Even = fun(X) -> (X rem 2) =:= 0 end.
  2. #Fun<erl_eval.6.56006484>

  其中 X rem 2 是对 x除2取余数, 而=:=是一个恒等测试符号. 现在我们可以测试一下 Even, 然后用它作为 map 和 filter 的参数.

  1. 15> Even(8).
  2. true
  3. 16> Even(7).
  4. false
  5. 17> lists:map(Even, [1, 2, 3, 4, 5, 6, 8]).
  6. [false, true, false , true, false, true, true]
  7. 18> lists:filter(Even, [1, 2, 3, 4, 5, 6, 8]).
  8. [2, 4, 6, 8]

  我们将像 map 和 filter 这样, 在一个函数调用中处理整个列表的操作称之为 list-at-a-time 操作. list-at-a-time 操作能让程序变得简洁易懂, 有了它, 我们就可以把处理整个列表的程序看做是一个抽象步骤. 这就是我们程序变得更为精炼的原因. 否则, 我们必须将处理列表元素的操作分解为一系列独立的步骤.
  
  

3.4.2 返回 fun 的函数

  fun不仅可以用作函数的参数(比如 map 和 filter), 而且其他函数也可以将 fun 当做返回值.
  下面是一个例子, 假设有一个名为 fruit 的列表:

  1. 1> Fruit = [apple, pear, orange].
  2. [apple, pear, orange]

  现在我们可以定义一个函数 MakeTest(L), 将这个列表(L)转换为一个测试函数, 这个测试函数将会检查它所传入的参数是否为列表 L 的成员:

  1. 2> MakeTest = fun(L) -> (fun(X) -> lists:member(X, L) end) end.
  2. #Fun<erl_eval.6.56006484>
  3. 3> IsFruit = MakeTest(Fruit).
  4. #Fun<erl_eval.6.56006484>

  如果 X 是列表 L 的成员, 那么函数 lists:member(X, L)返回 true, 反之则返回 false. 现在我们已经编写了一个测试函数, 可以进行一些测试:

  1. 4> IsFruit(pear).
  2. true
  3. 5> IsFruit(apple).
  4. true
  5. 6> IsFruit(dog).
  6. false

  同样, 我们还能将其用作 lists:filter/2的参数:

  1. 7> lists:filter(IsFruit, [dog, orange, cat, apple, bear]).
  2. [orange, apple]

  返回 fun 的 fun, 这个语法多少还有些让人迷惑, 下面我们再花一点时间来把它说清楚. 一个返回"正常"值的 fun 一般是这样的:

  1. 1> Double = fun(X) -> (2 * X) end.
  2. #Fun<erl_eval.6.56006484>
  3. 2> Double(5).
  4. 10

  括号之中的代码(更明确的说, 就是2*X) 明显就是函数的返回值. 现在我们试着把这个 fun 放进括号之中. 请记住, 括号之中的东西就是返回值:

  1. 3> Mult = fun(Times) -> ( fun(X) -> X * Times end ) end.
  2. #Fun<erl_eval.6.56006484>

  现在, 括号之中的 fun 就是 fun(X) -> X * Timesend, 这是关于 X 的一个函数, 但 Times是哪里冒出来的呢? 答案就是, 他是更外层的 fun 的参数.
  对 Mult(3)求值返回fun(X) -> X * 3 end, 也就是内层fun 的函数体, 其中的 Times为3所替代. 现在我们可以测试一下:

  1. 4> Triple = Mult(3).
  2. #Fun<erl_eval.6.56006484>
  3. 5> Triple(5).
  4. 15

  因此, Mult 只是 Double 的一个泛化版本. 它并非计算一个值然后返回它, 而是会返回一个函数, 这个函数只有在被调用的时候才会计算具体的值.
  

3.4.3 定义你自己的抽象流程控制

  等一下, 你有没有发现, 到目前为止, 我们还没有涉及任何 if switch for while 这些语句, 而且似乎也没有发现有何不妥, 原本应当出现这些语句的位置现在都被模式匹配和高阶函数替代了. 到目前为止, 我们不需要任何额外的控制结构.
  如果需要额外的控制结构, 那么就在我们的手边, 一个现成的超强胶水随时可用, 我们可以用它来自制所需的控制结构. 先来看一个例子, Erlang 没有 for 循环, 那么就来做一个:

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

  就这么简单, 例如, 对 for(1, 10, F)求值会生成列表[F(1), F(2), ..., F(10)].
  这个 for 循环中的模式匹配是如何工作的呢? 第一个子句仅当第1个参数和第2个参数相等时才会匹配. 那么, 在调用 for(10, 10, F)时, 第一个子句就会将 Max 绑定到10, 结果就是列表[F(10)]. 而如果调用参数是 for(1, 10, F)时, 第一个子句不会匹配, 因为 Max无法同时匹配成1和10. 这时, 得到匹配的是第二个子句, 这个子句返回的是[F(I) | for(I+1, 10, F)], 此时变量绑定为{I --> 1, Max --> 10}, 也就是 I 被1替换, Max 被10替换, 结果就是[F(1) | for(2, 10, F)].

  何时使用高阶函数
  
  正如我们看到的, 在使用高阶函数时, 可以创建自己的新的抽象控制结构. 可以将函数作为函数的参数传入, 可以编写返回fun的函数. 但在实践中, 这些技巧并不常用. 
  . 实际上, 我写的所有模块都会用到类似于 lists:map/2这样的函数 ----它如此通用以至于我几乎要把Map 当做是 Erlang 语言的一部分. 调用类似 map filter partition这些 list 模块中的函数特别常见. 
  . 有时, 我也创建自己的抽象控制结构, 但这远不如调用标准库模块中的高阶函数频繁. 在一些大的模块中, 也只是偶尔才会用到这种技术. 
  . 编写返回 fun 的函数我是很少做的, 编写上百个模块, 也可能只有一两个模块会用到这种编程技术. 返回 fun 的函数通常不容易调试. 但从另一方面来说, 这一技术可以用来解决诸如延时求值 可重入的解析器 解析组合子等问题. 因为这些问题本身就是返回解析器的函数. 

  现在我们有了一个简单的 for 循环(这与一般的命令语言并不完全相同, 但是它满足我们的基本需求), 可以用它去创建一个从1到10的整数列表:

  1. 1> lib_misc:for(1, 10, fun(I) -> I end).
  2. [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

  或者用它去计算1到10的平方:

  1. 2> lib_misc:for(1, 10, fun(I) -> I * I end).
  2. [1, 4, 9, 16, 25, 36, 49, 64, 91, 100]

  随着经验的不断积累, 你会发现创建自己的控制结构可以很大程度的降低程序的代码量, 有时他还能让程序变得更为清晰. 这是因为你可以根据需求量身定做最为合适的控制结构, 超越繁琐呆板的控制结构, 从语言的桎梏中解脱.

  常见错误
  
  有些读者错误的将源代码中的判断输入 shell, 它们并不是有效的 shell 命令. 如果尝试这么做, 你会看到一些很奇怪的错误信息. 因此再次提醒你, 不要这么做. 
  如果你的模块恰巧使用了与系统模块相同的名字, 那么在编译时会得到一个奇怪的消息, 告诉你不能从一个保留目录中加载模块. 你只需要重新命名, 并删掉之前编译所生成的 beam 文件即可. 

3.5 简单的列表处理

  现在我们已经介绍过 fun 了, 可以回头继续编写 sum 和 map, 它们是编写 total 函数改进版的必备部分(我相信你还没有忘记).

  1. mylists.erl
  2. sum( [H|T] ) -> H + sum(T);
  3. sum( [] ) -> 0.

  注意, sum 中两个子句的顺序是无关紧要的. 因为第一个子句匹配一个非空列表, 第二个子句匹配空列表, 这两种情况是不会互相干扰的. 我们可以这样来测试 sum:

  1. 1> c(mylists). %% <-- Last time I do this
  2. {ok, mylists}
  3. 2> L = [1, 3, 10].
  4. [1, 3, 10]
  5. 3> mylists:sum(L).
  6. 14

  第一行编译了 mylists模块. 从今往后, 在这本书中, 我都会忽略掉编译模块的命令, 你要自己记得做这件事. sum 函数的内部工作机制极易理解. 让我们跟踪一下具体的执行情况:
  (1) sum([1, 3, 10])
  (2) sum([1, 3, 10]) = 1 + sum([3, 10]) ( 注: by map(_, []) -> []; )
  (3) = 1 + 3 + sum([10]) ( 注: by map(_, []) -> []; )
  (4) = 1 + 3 + 10 + sum([]) ( 注: by map(_, []) -> []; )
  (5) = 1 + 3 + 10 + 0 ( 注: by map( F, [H|T] ) -> [F(H) | map(F, T)]. )
  (6) = 14
  
  最后, 看看我们早先论述过的 map/2. 下面是它的定义:

  1. mylists.erl
  2. map( _, [] ) -> [];
  3. map( F, [H|T] ) -> [F(H) | map(F, T)].

  第一个子句表示该对一个空列表做什么处理. 把任何函数映射到一个空列表(它没有任何元素)上只能产生一个空列表.
  第二个子句是一个处理非空列表的规则, 它的开头是 H, 尾是 T. 同样非常简单, 只是创建一个新列表, 其头式 F(H), 尾是 map(F, T).

  说明 
  
  mylists 的这个 map/2定义是从标准库 lists 中复制过来的. 在任何情况下都不要企图把你自己的模块名修改为 lists, 除非你确切的知道会发生什么后果.

  下面我们用两个函数来运行 map, 一个是对列表中元素乘以2, 一个是对列表中的元素求平方.

  1. 1> L = [1, 2, 3, 4, 5].
  2. [1, 2, 3, 4, 5]
  3. 2> mylists:map(fun(X) -> 2*X end, L).
  4. [2, 4, 6, 8, 10]
  5. 3> mylists:map(fun(X) -> X*X end, L).
  6. [1, 4, 9, 16, 25]

  对于 map 的探索是否就要告一段落了呢? 嗯, 没有, 绝对不会! 在稍后的章节里, 我们将用列表解析来展示一个极为精简的 map 版本. 在20.2节中, 我们还会看到如何并行化的计算 map 中的所有元素(在多核计算机上这将会显著提升程序的运行效率), 不过这对于现在的我们还言之尚早. 有了 sum 和 map, 我们就能利用这两个函数来重写 total:

  1. shop2.erl
  2. -module(shop2).
  3. -export([total/1])
  4. -import(lists, [map/2, sum/1]).
  5. total(L) ->
  6. sum( map( fun( {What, N} ) -> shop:cost(What) * N end, L) ).
  我如何写程序
  
  在写程序的时候, 我的方法是写一点测试一点. 从一个没有多少函数的小模块开始, 然后在 shell 中用几个命令来编译和测试它们. 等到测试结果令我满意, 才会继续写其他函数, 在对新代码编译测试, 整个过程都这么展开. 
  通常, 我也不会草率的决定程序需要什么样的数据结构. 在运行简单例子的同时, 我会不断地审视之前选择的数据结构是否合理. 
  我写程序倾向于循序渐进的扩展代码, 而不是在动手之前就已经完整的构思出来. 这种方法使我不会在发现错误前出大的错误. 最重要的是, 这种方法很有趣, 它能让我获得即时的反馈, 几乎是在输入的同时就能让我知道我的想法是否可行. 
  一旦找到某个问题在 shell 中的解决办法, 我通常就会立刻写个 makefile 以及一些代码来重新生成我从 shell 中所得的收获. 

  我们可以通过下面这些分解步骤来了解这个函数是如何工作的:

  1. 1> Buy = [{oranges, 4}, {newspaper, 1}, {apples, 10}, {pers, 6}, {milk, 3}].
  2. [{oranges, 4}, {newspaper, 1}, {apples, 10}, {pers, 6}, {milk, 3}]
  3. 2> L1 = lists:map( fun( {What, N} ) -> shop:cost(What) * N end, Buy ).
  4. [20, 8, 20, 54, 21]
  5. 3> lists:sum(L1).
  6. 123

  这个模块里的-import和-export声明的使用也是需要注意的.
  . 声明-import(lists, [map/2, sum/1])意味着 map/2是从 lists 模块中导入的. 也就是说我们可以用 map(Fun, ...)而不必去些 lists:map(Fun, ...). cost/1由于没有在导入声明中声明, 所以我们不得不使用完整的名称 shop:cost.
  . 声明-export([total/1])意味着函数total/1能够在模块 shop2之外调用. 只有从一个模块中导出的函数才能在模块之外调用.
  如果你现在认为我们的 total 函数已经没有什么改良的余地, 那就大错特错了. 我们还能进一步的改进它, 这就需要用到列表解析技术.
  

3.6 列表解析

  列表解析是一种无须使用 fun map 或filter 来创建列表的表达式. 他能让程序更为简洁且更加容易理解.
  先从一个例子开始. 假设我们有一个列表 L:

  1. 1> L = [1, 2, 3, 4, 5].
  2. [1, 2, 3, 4, 5]

  现在, 假设想要把列表当中的每个元素加倍. 这我们之前已经做过, 这里再重申一下:

  1. 2> lists:map( fun(X) -> 2*X end, L ).
  2. [2, 4, 6, 8, 10]

  与之相比, 我们还有一个更为精炼的方式, 那就是使用列表解析.

  1. 4> [ 2*X || X <- L ].
  2. [2, 4, 6, 8, 10]

  记号[ F(X) || X <- L ] 代表"由 F(X)组成的列表, 其中 X 是取值于列表 L". 因此 [ 2*X || X <- L ] 意味着"列表中每一个元素X*2后的列表".
  为了知道如何使用列表解析, 可以先在 shell 中输入几行表达式, 观察一下结果. 首先定义 Buy:

  1. 1> Buy = [ {oranges, 4}, {newspaper, 1}, {apples, 10}, {pears, 6}, {milk, 3} ].
  2. [ {oranges, 4}, {newspaper, 1}, {apples, 10}, {pears, 6}, {milk, 3} ]

  现在, 把原始列表中的每一个元素个数乘以2:

  1. 2> [ {Name, 2 * Number} || {Name, Number} <- Buy ].
  2. [{oranges,8},{newspaper,2},{apples,20},{pears,12},{milk,6}]

  注意, 记号(||)左边的元组{Name, Number}是用于匹配列表 Buy 中每个元素的模式. 左边的元组{Name, 2*Number}则是一个构造器.
  如果现在想要计算原始列表中所有元素的价格总和, 可以这么做, 首先用列表中每个元素的单价代替它的名字:

  1. 3> [ {shop:cost(A), B} || {A, B} <- Buy ].
  2. [{5, 4}, {8, 1}, {2, 10}, {9, 6}, {7, 3}]

  然后将价格与数量相乘:

  1. 4> [ shop:cost(A) * B || {A, B} <- Buy ].
  2. [20, 8, 20, 54, 21]

  再把它们加起来:

  1. 5> lists:sum( [shop:cost(A) * B || {A, B} <- Buy] ).
  2. 123

  最后, 如果想把这些都整合到一个函数中, 可以这么写

  1. total(L) ->
  2. lists:sum( [shop:cost(A) * B || {A, B} <- L] ).

  列表解析能明显地缩短代码, 同时也让它能够清晰易懂. 我们可以用列表解析来编写一个更为简洁的 map 定义, 看看到底能简洁到什么程度:

  1. map(F, L) -> [ F(X) || X <- L ]l

  下面这个表达式就是一个列表解析的最常见形式:

  1. [ X || Qualifier1, Qualifier2, ... ]

  X可以是任意一个表达式, 每个限定词(qualifier)可以是一个生成器或者是一个过滤器.
  . 生成器通常写为 Pattern <- ListExpr, 其中 ListExpr 必须是一个对列表项求值的表达式.
  . 过滤器可以是一个谓词(返回 true 或 false 的函数), 也可以是一个布尔表达式.
  
  注意, 列表解析中的生成器部分也可以像过滤器一样工作, 比如:

  1. 1> [ X || {a, X} <- [ {a, 1}, {b, 2}, {c, 3}, {a, 4}, hello, "wow" ] ].
  2. [1, 4]

  下面我们用几个例子来对本节的内容做一个总结.

3.6.1 快速排序

  下面这个例子展示了如何使用两个列表解析来完成一个排序算法: (这个代码着重于展现代码的优雅性而不是执行效率. 这样使用++, 一般而言不是一个良好的编程习惯.)

  1. lib_misc.erl
  2. qsort( [] ) -> [];
  3. qsort( [Pivot|T] ) ->
  4. qsort( [ X || X <- T, X < Pivot] )
  5. ++ [Pivot] ++
  6. qsort( [ X || X <- T, X >= Pivot ] ).

  (这里的++是一个中缀添加操作符):

  1. 1> L = [23, 6, 2, 9, 27, 400, 78, 45, 61, 82, 14].
  2. [23, 6, 2, 9, 27, 400, 78, 45, 61, 82, 14]
  3. 2> lib_misc:qsort(L).
  4. [2, 6, 9, 14, 23, 27, 45, 61, 78, 82, 400]

  为了弄懂它是如何工作的, 我们可以一步一步的跟踪它的执行情况. 首先定义一个列表L, 然后调用 qsort(L), 接下来它匹配了 qsort 的第二个子句:

  1. 3> [ Pivot|T ] = L.
  2. [23, 6, 2, 9, 27, 400, 78, 45, 61, 82, 14]

  其中的变量绑定情况为{Pivot --> 23}和{T-->[6, 2, 9, 27, 400, 78, 45, 61, 82, 14]}.
  现在, 将 T 分为两个列表, 一个列表中的所有元素都是列表 T 中小于 Pivot, 另一个列表中的所有元素都是列表 T 中大于或等于 Pivot 的:

  1. 4> Smaller = [X || X <- T, X < Pivot].
  2. [6, 2, 9, 14]
  3. 5> Bigger = [X || X <- T, X >= Pivot].
  4. [27, 400, 78, 45, 61, 82]

  现在, 对 Smaller 和 Bigger 进行排序, 然后用 Pivot 将它们合并起来:
  qsort( [6, 2, 9, 14] ) ++ [23] ++ qsort( [27, 400, 78, 45, 61, 82] )
  = [2, 6, 9, 14] ++ [23] ++ [27, 45, 61, 78, 82, 400]
  = [23, 6, 2, 9, 27, 400, 78, 45, 61, 82, 14]
  
  

3.6.2 毕达哥拉斯三元组

  毕达哥拉斯三元(毕达哥拉斯学派研究出了一个公式:若m是奇整数,则m,(m^2-1)/2及(m^2+1)/2便是三元数组)组是一个整数集合{A, B, C}, 它使得 A^2 + B^2 = C^2.
  函数 pythag(N)产生一个列表, 包含了所有满足 A^2 + B^2 = C^2, 且3条边之和小于等于整数 N 的整数集合{A, B, C}

  1. lib_misc.erl
  2. pythag(N) ->
  3. [ {A, B, C} ||
  4. A <- lists:seq(1, N),
  5. B <- lists:seq(1, N),
  6. C <- lists:seq(1, N),
  7. A + B + C =< N,
  8. A * A + B * B =:= C * C
  9. ].

  这里简单的解释几句: lists:seq(1, N)返回一个由1到 N 整数组成的列表, 所有 A<-lists:seq(1, N)意味着 A 的取值范围是1到 N 的所有整数. 因此我们的程序可以这么理解, "从1到 N 中得到 A 的所有可能取值, 从1到 N 中获得 B 的所有可能取值, 从1到 N 中获得 C 的所有可能取值, 使得 A+B+C <= NA*A + B*B = C*C"

  1. 1> lib_misc:pythag(16)
  2. [{3, 4, 5}, {4, 3, 5}]
  3. 2> lib_misc:pythag(30)
  4. [{3, 4, 5}, {4, 3, 5}, {5, 12, 13}, {6, 8, 10}, {8, 6, 10}, {12, 5, 13}]

3.6.3 变位词

  如果你着迷于英语式的纵横字谜, 你自己就会发现所谓的变位词. 现在让我们用 Erlang 编写一个漂亮的小函数 perms 去寻找一个字符串所有可能的排序. 下面就是这个函数的具体代码.

  1. lib_misc.erl
  2. perms( [] ) -> [ [] ];
  3. perms( L ) -> [ [H|T] || H <- L, T <-( L -- [H] ) ].
  4. 1> lib_misc:perms("123").
  5. ["123", "132", "213", "231", "312", "321"]
  6. 2> lib_misc:perms("cats").

3.7 算术表达式

3.8 断言

3.8.1 断言序列

3.8.2 断言样例

3.8.3 true 断言的使用

3.8.4 过时的断言函数

3.9 记录

3.9.1 创建和更新记录

3.9.2 从记录中提取字段值

3.9.3 在函数中对记录进行模式匹配

3.9.4 记录只是元组的伪装

3.10 case/if 表达式

3.10.1 case 表达式

3.10.2 if 表达式

3.11 以自然顺序创建列表

3.12 累加器

  如何在函数之外得到两个列表? 如何写一个函数将一个整数列表分解为偶数列表和奇数列表? 我们可以这样做:

第4章 异常

4.1 异常

4.2 抛出异常

4.3 try…cache
4.3.1 缩减版本
4.3.2 使用 try…cache 的编程惯例

4.4 cache

4.5 改进错误信息

4.6 try…cache 的编程风格
4.6.1 经常会返回错误的程序
4.6.2 出错几率比较小的程序

4.7 捕获所有可能的异常

4.8 新老两种异常处理风格

4.9 栈跟踪

第5章 顺序型编程进阶

5.1 BIF

5.2 二进制数据

5.3 比特语法
5.3.1 16bit 色彩的封包与解包
5.3.2 比特语法表达式
5.3.3 高级比特语法样例

5.4 小问题集锦
5.4.1 apply
5.4.2 属性
5.4.3 块表达式
5.4.4 布尔类型
5.4.5 布尔表达式
5.4.6 字符集
5.4.7 注释
5.4.8 epp
5.4.9 转义符
5.4.10 表达式和表达式序列
5.4.11 函数引用
5.4.12 包含文件
5.4.13 列表操作符++和—
5.4.14 宏
5.4.15 在模式中使用匹配操作符
5.4.16 数值类型
5.4.17 操作符优先级
5.4.18 进程字典
5.4.19 引用
5.4.20 短路布尔表达式
5.4.21 比较表达式
5.4.22 下划线变量

第6章 编译并运行程序

6.1 开启和停止 Erlang shell

6.2 配置开发环境
6.2.1 为文件系统加载器设定搜索路径
6.2.2 在系统启动时批量执行命令

6.3 运行程序的几种不同方法
6.3.1 在 Erlang shell 中编译运行
6.3.2 在命令提示符下编译运行
6.3.3 把程序当做 escript 脚本运行
6.4.4 用命令行参数编程

6.4 使用 makefile 进行自动编译
6.4.1 makefile 模板
6.4.2 定制 makefile 模板

6.5 在 Erlang shell 中的命令编辑

6.6 解决系统死锁

6.7 如何应对故障
6.7.1 未定义/遗失代码
6.7.2 makefile 不能工作
6.7.3 shell 没有响应

6.8 获取帮助

6.9 调试环境

6.10 崩溃转储

第7章 并发

第8章 并发编程

8.1 并发原语

8.2 一个简单的例子

8.3 客户/服务器介绍

8.4 创建一个进程需要花费多少时间

8.5 带超时的 receive
8.5.1 只有超时的 receive
8.5.2 超时时间为0的 receive
8.5.3 使用一个无限等待超时进行接收
8.5.4 实现一个计时器

8.6 选择性接收

8.7 注册进程

8.8 如何编写一个并发程序

8.9 尾递归技术

8.10 使用 MFA 启动进程

8.11 习题

第9章 并发编程中的错误处理

9.1 链接进程

9.2 on_exit 处理程序

9.3 远程错误处理

9.4 错误处理的细节
9.4.1 捕获退出的编程模式
9.4.2 捕获退出信号(进阶篇)

9.5 错误处理原语

9.6 链接进程集

9.7 监视器

9.8 存活进程

第10章 分布式编程

10.1 名字服务
10.1.1 第一步: 一个简单的名字服务
10.1.2 第二步: 在同一台机器上, 客户端运行于一个节点而服务器运行于第二个节点
10.1.3 第三步: 让客户机和服务器运行于同一个局域网内的不同机器上
10.1.3 第四步: 在因特网上的不同主机上分别允许客户机和服务器

10.2 分布式原语

10.3 分布式编程中使用的库

10.4 有 cookie 保护的系统

10.5 基于套接字的分布式模式
10.5.1 lib_chan
10.5.2 服务器代码

第11章

11.1 消息序列图

11.2 客户端程序

11.3 客户端程序

11.4 服务器端组件
11.4.1 聊天控制器
11.4.2 聊天服务器
11.4.3 群组管理器

11.5 运行程序

11.6 聊天程序源代码
11.6.1 聊天客户端
11.6.2 lib_chan 配置
11.6.3 聊天控制器
11.6.4 聊天服务器
11.6.5 聊天群组
11.6.6 输入输出窗口

11.7 习题

第12章

12.1 端口

12.2 为一个外部 C 程序添加接口
12.2.1 C 程序
12.2.2 Erlang 程序

12.3 open_port

12.4 内联驱动

12.5 注意

第13章

13.1 库的组织结构

13.2 读取文件的不同方法
13.2.1 从文件中读取所有 Erlang 数据项
13.2.2 从文件的数据项中一次读取一项
13.2.3 从文件中一次读取一行数据
13.2.4 将整个文件的内容读入到一个二进制数据中
13.2.5 随机读取一个文件
13.2.6 读取 ID3 标记

13.3 写入文件的不同方法
13.3.1 向一个文件中写入一串 Erlang 数据项
13.3.2 想文件中写入一行
13.3.3 一步操作写入整个文件
13.3.4 在随机访问模式下写入文件

13.4 目录操作

13.5 查询文件的属性

13.6 复制和删除文件

13.7 小知识

13.8 一个搜索小程序

第14章 套接字编程

14.1 使用 TCP
14.1.1 从服务器上获取数据
14.1.2 一个简单的 TCP 服务器
14.1.3 改进服务器
14.1.4 注意

14.2 控制逻辑
14.2.1 主动消息接收(非阻塞)
14.2.2 被动型消息接收(阻塞)
14.2.3 混合型模式(半阻塞)

14.3 连接从何而来

14.4 套接字的出错处理

14.5 UDP
14.5.1 最简单的 UDP 服务器和客户机
14.5.2 一个计算阶乘的 UDP 服务器
14.5.3 关于 UDP 协议的其它注意事项

14.6 向多台机器广播消息
14.7 SHOUTcast 服务器
14.7.1 SHOUTcast 协议
14.7.2 SHOUTcast 服务器的工作机制
14.7.3 SHOUTcast 服务器的伪代码
14.7.4 允许 SHOUTcast 服务器

14.8 进一步深入

第15章 ETS 和 DETS: 大数据的存储机制

15.1 表的基本操作

15.2 表的类型

15.3 ETS 表的效率考虑

15.4 创建 ETS 表

15.5 ETS 程序示例
15.5.1 三字索引迭代器
15.5.2 构造表
15.5.3 构造表有多块
15.5.4 访问表有多块
15.5.5 胜出的是......

15.6 DETS

15.7 我们没有提及的部分

15.8 代码清单

第16章 OTP 概述

16.1 通用服务器的进化路线
16.1.1 server 1: 原始服务器程序
16.1.2 server 2: 支持事务的服务器程序
16.1.3 server 3: 支持热代码替换的服务器程序
16.1.4 server 4: 同事支持事务和热代码替换
16.1.5 server 5: 压轴好戏

16.2 gen_server 起步
16.2.1 第一步: 确定回调模块的名称
16.2.2 第二步: 写接口函数
16.2.2 第三步: 编写回调函数

16.3 gen_server 回调的结构
16.3.1 启动服务器程序时发生了什么
16.3.2 调用服务器程序时发生了什么
16.3.3 调用和通知
16.3.4 发给服务器的原生消息
16.3.5 Hasta la Vista, Baby(服务器的终止)
16.3.6 热代码替换

16.4 代码和模板
16.4.1 gen_server 模板
16.4.2 my_bank

第17章 Mnesia: Erlang 数据库

17.1 数据库查询
17.1.1 选取表中的所有数据
17.1.2 选取表中的数据
17.1.3 按条件选取表中的数据
17.1.3 从两个表选取数据(关联查询)

17.2 增删表中的数据
17.2.1 增加一行
17.2.2 删除一行

17.3 Mnesia 事务
17.3.1 取消一个事务
17.3.2 加载测试数据
17.3.3 do() 函数

17.4 在表中保存复杂数据

17.5 表的类型和位置
17.5.1 创建表
17.5.2 表属性的常见组合
17.5.3 表的行为

17.6 创建和初始化数据库

17.7 表查看器

17.8 进一步深入

17.9 代码清单

第18章 构造基于 OTP 的系统

18.1 通用的事件处理

18.2 错误日志
18.2.1 记录一个错误
18.2.2 配置错误日志
18.2.3 分析错误

18.3 警报管理

18.4 应用服务
18.4.1 素数服务
18.4.2 面积服务

18.5 监控树

18.6 启动整个系统

18.7 应用程序

18.8 文件系统的组织

18.9 应用程序监视器

18.10 进一步深入
18.11 我们如何创建素数

第19章 多核小引

第20章 多核编程

20.1 如何在多核 CPU 上更有效率的运行
20.1.1 使用大量进程
20.1.2 避免副作用
20.1.3 顺序瓶颈

20.2 并行化顺序代码

20.3 小消息 大计算

20.4 映射——归并算法和磁盘索引程序
20.4.1 映射——归并算法
20.4.2 全文检索
20.4.3 索引器的操作
20.4.4 运行索引器
20.4.5 评论
20.4.6 索引器的代码

20.5 面向未来的成长

附录A 给我们的程序写文档

附录B Microsoft Windows 环境下的 Erlang 环境

附录C 资源

附录D 套接字应用程序

附录E 其他

附录F 模块和函数参考

索引

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注