七周七语言(卷2)

978-7-115-42735-9
作者: 【美】Bruce A. Tate(泰特) Fred Daoud(达乌德) Ian Dees(迪斯) Jack Moffitt(墨菲特)
译者: 7ML翻译组
编辑: 陈冀康

图书目录:

详情

本书带领读者认识和学习7种编程语言,更好地帮助读者探索更为强大的编程工具。全书共8章,前7章介绍了Lua、Factor、Elm、Elixir、Julia、miniKanren和Idris共计7种编程语言,最后一章总结回顾了所有的知识点。书中对每一种编程语言的介绍,都为编程开发带来了独特而强大的思路。除此之外,书中还提供了一系列代码示例和在线资源以供参考。

图书摘要

版权信息

书名:七周七语言(卷2)

ISBN:978-7-115-42735-9

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

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

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

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


• 著    [美]Bruce A. Tate Fred Daoud Ian Dees Jack Moffitt

  译    7ML翻译组

  责任编辑 陈冀康

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

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

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

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

  反盗版热线:(010)81055315


Copyright © 2014 The Pragmatic Programmers, LLC. Original English language edition, entitled Seven More Languages in Seven Weeks.

Simplified Chinese-language edition Copyright © 2016 by Posts & Telecom Press.

All rights reserved.

本书中文简体字版由The Pragmatic Programmers, LLC授权人民邮电出版社独家出版。未经出版者书面许可,不得以任何方式复制或抄袭本书内容。

版权所有,侵权必究。


本书带领读者认识和学习了7种编程语言,旨在帮助读者探索更为强大的编程工具。

本书延续了同系列的畅销书《七周七语言》《七周七数据库》和《七周七Web开发框架》的体例和风格。全书共8章,前7章介绍了Lua、Factor、Elm、Elixir、Julia、miniKanren和Idris共计7种编程语言,最后一章总结回顾了所有的知识点。书中对每一种编程语言的介绍,都为编程开发带来了独特而强大的思路。除此之外,书中还提供了一系列代码示例和在线资源以供参考。

本书适合有一定基础的开发人员阅读,能够帮助读者拓宽思路,激发更多的灵感。


7ML翻译组是一个临时性的小组,他们“来自五湖四海,为了一个共同的革命目标,走到一起来了”。7ML是本书英文书名“7 More Languages in…”的简写。

崔鹏飞,程序员,任职于ThoughtWorks,最爱删代码,光头迎风照三里。他负责本书第1章的翻译。

窦衍森,ThoughtWorks高级咨询师,曾供职于汤森路透等多家外企。一直从事.NET,Web相关技术开发,是新技术的追随者。他负责本书文前部分和第2章的翻译。

杨云,ThoughtWorks公司资深咨询师,《深入理解Scala》译者,Haskell函数式编程QQ群(72874436)群主。他拥有19年软件开发经验,是函数式编程深度粉丝,致力于研究函数式编程在工业中的使用和工程实践。他负责本书第3章的翻译。

许晓斌,阿里巴巴技术专家,目前在AliExpress从事研发效率改进和微服务实施及推广工作。他是《Maven实战》的作者,《Cucumber:行为驱动开发》的合译者。他负责本书第4章和第8章的翻译。

李小波,ThoughtWorks咨询师,热爱编码,用过Java Web、Android、AngularJS、Ruby on Rails。追求简洁与高效的工作方式,热爱传播知识。组织过敏捷之旅、Scrum Gathering、Global Day of CodeRetreat等活动。创办了“软件匠艺社区”(http://codingstyle.cn),个人博客是http://seabornlee.cn。他负责本书第5章的翻译。

肖鉴明,全栈工程师,任职于微软亚太研发集团(Windows & Device Group);热爱各种新(旧)技术、微软系与非微软系技术都有所涉猎;业余爱好魔方、摄影和Lego。他负责本书第6章的翻译。

鄢倩,ThoughtWorks高级软件工程师,函数式编程狂热爱好者,Clojurian,业余喜读书,囊括文史哲。他负责本书第7章的翻译。


我厌倦了学习新的编程语言,曾经以为增设七个玄之又玄的语言不会太有用。但是我错了,我很喜欢这本书。这些语言是那么有趣,介绍也令人信服,我现在希望体验一下它们。

• Brian Sletten,总裁,Bosatsu咨询公司

语言不只是新的语法,还有它们思考问题的新方法。例如,它们考虑用户界面、科学计算、分布式系统或安全保障的最佳方式是什么?当你深入学习这本书的每一门语言时,会得到新的抽象和原则,这将有助于你使用任何语言写程序。赶紧来吧!

• Evan Czaplicki Elm,Prezi的创造者

如果你认为读一本有关编程语言的书并不会改变你对编程的思考,你可以去阅读Idris那一章—当然,除非你不想让自己的C ++(或C#或Java)的代码更加清晰,并且不想把成百上千行的代码缩减为两行。

• Ted Neward,作者、讲师、导师,Neward and Associates有限责任公司

就像一个艺术家选择不同的绘画颜料会限制了他们可以实现效果一样,你选择的语言也会限制你所能编写的程序。学习一门新的语言,你可以设想新的解决方案,并以新的方式表达它们。阅读这本书可以添加七个特别有趣的语言到你的技能列表中。

• Paul Butcher,《七周七并发模型》作者

本书以很好的节奏来介绍了一组有趣的语言,这些语言对大部分人来说都是新的。本书不仅节奏适中,而且提供了足够有用的细节,这些细节也不至于多到影响读者天生的好奇心。这绝对是一本好书,我推荐给所有想开拓自己的编程视野的人。

• Matthew Wild,Prosody IM XMPP服务器作者

本书不仅向我们介绍了一组更广泛的语言,而且就如何思考语言的使用和设计提出挑战。软件开发是一项艰巨的事业,而学习新的语言永远是至关重要的。这就是为什么“七周七X”系列对任何一名认真的程序员都是最宝贵的读物。

• Daniel Hinojosa,开发者、演讲者、教练、《Testing in Scala》作者


早在2010年,我深感不安,编写并发性软件日益增长的困难困扰着我。我手头的工具都不顺手,它们没有提供一个参考模型,来帮助我认清面临的问题。

我觉得现在是改变的时候了。

然而,从数以百计的编程语言中,怎么才能找到一个适合我的标准的呢?怎么才能把这个巨大的集合过滤成一个较小的、可以更详细地探究的集合?后来我发现,有人做了刚好符合我的需求的事情:Bruce Tate 刚写了一本《七周七语言》,探讨了Ruby、Io、Prolog、Erlang、Scala、Clojure和Haskell。

我熟悉《七周七语言》中的很多语言,但这本书不仅仅是介绍编程语言结构,它还介绍了语言中的哲学思想、社区和思考模型。对我来说,这本书是讲述一个关于并发的故事,当我读书的时候,不可变、线程、并发Futures、并发Actors、软件事务内存,还有很多很多概念,都一一浮现在我脑海里。

当我读完这本书,我清楚地知道哪些语言和范式是我接下来想要探索的。我买了一堆Erlang、Clojure和Haskell的图书,并且开始编写代码了。

几个月后,仍然没有找到适合我所有标准的语言:我想要Erlang的鲁棒性和虚拟机分配,但我也想要Clojure的元编程和多态,它的语法让我很舒服。这时候,我决定在Erlang的虚拟机上创建Elixir语言。现在,四年过去了,Elixir成了这本新的《七周七语言》所介绍的语言之一。

有趣的是,第一本《七周七语言》实际上不是像我理解的一样是一个关于并发的故事。《七周七语言》和任何其他优秀的著作一样,给读者留下空间,让他们将自自己的经历纳入为故事的一部分,让每个读者都有不同的收获,并在其特定的情况下继续选择其他的语言来进行探索。

这使得这本新的《七周七语言》图书更是雄心勃勃。书中的许多语言是相对比较新的,还在开发中,由此也带来了一系列可以学习的新的理念和经验。这也为读者开辟了一个挑选下一门语言的途径,不仅可以掌握它们而且能够参与语言开发的部分工作。

《七周七语言》对我的编程生涯产生了深刻的影响,我相信阅读这本书也会对你产生同样的效果。

请记住:阅读这本新《七周七语言》仅仅是旅程的开始。

José Valim

Elixir的发明者


2012年,在伦敦一个温暖的房间里,当时我很紧张。我一直在世界各地巡回做同样的讲座,每次都人满为患。当然,我相信观众会对我以往的那些笑话发出笑声,甚至每次在需要时也会鼓掌。但这次,有一个问题:我的“七语言”图书中的4种语言的发明人就坐在我面前的观众席中。我很担心,在这样一个场景下,我是否有足够的自信来谈论这些美丽的语言。最终,虽然我卡壳了一两次,但是整个演讲还是很好的。Erlang的发明人Joe Armstrong,现在已经是我亲密的朋友了,他甚至在演讲中称赞了我。然后,他还邀请我在6个月之后去斯德哥尔摩给Erlang用户社区做一次演讲。

最令我感动的是一个听众的提问。她问:“你真的能在七周内学会七种语言吗?”我们都知道答案。跟学习语言一样,要想真正学到一门编程语言需要几个月甚至几年的熏陶。但是,为什么我们要尝试一下呢?

每一种新的语言都会向你展示一个词汇表,而不仅仅是一两个单词。这个新的词汇表包含了你用来描述世界的各种想法。尽管精确的语法几乎肯定不会从沙箱转换到生产环境的解决方案,但你会看到很多习惯是会这样转换的。当使用Elixir的宏时,你将学会在模板中表达代码,这种元编程可以从根本上让任何程序员得到提高。当使用Factor时,你将学习很自然地用一种强大和有趣的方式去组合函数,而在以前这似乎不是那么的自然。当用过miniKanren几周后,你会发现用单独的步骤来表达程序似乎没有使用简单的规则那么有效。

想想一个画家,在尝试了雕塑后,就学会了表达景深。或一个年轻的业务主管,在上了一节数学或者编程课后,就学会了新的电子表格技术。思想是我们交易时使用的“货币”,每次你掌握的习惯都会增加你的价值。

“七周七X”系列的每本书都在试图讲一个故事,通过明智的选择,教给你最需要知道的习惯。作为作者,我们的工作就是找到一套合适的、你最想看到的并且最重要的习惯。要做到这一点,我们需要深刻理解行业的趋势。

从硬件角度来看,我们认为,多核编程、质量及复杂性正在推动向函数式语言的方向发展。尽管移动技术仍然落后于其他编程技术,移动设备正在爆炸式发展。

正如我们之前讲的,我们认为软件的复杂性正在推进函数式编程。横切关注点和代码质量非常有利于拥有元编程功能的语言。更好的类型模型,就像Haskell那样,正在强势复出,这样可以在达到生产环境之前由编译器来捕捉更多的错误。

在此背景下,是时间来开始学习新的七种语言了。你会发现这本书和你读的第一本《七周七语言》之间至少有三个很大的差异。首先,作者已经从1个人变成4个人了。然后,作者团队允许每种语言更深入,所以整体的篇幅更长,并可以更迅速地开始上手每一种语言。最后,这些语言大多数都是新的,而不像在第一本《七周七语言》中一样,从4个不同的十年里找到那些代表性的语言。

本书的团队和语言

我们通常会在其他地方介绍作者团队,但因为这些语言专家实际上是你的学习向导,我想在这里介绍他们以及他们要讲解的语言。

Bruce Tate(Elixir和Elm)

我是Bruce Tate,来自德克萨斯州奥斯汀市,是一个山地车手、皮艇手和两个孩子的父亲。我是icanmakeitbetter.com的CTO。我管理一个Ruby程序员的小团队,但很快就会转成Elixir的开发。你可能知道,我是“七周七X”系列的第一本书—《七周七语言》的作者。

Elixir

我选择Elixir,因为它是Erlang虚拟机上的纯函数式语言,具有丰富的Ruby风格的语法和Lisp风格的宏。我很看重语法,认为它也许比什么都重要。要向说英语的人表达想法,需要一个丰富而强大的语法。

Elm

我选择Elm,因为它是彻底背离当今在浏览器中以回调为中心的开发风格的代表语言。简化浏览器中的代码,这实际上是最活跃的语言运动之一。Elm致力于响应式编程,使用数据流和函数来响应变化。使用映射到函数之上的信号来表示用户交互,就可以移除回调从而简化JavaScript程序的复杂性。

Fred Daoud(Factor)

Fred是一名来自加拿大蒙特利尔的、充满激情的软件开发人员。他喜欢学习新的语言、框架和编程技术,从OO到FP和响应式模型,他都很感兴趣。Fred是《七周七Web框架》的合著者。

Factor

Fred选择Factor,因为这种串联的、基于栈的编程模型从根本上改变了程序员的思维方式。这不只是一个智力练习,Factor带有一个功能齐全的类库、UI框架和Web框架。构建一个Factor的程序将改变你使用原生语言的方式。

Ian Dees(Lua和Idris)

白天,Ian Dees在波特兰地区的测试设备制造商编写代码并测试程序。到了晚上,他喝着意式咖啡编写编程图书,其中包括《Cucumber Recipes》。他的tweets @undees。

Lua

Ian选择Lua,因为它是一种快速、灵活的语言,非常适合作为脚本添加到现有的项目中。当使用Lua构建一个产品系统后,Ian爱上了这门可嵌入的原型语言。

Idris

当Ian看到把在Agda和Idirs语言中的依赖类型应用到C++和Java中的一个演示时,他就被Idris的潜力所吸引了。从那时起,他一直想要更详细地探讨这些概念。

Jack Moffit(Julia和miniKanren)

作为Mozilla基金会的开发人员和管理者,Jack经常接触新的语言和技术。他已经针对各种主题写作长达五年时间了,包括最近合著的《七周七Web开发框架》。

miniKanren

miniKanren不是一门真正独立的语言,它是用于逻辑编程的领域特定语言。它结合了像Clojure这样带有宏的函数式编程语言,其结果是产生了一种新的、引人注目的编程模型。通常情况下,逻辑程序员发现很难让自己的逻辑程序和外面的世界相结合,而嵌入一个通用语言编写的逻辑DSL就可以解决这个问题。你会发现,这样的组合开辟了一个全新的编程范式,这就是为什么Jack选择miniKanren。

Julia

Jack选择Julia,因为它很有趣,并且和他一直在使用的Clojure和Erlang完全不一样。Julia的关注点是计算统计和多维度数学。它天生就是为并发和分布式而设计的。R是目前占主导地位的技术计算语言,但有时性能是R致命的弱点。Julia的早期用户已经做出了这样的评论:基于Julia的语言特性,它的性能有实质性的改进。

就像本系列中的其他书籍一样,新的《七周七语言》与一般的技术图书略有不同。我们将尽量涵盖更广泛的内容,而且我们要给你一点学习的压力,我们想你会喜欢最终结果的。不过,这并不适合每一个人。

除非你读到这里仍然可以接受本书的内容,否则就不要购买这本书。最主要的是,要明白我们的目标是让你越来越自给自足,学习速度远远超过一般那种只一种技术的技术书籍。这是需要付出的,你将不得不做更多的工作。

本书不是安装指南或支持渠道

那些读过“七周七X”系列图书的读者知道,我们不会把注意力集中于让你上手,我们不会尝试在七个平台上支持七种语言。我们不会这么做的。我们避免选择付费支持的语言。你可以访问这些语言的编程社区以获得帮助。在大多数情况下,他们会很愿意提供帮助。

本书不会给出每种语言丰富的安装指南,并且你可能会发现,一些练习的输出在你的系统上略有不同。如果你是一个喜欢每一个字符都一致,并且希望能够掌控整个学习过程的读者,不好意思,我们帮不了你。事实上,我们认为,自行构建和支持安装过程会帮助你更快地了解你所选择的语言。

相反,我们会向你保证:如果你努力完成自己的安装,我们将带你走得更远。我们的目标是,可以把你带到可以用每种语言解决一个常见的问题的高度。这是一个艰巨的目标,但我们认为我们已经做到了。

本书有4种写作风格

我从来没想过要再写一本“七语言”的书,因为第一本《七周七语言》的要求是如此之高,而且语言的列表是如此地引人注目。我从来没有想过我会找到七种语言来比肩第一本书中的7种语言,如果我做到了,我确信我也不想再花费一两年的努力来完成这本书了。我过去告诉自己,在“七语言”的书里,通过引入不同作者来改变风格是行不通的。

你看,即使《七周七语言》由不同的语言组成,那本书也给我们展示了一个业界当时的情况。所选的语言,从面向对象向声明函数式的方向发展,然后以纯函数式语言Haskell结束。这些语言的集合不是没有关系的散文,所以它们就能准确地综合到一起。当然,如果是一个作者团队,故事会更发散,整体的统一性也会被分散。

然后,Eric Redmond和Jim R. Wilson 写完了《七周七数据库》,这打消了我最初的念头。他们写了一本书,讲了一个数据库行业发展的故事,他们讲得很好。两个作者可以更容易地跟上瞬息万变的版本。

关键是,他们主要以第一人称复数来写。学习语言和学习使用数据库引擎是有一些不同的。

每个探索都是一次非常个人的体验。我相信,如果我们可以让你走进每一章的作者的内心,如果你可以更深入地分享他们的经验,那么,你自己的学习经验也将会更加丰富且更加强大。出于这个原因,每个作者都会以第一人称来写他们自己所选择的语言。当你看到文中采用“我”和“我的”,而不是“我们”和“我们的”的时候,你就知道个中缘由了。我们正在努力为你提供更加个人化的体验。我们认为你会很欣赏这种差异。

本书不会很枯燥的

有人想要简洁的。我们不会那样做。

你会发现,我们把每种语言比作成一个电影角色。我们这样做是因为:根据我们的经验,我们要帮助读者从一种语言过渡到下一种语言。我们发现,这些比喻能比一节乏味的历史课能更好地达到我们的目的。我们知道,这种风格可能会让一些读者不适应。但没关系。我们认为,这种写作方式能够大多数读者的沉浸其中并打开学习通道。对于剩下的读者,我们相信我们的故事很有说服力,因此大多数读者将能够理解我们的比喻。

如果你可以适应这些基本规则,那本书是为你量身打造的。语言的这种组合将会吸引你并使你高兴。作者的组合将带你领略单个作者所不能达到的境界。该说的都说了,该做的都做了,我们的目标是让你觉得这本书可以使你成为一个更好的程序员,提高你的编码能力,而无论你选择哪种语言。

当你有机会使用一种以上的语言时,语言学习会变得更加相关。事情似乎肯定要朝那个方向发展的。成为一个语言高手会很酷。这本书会讲述静态类型、动态类型和五个不同的编程模型。在第一本《七周七语言》中,最令人欣慰的事情是在博客和播客的相关讨论很激烈。当你阅读这本书时,我们鼓励你问问题,并在别人能看到的地方谈论它们。激发群众的智慧。

在线资源

这本书中显示的应用程序和实例,都可以在Pragmatic Programmers的网站上找到 。你还可以找到社区论坛和勘误表提交表格,你可以报告文本问题,或对未来的版本提出建议。


这里提到的人值得尊敬。他们中的许多人正在塑造大家思考编程的方式,还有一些仍在孜孜不倦、无怨无悔的工作,以使得这本书值得一读。

本书中的“明星”就是七个编程语言。我们非常感谢那些语言的创造者,也感谢那些帮助新手进步的人。

Lua

我们要感谢Roberto Ierusalimschy,是他认识到一个单一的、统一的抽象模型的重要性。Lua的表格是多才多艺的,足以构建对象系统、自定义数据结构,甚至是视频游戏。底层干净的实现使得在任何地方运行Lua、将其嵌入到自己的项目中、以及以新的方式拓展它,都成为一件轻而易举的事情。特别感谢Matthew Wild为我们带来了一个经验丰富的Lua开发者的看法。你在现实世界中的来之不易的经验帮助我们坚持到底。

Factor

我们要感谢Slava Pestov采取的串联编程模型,并让它在一个功能齐全、实用的环境中茁壮成长。发现这种编程思路真是令人极度兴奋。有一个能在其上建立真正的应用程序的平台,使得该语言更有价值。

特别感谢John Benediktsson审校这一章,他是在Factor社区最活跃、最有帮助的成员。他的知识、快速的响应,以及热心助人,真是令人鼓舞。

Elm

谢谢你,Evan Czaplicki。你已经为你的用户创建了“太虚幻境”。感谢你在旅行之中帮助我们,即便你自己要在旧金山的公寓和布达佩斯酒店之间来回穿梭。

Elixir

José Valim已对本书产生了如此深刻的影响。你在前言中的话激励着我们,你发明的Elixir语言深深吸引我们。你总是以谦卑和尊重的态度跟人相处。 我们也想特别地大声感谢 Eric Meadows-Jönsson。你对Elixir一章的审校表现出超高的水准。是你使我们的代码保持在Elixir 1.0的水平。我们的读者对此表示感激,我们也对此表示感谢。我们期待你做出更伟大的事情。

Julia

Bruce第一次听说Julia是在伦敦的一次会议上。他开始把该语言纳入,并且组织这本书的作者团队。有几个人提到Julia,但我们一直担心会没有足够的思路来推动我们试图讲述的内容。哦,是我们错了。感谢Jeff Bezanson、Stefan Karpinski、Viral Shah和Alan Edelman,你们的社区已经从一群孤立的、仅仅知道Julia的陌生人,变成了一个关心语言和各个成员的“大家庭”。这种快速改变不是偶然的。你们在整个过程中都极其有帮助性并能够快速响应。

miniKanren

还有另外一门语言得益于在伦敦举办的CodeMesh大会。在几年前,早在本书的思路还没有成型的时候,Bruce就看到过David Nole的Core.Logic,,并且他看到这不止“仅仅是另一个Clojure框架”。这是一个独特而有趣的编程模型。不停地编写面向对象程序并不能使你变得更聪明,真正的成长需要探索未知,有时是要接触根本不同的概念。感谢David以及你的灵感和你的支持。

还要感谢Stuart Halloway,他是我们的好朋友,也是Clojure的最忠实和热情的“管家”之一。

Idris

我们想冒昧地猜测,这里许多语言都将令读者感到震惊,虽然有时这种感觉颇为不快。说实话,有时候一些语言甚至令作者团队感到震惊。感谢Edwin Brady走在实用和学术之间的边缘。你已经创建了一门语言,它有大胆的目标和令人惊叹的执行能力。

虽然其中一些我们都要感谢的人,但还是有一些各个作者想要单独感谢的人。

Bruce Tate的致谢

编写技术书籍并不像以前那样合算了,即使是编写出很成功的书籍。这些真心愿意写书的人,他周围的人常常为此做出了牺牲。Maggie、Kayla 和 Julia(是人名,不是语言名),你们经常是我督促的目标,但你们也激励着我,当我低沉时也给我以鼓励。

Terry Cole,你是我见过的最勇敢的人之一。每本书的一部分都是源自你的使命,当我拖拉之时,我可以打开其中一个孩子的照片,我就会又准备开始写了。

Fred Daoud的致谢

在《七周七Web框架》出版之后,我想我很长时间不会写作了。当Bruce邀请我参加编写新的“七语言”时,我很荣幸,我无法拒绝。我非常感谢能有机会与这样一个伟大的作者团队一起工作,去发现一些奇妙的、挑战想象力且能带来编程快感的语言。

对我个人而言,在这里要感谢我的妻子Nadia,她是我所认识的在宇宙中(及以外)最美丽的人。我生命中最美好的时光都是与你一起度过的,这使我避免了在计算机上花费太多时间。

Ian Dees的致谢

致我的妻子Lynn,以及我的女儿Avalon和Robin,谢谢你们,你们是令人震惊的、有创意的人。

致我的同事、作家Bruce、Fred和Jack,能成为这个写作团队的一员是一种荣誉。谢谢你们邀请我加入。你们的工作确立了很高的标准,为了达到(或试图达到)预期标准,我成为了一个更好的程序员和作家。

致我工作中的队友,感谢你们的高标准严要求,以及忍受偶尔的吵嚷:“你必须要看一下Lua/Idris的这个新功能……”

Jack Moffitt的致谢

我又一次以相同的方式生活了九个月。还在写完《七周七Web框架》之前,Bruce就来找我要帮助写这本书。我对语言的热爱,以及以前和Bruce、Fred和Pragmatic Programmers出版社一起工作的乐趣,使得我很容易地决定开始写另外一本书。

我要感谢我的妻子Kim、我的儿子Jasper和我的女儿Beatrix对我的支持。Kim是我想法的“共鸣板”,她一如既往地帮助我的写作工作。我的孩子们则激励着我一直认真工作。

本书的写作团队集体致谢

所有这一系列的书需要比一般的技术书籍多一些的付出。这些书的作者一直在先于读者一步去学习很多新的概念。我们还需要审校者的出色的支持,而他们所得到的回报,也仅仅是在此表示感谢。 通常情况下,审校者阅读一两章内容达到两遍以上。我们要感谢John Benediktsson、Jeff Bezanson、Edwin Brady、Erin Chapman、Evan Czaplicki、Alan Edelman、John Heintz、Daniel Hinojosa、Carsten Jørgensen、Stefan Karpinski、Eric Meadows-Jönsson、Ted Neward、David Nolen、Viral Shah、Brian Sletten、José Valim、Matthew Wild和Simon Wood。没有你们,我们真的不可能完成这本书。

我们知道可能会漏掉一些人,所以如果你的名字没有出现在这里,请接受我们最诚挚的歉意。我们感谢你的努力。

我们也想感谢出版团队。我们知道版权页已经有你们的名字了,但我们也想在这里再次感谢你们:编辑Jackie Carter、文字编辑Liz Welch、索引制作Seth Maislin,以及产品经理Janet Furlow。整个行业都知道,Pragmatic Programmers出版社的图书很特别,你们帮助我们一直保持这样的高品质标准。再次感谢!

最后,我们想感谢Dave和Andy的信念。你们已经建立了一个专门的工作场所,这是作者能感到尊敬和赞赏的好地方。我们写作,因为我们喜欢它;我们喜欢写作,因为你们使这里成为了一个特殊的地方。

各位,向你们致以我们的爱和尊重。

谢谢!


Ian Dees

2004年,我在和一堆纠缠的像迷乱的丛林一样的硬件测试代码搏斗。我们当时使用的脚本引擎看起来像是神龛上的圣物:自带光环,功能强大,好像我们马上就能用它赚大钱一样。但是真正用起来才知道,这简直就是一个坑。

它的plugin API就像是山坡上滚下的巨石一样,将无可避免地碾碎我们的代码。时间紧急,我们已经没有退路了。每次系统崩溃的感觉就像胸口被人捅了一刀。

就在这时,Lua就像《夺宝奇兵》里的印第安纳•琼斯一样,高举皮鞭冲了出来,带着一丝幽默,而又勇猛无比地解决了我们的问题。有了Lua,一切都不一样了。

这个项目又继续了好几年,Lua一直都处于核心位置。我会一直记得这个轻巧而又易于移植的小语言是如何颇具风格地打败了它的竞争敌手。

听起来很有趣,是吗?那我们就开始吧。

项目刚开始的时候,构建系统的配置文件写起来很麻烦。用来描述测试输入和输出的文件格式不是很具表意性。

以CSV文件为例,假设你需要描述一个电子游戏中的角色和车辆,你的CSV文件有可能会是这样的:

name,   treasure1, treasure2, treasure3, treasure4, treasure5
knight, -1000,     +200,        --,         --,          --

现在,假设你需要在游戏中加入一辆正方形的车,你需要确保这辆车的宽变化时高必须同时改变:

name,       width,        height
mine cart, $cube_size, $cube_size

问题就来了:CSV无法表示集合或者约束条件。要想表示上述车辆,你要么得加一个很少会用到的额外的列,要么得自己实现(并且维护!)一个CSV的方言。现在我们来看看如果用Lua的话,以上配置可以写成什么样子:

Monster{
  name      = "knight",
  treasure = {-1000, 200}
}

local cube_size = 20

Vehicle{
  name     = "mine cart",
  width     = cube_size,
  height = cube_size
}

漂亮。仅此一招,CSV无法表示集合或者约束条件这两个问题就同时都解决了。这都要归功于一个精确地实现了这两个语言特性的语言。你还可以在关卡设计中给怪兽赋予行为:

Monster{
  name = "cobra",
  speed = function() return 10 * damage_to_player() end
}

这样,我们就描述了游戏中的敌人的属性,在属性旁边还可以给它加上定制化的行为。

第一天,我们会安装Lua并做一些简介。你将会学习Lua的基本数据类型并且写几个简单的Lua程序。

第二天我们将深入了解Lua的table。Lua之所以能够具有很好的表意性,关键就在于table。Lua的table就像数组和字典的结合体,其应用场景小到配置文件,大到自制对象系统。第二天我们还会涉及Lua强大的并发性。

第三天,我们会做总结,以符合Lua的设计初衷的风格来使用它:作为一种表意性很强的声明式语言和快速的、底层的C代码结合工作。具体来说,我们会写一个音乐播放器,它能够读取声明式的乐谱,并在你的电脑上实时播放。

先说今天的事,我们会学习一点Lua的基础知识。然后,我们会用Lua的数字、字符串、布尔、函数和条件语句来写一些简单的程序。你会觉得这些东西都很熟悉,不过Lua把它们表现得更平易近人。

Lua是一门基于table的编程语言,它的核心抽象层简单而强大,你可以用它来实现自己的范式——过程式、面向对象、事件驱动等。

Lua的table很适合基于原型的面向对象编程。在这种编程风格中,类和实例不是割裂的概念。你不会先绘制蓝图(类)然后再依据蓝图构造很多个物体(实例)。

在基于原型的面向对象系统中,你先创建一个实例。这个实例看起来像你想要的那个对象。然后你再把这个实例复制很多份,对每一份做一些定制化。这样的面向对象系统和传统的基于类的面向对象系统一样强大,但是更简单。

易于移植是Lua设计之初的目标之一。Lua的设计者们严格坚持只使用ANSI C的一个适用于多种编译器和多个操作系统的子集。在我所使用的某个受限的嵌入式系统上,我只能编译成功两个脚本语言,Lua就是其中之一,足见Lua的作者们在可移植性上所做努力之成功。(如果你好奇的话,另一个是REXX[1]。)

最有趣的安装Lua的方式就是用源码自己编译[2]。如果你比较着急的话,也可以下载预编译好的二进制包,这种方式受支持的操作系统有十多个[3]

就像很多其他的脚本语言一样,Lua也支持交互式的REPL(Read-Eval-Print-Loop,读取-求值-输出循环)。在命令行输入lua来启动:

$ lua
Lua 5.2.3 Copyright (C) 1994-2013 Lua.org, PUC-Rio
>

请注意我们使用的是Lua 5.2.3,这是本书写作时的最新版本。本章的代码多数都可以在更低的Lua版本下运行,不过大量的测试是在Lua 5.2版本下进行的。

本章的多数代码示例都在REPL中完成。你可以把>或者>>后面的内容输入命令行。

我们在REPL中输入一个数值吧,比如我最喜欢的探险电影摄制的那一年:

> 1989
stdin:1: unexpected symbol near '1989'

有意思,Lua并不会把输入的数值打印出来。这很容易解决。你可以使用print()或显式的返回该数值,或者加上一个=:

> print(1989)
1989
> return 1989
1989
> =1989
1989

如果你想要退出REPL,只需要输入Ctrl- D,不过先别退出,我们还要试一些别的代码。

Lua的语法友好,且易于接近,不需担心分号或者空格该写在哪的问题。实际上,空格对Lua来说不太重要。以下两个语句的输出是一样的:

> print "No time for love"
No time for love
> print
>> "No time for love"
No time for love

两个语句之间甚至都不需要换行:

> print "No time" print "for love"
No time
for love

Lua的类型也是一样的易用。

和很多其他脚本语言一样,Lua也是动态类型的,也就是说源码中的变量不需要声明类型,而在运行时才会决定其具体类型。Lua有一些基础类型,和你的预期应该相差不远:数字、布尔值和字符串:

> =3.14
3.14
> =true
true
> ="The dog's name was 'Indiana!'"
The dog's name was 'Indiana!'

你在想整数在哪儿?不用想了,Lua没有整数。如果你安装的是标准的Lua,那只有64位的浮点数,这和JavaScript一样。(有一个例外:如果你使用不支持浮点运算的嵌入式系统,也可以通过用源码编译Lua来启用整数。)

字符串可以写在单引号内,也可以写在双引号内。可以用反斜杠来转义特殊字符或者不可显示的字符:

> ='Separated\tby\t\ttabs'
Separated        by               tabs

可以用..来做字符串拼接:

> ='fortune' .. ' and ' .. 'glory'
fortune and glory

可以用#来取字符串的长度:

> =#'professor'
9

在Lua中,nil具有自己的类型,它表示“找不到”或者“不存在”。(也可以用它从集合中删除元素,第二天就能看到。)

> =some_variable_that_does_not_exist
nil

现在你已经了解Lua的基础了,下面我们把它们组合进表达式里去。

Lua里的数学表达式和其他语言中的看起来一样。就如同你所预期的一样,乘法和除法的优先级高于加法和减法,你可以用括号来组合数学操作。

> =6 + 5 * 4 - 3 / 2
24.5
> =6 + (5 * 4) - (3 / 2)
24.5
> =(6 + 5) * (4 - 3) / 2
5.5

Lua也支持乘方(^)和取模(%)。

> =1899 % 100
99
> = 2 ^ 3
8

Lua的布尔操作是用and、or和not这三个关键字来做的。很方便的是,Lua的逻辑表达式是会“短路”的,也就是说只有在必需的时候Lua才会对布尔表达式的两部分都求值。

> =not ((true or false) and false)
true
> =true or spill_antidote()
true

(以上代码运行的时候,spill_antidote这个函数不会被执行。)

可以用==和~=来比较任意两个值相等或者不相等。用来比较大小的<、<=、>和>=只对字符串和数字适用:

> ='cobras' < 'rats'
true
> =#'cobras' < #'rats'
false
> =42 < '43'
stdin:1: attempt to compare number with string
...
> =true < false
stdin:1: attempt to compare two boolean values

我们已经学习了一些数据类型和表达式。让我们用函数给它们带来活力吧。

 

Lua的函数有另一个很方便的特性,那就是尾递归优化。当一个递归函数对自己的调用是其所做的最后一件事时,该函数就会被尾递归优化处理:

以上的函数会把很多其他的脚本语言噎死;比如Google Chrome的JavaScript引擎在执行以上代码时会发生栈溢出。而Lua则可以正确地把尾递归优化为goto语句,并完成计算。

Lua的函数定义看起来和其他的常见脚本语言很相似:

> function triple(num)
>> return 3 * num
>> end
>>
=triple(2)
6
function reverse(s, t)
     if #s < 1 then return t end
     first = string.sub(s, 1, 1)
     rest = string.sub(s, 2, -1)
     return reverse(rest, first .. t)
end

large = string.rep('hello ', 5000)
print(reverse(large, ''))

严格来说,函数的名字不是必需的。如果你输入以下的代码也是可以的:

> =(function(num) return 3 * num end)(2)
6

在Lua的世界里,函数是一等公民;它们和其他值一样,可以被赋值给变量,可以被当成参数传递给其他函数,也可以被保存在数据结构中。

例如,你可以写一个叫作call_twice()的函数,它接收一个叫作f()的参数,并且返回一个叫作ff的函数,ff会调用f两次:

>
> function call_twice(f)
>>      ff = function(num)
>>            return f(f(num))
>>    end
>>    return ff
>> end
>
> function triple(n)
>> return n * 3
>> end
>
> times_nine = call_twice(triple)
>
> =times_nine(5)
45

能够把函数当成数据来用是Lua的强大功能,也是其简洁性的重要来源。我们接下来会看到更多的例证。

灵活的参数

调用函数的时候,传入的参数太少了会怎么样呢?有些语言会抛出一个错误。其他的语言,比如Haskell,会返回一个新的函数。而Lua则简单地把所有未传入的参数赋值为nil:

> function print_characters(friend, foe)
>>    print('*Friend and foe*')
>>    print(friend)
>>    print(foe)
>> end
> print_characters('Marcus', 'Belloq')
*Friend and foe*
Marcus
Belloq
> print_characters('Marcus')
*Friend and foe*
Marcus
nil

多余的参数则会被忽略掉:

> print_characters('Marcus', 'Belloq', 'unused')
*Friend and foe*
Marcus
Belloq

你也可以写可变参数的函数,也就是说函数的参数数量可以是不固定的,只要把函数的最后一个参数标为省略号(...)即可:

> function print_characters(friend, ...)
>> print('*Friend*')
>> print(friend)
>>
>> print('*Foes*')
>> foes = {...}
>> print(foes[1])
>> print(foes[2])
>> end
>
> print_characters('Marcus', 'Belloq')
*Friend*
Marcus
*Foes*
Belloq
nil

尾递归

> print_characters('Marcus', 'Belloq', 'Donovan')
*Friend*
Marcus
*Foes*
Belloq
Donovan

我们把参数列表赋值给foes这个table,在这里我们仅是把table当成数组使用。关于Lua的table的这个特性我们会在第二天接触得更多。

多个函数返回值

和参数类似,函数的返回值也可以有多个,你可以选择使用所有的返回值,或者忽略其中的一部分:

> function weapons()
>> return 'bullwhip', 'revolver'
>> end
>
> w1 = weapons()
> print(w1)
bullwhip
>
>w1, w2 =weapons()
> print(w1)
bullwhip
> print(w2)
revolver
>
> w1, w2, w3 = weapons()
> print(w1)
bullwhip
> print(w2)
revolver
> print(w3)
nil

和参数的规则一样,未使用到的返回值会被忽略,未使用到的变量会被赋值为nil。

Lua不像Python或者Ruby一样在语法级别方面支持具名函数[4]。不过你可以通过把table当参数传递给函数来获取类似的效果:

> function popcorn_prices(table)
>>    print('A medium popcorn costs ' .. table.medium)
>> end
>
> popcorn_prices{small=5.00,
>> medium=7.00,
>> jumbo=15.00}
A medium popcorn costs 7

上例中,大括号中的爆米花大小和价格就是table(在这里Lua允许我们不给函数调用加小括号)。函数体可以通过点号读取table中的某个特定的值:table.medium。

你可以通过函数构造出很多东西,甚至是一门编程语言!不过为了方便起见,我们还是先看一下控制流程。

Lua内建的流程控制包括if语句、两种for循环,以及while循环。

if语句可以对应一个else分支,0到多个elseif分支。和某些脚本语言不同,Lua的if语句是没有返回值的;你需要把结果值存在变量中或者打印出来:

> film = 'Skull'
>
> if film == 'Raiders' then
>>  print('Good')
>> elseif film == 'Temple' then
>>  print('Meh')
>> elseif film == 'Crusade' then
>>  print('Great')
>> else
>>  print('Huh?')
>> end
Huh?

for循环遍历一系列的数值(步进值是可选的):

> for i = 1, 5 do
>> print(i)
>> end
1
2
3
4
5
> for i = 1, 5, 2 do
>> print(i)
>> end
1
3
5

你也可以使用for循环来遍历集合,这部分我们会在详细了解table时再讲。

Lua的最后一个内建流程控制是while循环(以及它的近亲repeat循环,你会在习题中用到它):

> while math.random(100) < 50 do
>>  print('Tails; flipping again')
>> end
Tails; flipping again
Tails; flipping again

Lua并不会限制你仅能使用if、for和while这“三大招”。如果你把它们和高阶函数结合起来使用,就可以构造出你的程序需要的任意流程控制。这正是你在第一天的习题中需要做的事。

在前面的例子中,我们已经见过变量了,但是我们只是粗略地一笔带过。现在我们来仔细谈一下变量。

比较奇怪的一点是,Lua的变量默认是全局的:

> function hypotenuse(a, b)
>> a2 = a * a
>> b2 = b * b
>> return math.sqrt(a2 + b2)
>> end
>>
=hypotenuse(3, 4)
5>
=a2
9 -- WHOOPS!

或许你会希望a2这个临时变量不会泄露到函数之外。值得庆幸的是,我们只需要给局部变量的声明前面加上local关键字就可以了:

> function hypotenuse(a, b)
>>  local a2 = a * a
>>  local b2 = b * b
>>  return math.sqrt(a2 + b2)
>> end

最开始的时候我对于Lua的变量默认不是local这一点感到很惊讶。不过后来我发现这样做是有合理的原因的。如果我们真的想防止不小心创建出全局变量,可以用Lua的table来实现这一点;第二天我们会做这件事。

到目前为止,我们都是把表达式输入REPL中。用这种方式学习Lua是最好的,这样可以在输入代码的同时就把程序构造出来了。

然而,接下来我们要做一些练习题。你或许会想先用文本编辑器把代码写好,然后从命令行运行。你只需要把代码保存为.lua后缀的文件,然后再用lua这个命令,也就是启动REPL的命令来执行就可以了:

lua my_program.lua

这样做不如使用REPL那么具有交互性,不过现在你要自己出去闯天下了,把代码保存到文件里使得修改错字更容易。

今天,你学习了Lua的基本语法。你看到了定义函数有多么容易,包括很高端的可以接受函数作为参数的高阶函数。你现在已经掌握了足够的Lua的知识,可以写一些简单的程序了,而且你马上就要这样做。

现在,你可能会觉得Lua是一个易于使用的脚本语言,但是也没有什么鹤立鸡群之处。这也是我刚刚接触Lua时的反应。

然后我发现了使得Lua的表意性得以展现的杀手级特性:table。第二天,我们会谈到table到底有什么特别的。

找到

练习(简单)

练习(中等)

练习(困难)

function add(previous, next)   
 return previous + next   
end 
reduce(5, 0, add) -- add the numbers from 1 to 5 

-- We want reduce() to call add() 5 times with each intermediate   
-- result, and return the final value of 15:   
--   
add( 0, 1) --> returns 1; feed this into the next call 
add( 1, 2) --> returns 3 
add( 3, 3) --> returns 6 
add( 6, 4) --> returns 10 
add(10, 5) --> returns 15

今天,我们要介绍使得Lua别具风格的两个概念:table和协程。和很多其他的基于原型的面向对象语言类似,Lua用table定义数据,而协程则是用来定义控制流程的。二者虽然都很简单,但是非常强大,它们可以作为很多东西的基础,Lua的对象系统或者你自己定义的领域特定语言都要依靠它们来实现。

我们先来讲table。

当学习一门新的编程语言的时候,你总是会被一大堆的数据结构弄得头晕:数组、元组、矢量、列表、字典等。它们都有自己的API、语法、奇诡之处,性能表现也不尽相同。

这些集合类型都很有用,但是当我最初尝试学习一门新语言的时候,我通常考虑的是一些更基本的事情:(1)如果我想通过名字访问数据,我该把数据放在哪儿?(2)如何把数据按照某种顺序存储?

和Python的字典(dictionary)或者Ruby的哈希(hash)类似,Lua的table是键值对的集合。你通过大括号来创建table,这样的表达式在Lua里叫作table构造器(table constructor):

> book = {
>> title = "Grail Diary",
>> author = "Henry Jones",
>> pages = 100
>> }

要从table中读取数据,只需要写下table的名字,一个点号,然后是你想要读取的键:

> =book.title
Grail Diary

添加和修改数据的方式和读取数据的方式类似:

> book.stars = 5 -- new item
> book.author = "Henry Jones Sr." -- modified item

如果键里包含空格或者小数点该怎么办?如果键是在运行时计算出来的该怎么办?这些情况下,把键放在方括号内就好了:

> key = "title"
> =book[key]
Grail Diary

以这样的方式,你可以使用任何类型的数据作为键:布尔、函数,甚至是table。不过多数情况下我们用的是字符串或者数字。

要从table中删除一个元素,只要把该元素的值设置为nil就可以了:

> book.pages = nil

Lua并不自带打印table的函数。值得庆幸的是,你可以自己定义一个函数来打印以上示例代码里的table。让REPL处于运行中,切换到文本编辑器,把以下的代码保存到util.lua里:

  lua/day2/util.lua
  function print_table(t)
➤    for k, v in pairs(t) do
       print(k .. ": " .. v)
     end
  end

pairs()是Lua的内建函数。说得更具体一些,它是一个迭代器(Iterator),可以很好地和for循环一起工作。如果你想知道构造一个迭代器的更多细节,请查看在线Lua书的相关章节[5]。简单来说,pairs()会返回一个新的函数,for循环会反复地调用这个函数直到它返回nil。

上面的print_tables函数无法正确地处理嵌套多层的table,或者其他复杂一些的情况。不过目前来说,它够用了。你可以使用dofile()函数把它加载入REPL:

> dofile('util.lua')

或者你也可以通过使用-l参数在REPL启动之初就把需要的库预加载进来:

$ lua -l util

dofile()简单又粗暴,它仅仅是把你指定的文件吞进去。它不会检查文件是否已经加载过,也不会允许你指定去哪里寻找要加载的文件。稍后我们会使用Lua的模块系统,它除了可以做到上面提到的两件事之外,还可以做更多的工作。

下面我们来调用print_tables(),并把前面定义的book table做参数传入:

> print_table(book)
author: Henry Jones
title: Grail Diary
pages: 100

到目前为止,table看起来就像普通的字典。但是table可并不是只能处理键值对。

有时,你需要以某种有序的方式存储数据。其他的语言会给你提供列表或者数组,它们的语法和API与字典是不同的。

在Lua里,不需要这好几种抽象。对Lua来说,数组只是键值对存储结构的一个特例,键是连续的数字。你可以用之前用到过的语法来创建数据,只是不要写键就好了:

> medals = {
>>     "gold",
>>     "silver",
>>     "bronze"
>> }

你可以通过熟悉的方括号来读写数组的内容:

> =medals[1]
gold
> medals[4] = "lead"

请注意,Lua的数组下标是从1开始的,数学家和正常人也是这样计数的。

现在你有可能在想,“Lua数组性能不太好吧?”在多数语言中,字典比数组更慢;计算一个字符串的哈希值比给一个数字加一要慢得多。

值得庆幸的是,Lua运行时给数组提供了特殊的快车道[6]。只要你连续用数字作键向字典中添加数据,Lua就能很高效地存储和访问数据。

Lua中的数组和字典并不是互斥的。你可以在同一个table中混用两种风格,Lua自有办法把数据高效地存储起来。有些程序员遵循一个惯例,即把数组和字典用分号隔开:

> ice_cream_scoops = {
>>  "vanilla",
>>  "chocolate";
>>
>>  sprinkles = true
>> }
>>
=ice_cream_scoops[1]
vanilla
> =ice_cream_scoops.sprinkles
true

能够使用字符串或者数字作为键是很有趣的技巧。不过如果你需要自定义查找逻辑该怎么办呢? 这时,我们就需要用到Lua的metatable。

到目前为止我们见过的所有table,如果你给它一个键,Lua就给你找到一个值。这个查找逻辑是Lua内建的。

有时,这种默认行为未必是你的程序所需要的。比如你想在找不到键所对应的值时不返回nil,而是返回其他的默认值;或者你想把读写的历史记录到某一个table,你可以通过使用一个叫作metatable数据结构来实现上述两种行为。

metatable这个名字听起来有些抽象,它是“元表”的意思。如果你熟悉JavaScript的原型或者Python的特殊双下划线方法名,你会发现Lua的方式也很熟悉。[7]目前来说,你认为metatalbe意味着“自定义行为”就好了。

Lua中的每一个table都有一个metatable,其中可以包含读写键值对的函数,可以包含用来遍历table内容的代码,也可以重载某些操作符。多数table的metatable是nil,这就意味着table的很多操作用的都是Lua的默认实现:

> greek_numbers = {
>> ena = "one",
>> dyo = "two",
>> tria = "three"
>> }
>
>=getmetatable(greek_numbers)
nil

不过你可以很简单地覆盖Lua的默认行为。Lua默认打印table的方式实在太差劲了,其战斗力就像用赶牛的鞭子去和M1917机枪对决一样。

比如Lua把table打印到标准输出的方式就非常地“简洁”:

> =greek_numbers
table: 0x7fec0ad002b0

如果能够不用调用额外的函数就让打印的字符串里包含键和值就好了。幸运的是,这是可以做到的。

我们只需要创建一个metatable,在里面写一个名字叫作__tostring的函数就好了。只要有人试图打印某个table,Lua就会调用这个函数。我们可以把之前写的print_table函数做一点修改,让它返回一个字符串而不是输出到命令行。

把下面的代码添加到util.lua:

lua/day2/util.lua
function table_to_string(t)
    local result = {}

    for k, v in pairs(t) do
      result[#result + 1] = k .. ": " .. v
    end

    return table.concat(result, "\n")
end

这个新版的函数把所有键值对的字符串表达形式存在一个列表里,最后把它们一次性全部拼接到一个字符串里去。如果table很大的话,这样做会比一个一个地拼接字符串快。

现在,在REPL里重新加载util.lua:

> dofile('util.lua')

最后,我们可以把自定义的输出逻辑集成进来了:

> mt = {
>> __tostring = table_to_string
>> }
>>
setmetatable(greek_numbers, mt)
>>
=greek_numbers
ena: one
tria: three
dyo: two

这样我们就实现了自定义table输出为字符串的行为。现在,如果我们调用print(),Lua就会去metatable里面寻找__tostring,并且找到我们自定义的函数。最终打印的结果就变的可读多了。

现在我们已经搞定了简单的自定义函数,接下来试试复杂一些的。

读和写

Lua的table是非常宽容的,如果你试图去读取一个不存在的键,也并不会有什么不好的事情发生,只是会得到一个nil。假设你想创建一个更加严格的table,读取不存在的键或者覆盖已存在的键都会导致运行时错误。

要用Lua做到这件事,只需要很简单的几步:

1.把你想要的自定义的读写逻辑写到两个函数里。

2.把这两个函数存储在一个table里,分别命名为__index和__newindex。

3.把上一步中创建的table设置为你的数据的metatable。

我们先来看第三步,把以下的代码写入strict.lua:

lua/day2/strict.lua
local mt = {
  __index = strict_read,
  __newindex = strict_write
}

treasure = {}
setmetatable(treasure, mt)

strict_read()会从一个table中读取数据,这个table是私有的,不会被其他代码直接访问到。把以下代码加入strict.lua:

lua/day2/strict.lua
local _private = {}

function strict_read(table, key)
  if _private[key] then
    return _private[key]
  else
    error("Invalid key: " .. key)
  end
end

Lua会把table和key这两个参数传入这个函数。我们需要做的是仅在key存在的情况下返回数据。

strict_write()函数也是类似的,它需要检查私有table是否已经包含这个键了。把下面这段代码写到strict_read()后面:

lua/day2/strict.lua
function strict_write(table, key, value)
  if _private[key] then
    error("Duplicate key: " .. key)
  else
    _private[key] = value
  end
end

用dofile()或者-l参数把strict.lua加载入REPL。然后,把宝物放入你的珍宝箱:

> treasure.gold = 50
>
> =treasure.gold
50
>
> =treasure.silver
strict.lua:8: Invalid key: silver
...
>
> treasure.gold = 100
strict.lua:16: Duplicate key: gold
...

到目前为止,我们使用metatable做了自定义的查找逻辑,还做了自定义的输出格式。你也可以用metatable去重载数学、逻辑还有比较运算符。做这些事的方法是一样的:把函数放到一个table里,给它们特殊的键名,比如__add和__sub,然后通过调用setmetatable()来把自定义的行为绑定到你的数据上去。

接下来,你将会看到metatable有多强大:我们要自己构建一个基于Lua的原始类型之上的面向对象系统。

Lua有它自己的面向对象语法。不过我接下来要展示在Lua强大的抽象机制下自定义另一种面向对象风格有多容易。然后我们会观察自制的面向对象风格和Lua的有何区别。

面向对象编程核心的观念就是自制的对象之间互相发送消息。你接触过的Lua知识就已经够用了,就是普通的table和函数。

假设你在做一个游戏,在游戏的最后一关你想要让玩家对决一个Boss等级的反派。游戏引擎通过发送take_hit()的消息来响应对反派的攻击。

Lua中的函数其实就是普通的数据,可以被存储起来,也可以被当成参数传递。所以,你可以把take_hit写成一个函数,并把它存储在反派的状态的附近:

dietrich = {
   name = "Dietrich",
   health = 100,

   take_hit = function(self)
      self.health = self.health - 10
  end
}
dietrich.take_hit(dietrich)
print(dietrich.health) --> 90

游戏中的反派应该不止一个。如果这些反派都是共用同一份take_hit()代码,我们就需要知道该让谁掉血。这就是传入self参数的目的。(等一下我们会看到隐藏它的方式。)

请注意我们暂时还没有用一个叫作Villain(反派)的类来创建实例,而是用一个table包含了反派的数据。如果我们想要创建一个新的反派,就需要初始化这个反派的字段值,或者从一个已存在的反派那里拷贝数据。

clone = {
   name = dietrich.name,
   health = dietrich.health,
   take_hit = dietrich.take_hit
}

如果我们在开始攻击一个反派之前给它创建了一个克隆,我们就可以发现它们确实是两个独立的对象:

print(clone.health) --> 100

老是这样手动复制字段值会很累的。我们接下来就解决这个问题。

原型

要想在每次创建反派的时候自动给字段赋值,我们需要写一个函数。为了把代码模块化,我们把和反派相关的函数都写在一个叫Villain的table里。以下是粗略写就的第一版,一会我们会发现它有些问题:

Villain = {
  health = 100,

 new = function(self, name)
    local obj = {
      name = name,
      health = self.health,
    }

    return obj
end,

take_hit = function(self)
  self.health = self.health - 10
end
}

dietrich = Villain.new(Villain, "Dietrich")

我们现在有了一个能为我们创建反派的函数。我们不会希望好几百份take_hit()函数被拷贝得到处都是,所以我们把它移到了Villain这个通用table里。不过现在,我们不能再像原来一样使用dietrich了:

Villain.take_hit(dietrich)       --> ok 
dietrich.take_hit(dietrich)      --> error: attempt to call field
                                 --> 'take_hit' (a nil value)

dietrich不再有take_hit()这个成员了。take_hit()现在属于Villain这个对象。如果能像JavaScript一样有一个基于原型的系统就好了,这样在dietrich里找不到某个成员的时候,就会去原型里去找。在讲metatable的时候,我们提到了可以利用Lua的metatable来实现任何我们想要的成员查找逻辑。下面是改版后的new函数:

new = function(self, name)
  local obj = {
    name = name,
    health = self.health,
  }

➤ setmetatable(obj, self)
➤ self.__index = self

  return obj
end,

以上标出的新加的两行把成员查找委托给了Villain这个原型。你可能注意到这段代码和前面使用metatable的代码的不同。之前我们使用特殊命名的函数来实现自定义的行为。而在这里,我们使用了一个table。其实这就是Lua语言体系里“把这个table的成员作为查找时的后备”的意思。

现在调用dietrich的take_hit()函数又可以正常工作了:

dietrich = Villain.new(Villain, "Dietrich")
dietrich.take_hit(dietrich) --> ok

到目前为止,我们只是把同一个反派复制了很多次。我们要如何创建不同种类的反派呢?

继承

基于原型的面向对象系统的一个好处就是你不需要特殊的机制来实现继承。你只需要像前面已经做过的那样复制对象就好了。

比如想要创建超级大反派,他们的盔甲更厚,你只需要创建一个SuperVillain原型,然后开始复制它就好了:

SuperVillain = Villain.new(Villain)

function SuperVillain.take_hit(self)
  -- Haha, armor!
  self.health = self.health - 5
end

toht = SuperVillain.new(SuperVillain, "Toht")
toht.take_hit(toht)
print(toht.health) --> 95

这看起来就比较像完整的面向对象系统了。不过每次都把同一个对象传递两次还是挺烦的。

语法糖

要掌握Lua的面向对象系统的最后一步就是使用语法糖。如果你写的是table:method()而不是table.method(self),Lua会替你隐式地传入self这个参数,这样你就不需要自己显式传递了。

Villain = { health = 100 }

function Villain:new(name)
  -- ...same implementation as before...
end
function Villain:take_hit()
  -- ...same implementation as before...
end

SuperVillain = Villain:new()

function SuperVillain:take_hit()
  -- ...same implementation as before...
end

现在,我们自制的面向对象系统看起来和其他常见语言的很相似了:

dietrich = Villain:new("Dietrich")
dietrich:take_hit()
print(dietrich.health)     --> 90

toht = SuperVillain:new("Toht")
toht:take_hit()
print(toht.health)         --> 95

在第二天中,我们从一个简单灵活的数据结构开始,用它构造出了一个复杂的结构,而全部这些只需要几行代码。接下来,我们会对控制流程做同样的事情。

我们之前的Lua代码都是以串行的方式执行的。你可能在想Lua是如何处理多线程的呢?

Lua不处理多线程。

是的,你没看错。Lua并没有内建的多线程API。不过Lua内建了一套更简单、更容易理解的用于做多任务处理的基本类型:协程。

协程已经存在几十年了。和线程一样,协程让你的程序能够同时执行多个任务。与线程的不同之处在于,协程不是抢占式的。你的代码中必须显示标注出在什么时候可以安全地暂停执行当前任务,让位给其他任务运行。竟然还要烦劳程序员给标注出来,那我们为什么要用它呢?因为它在概念上更简单,而且能够免去很多并行的问题。当你读Lua代码的时候,你明确知道什么时候任务有可能中断,什么时候又不会中断。

协程VS线程

 

如果协程这么好的话,那为什么不是所有语言都采用它呢?因为就像所有的其他语言特性一样,协程也要涉及权衡。获取简洁性和正确性上的优势的同时,我们也会损失多核处理的能力。

一个包含有许多协程的Lua进程在任何一个时刻只能使用一个核。不过,在一个进程里倒是可以很容易地开启多个Lua解释器,并让解释器运行在不同的核上。[8]另外一个关于协程需要注意的事是它与阻塞式I/O操作合作不好。如果你的Lua程序使用了协程,你最好使用非阻塞的select()函数以及与其相关的函数[9]

单个任务:生成器

我们以一个简单的例子开始学习协程。我们来创建一个协程并给它分配一个任务。这个协程启动时默认处于暂停状态,所以我们开始就可以调用resume()来恢复执行。在协程之内,在每一部分工作做完之后我们会写一些代码把结果返回。

首先,我们来定义一个会运行很久的函数,实际上就是一个死循环。

> function fibonacci()
>> local m = 1
>> local n = 1
>>
>> while true do
>>       coroutine.yield(m)
>>       m, n = n, m + n
>>   end
>> end

这个函数永远不会返回;它每计算出一个新的裴波那切数字就把该数字返回给调用者。yield和返回有什么区别呢?返回之后,我们还可以回到程序中断的位置继续执行。

译者注:yield的概念存在于很多编程语言中。在某些语言中会以关键字的形式出现,比如C#和Python。而在Lua中则是以库中函数的形式出现的,其作用通常是用于生成可迭代数据类型中的一个元素。yield的字面含义为生产或产出,但在编程的语境中并无确切的中文翻译,因而下文中一概保留原文。

要开始运行协程,我们需要先用coroutine.create()函数创建出一个协程,然后调用coroutine.resume():

> generator = coroutine.create(fibonacci)
> succeeded, value = coroutine.resume(generator)
> =value
1

在调用resume的时候,就会开始执行fabonacci(),直到遇到第一个yield()调用就会跳回到调用者处执行,也就是resume()后面那一句。resume()会返回一个状态码,以及yield()被调用时使用的参数。每次调用resume()时,会回到上次yield()被调用之后的地方继续执行。

> succeeded, value = coroutine.resume(generator)
> =value
1>
> succeeded, value = coroutine.resume(generator)
> =value
2

这种方式很适用于耗时较长的计算或者网络操作,你可以把任务分解为较小的子任务来执行,以维持程序的响应性。

协程虽然简单但功能强大,它足以实现类似线程的行为。操作系统的进程调度器通常都要几千行代码,不过接下来你可以用几十行代码写出一个。[10]

我们需要定义几个可以并行执行的顶层函数:

function punch()
  for i = 1, 5 do
      print('punch ' .. i)
➤    scheduler.wait(1.0)
 end
end

function block()
    for i = 1, 3 do
       print('block ' .. i)
➤     scheduler.wait(2.0)
 end
end

然后这样来调度它们:

scheduler.schedule(0.0, coroutine.create(punch))
scheduler.schedule(0.0, coroutine.create(block))

scheduler.run()

以上用到的看起来像线程风格的API并不存在,我们必须要自己把它写出来。我们先来列一个单子,里面包含我们未来需要做的事情,按照要做的时间先后排序。

看看我们调用调度器的代码,这是Lua的模块系统,就像Lua中的其他系统一样,它也是构造在table的基础上的。我们要用这套系统来定义我们的API。

把下面的代码写到scheduler.lua里:

 lua/day2/scheduler.lua
 local pending = {}

 local function schedule(time, action)
➤     pending[#pending + 1] = {
       time = time,
       action = action
     }

     sort_by_time(pending)
 end

我们想让某件事情在未来发生的话,就把它丢到pending数组里,这个数组以时间戳(程序启动到现在所经历过的秒数)排序。

顺便说一下,#pending前置的#是长度操作符。你在第一天使用它获取过字符串的长度。在这里我们可以发现,它也能用在数组上。高亮的那一行是Lua用来给数组增加元素的惯用方法。

需要注意的另一件事是:为了避免命名冲突,我们把这个文件里的schedule以及其他函数都写成了local的。稍后,我们会把想让调用者看到的函数公布出去。

接下来的sort_by_time()函数利用Lua内建的table.sort()函数来给数组排序,它接收一个可选的用于比较数组内两个元素的函数的参数。把以下这段代码写到scheduler.lua的最上面:

lua/day2/scheduler.lua
local function sort_by_time(array)
  table.sort(array, function(e1, e2)
                      return e1.time < e2.time
                    end)
end

协程应该是轻量级的。它们应该在开始运行之后就做它该做的事,做完马上就返回或者yield。所以wait()函数不应该真的等待,而应该向调度器yield:

local function wait(seconds)
  coroutine.yield(seconds)
end

叫作run()的主函数会在没有任务时等待,而在有任务时执行任务。如果执行的任务调用wait(),任务执行所需要消耗的秒数就会被yield给我们。我们会用这个秒数来决定在未来的什么时候继续执行该任务:

local function run()
 while #pending > 0 do
    while os.clock() < pending[1].time do end -- busy-wait

    local item = remove_first(pending)
    local _, seconds = coroutine.resume(item.action)

    if seconds then
      later = os.clock() + seconds
      schedule(later, item.action)
 end
 end
end

当任务执行完时,协程不会yield任何东西回来。这时调用resume()函数将会返回nil,我们也不会再给该任务分配执行时间了。剩下的最后一个需要实现的函数就是remove_first()了,它将会删除并返回数组里的第一个元素:

local function remove_first(array)
  result = array[1]
  array[1] = array[#array]
  array[#array] = nil
 return result
end

现在我们把这个API包装在一个Lua模块里面吧。其实就是一个很普通的Lua table,其中包含着我们想向外界调用者暴露的函数。把下面的代码写到scheduler.lua的最下面:

return {
  schedule = schedule,
  run = run,
  wait = wait
}

现在可以执行了!创建一个叫作punch.lua的新文件,并把以下代码写入该文件的顶部:

lua/day2/punch.lua
scheduler = require 'scheduler'

我们使用require()函数而不是dofile()函数来加载模块。这两个函数类似,不过require()为你做的更多:

然后,把我们在开始时写的punch()和block()这两个函数,以及schedule()和run()函数加载进来。当你执行lua punch.lua时,你应该看到有五次出拳的动作,而格挡的频率大概是出拳的一半。看起来我们更擅长攻击而不是防守。

我们:你为什么写了Lua?

Roberto:在1993年的时候,我在Tecgraf做咨询工作。Tecgraf是我的大学(PUC-Rio)和Pestrobras(巴西石油公司)之间的合作组织。有两个项目都有类似的终端用户配置的问题。

Tecgraf开发了一些小型语言来局部解决这些问题,但是很快他们就意识到需要更强大的语言,比如说需要支持数学表达式、变量、条件表达式,甚至还需要一定程度的抽象能力(函数)。

不过,他们又不想把整个项目和这个新的语言耦合在一起。当时,唯一能满足他们需要的语言是Tcl,不过Tcl的语法对于我们的非专业程序员用户(地理学家和机械工程师)来说太过于费解了。所以,我们一开始就把Lua定位为给程序提供配置的语言。

我们:你最喜欢Lua的哪一点?

Roberto:Lua的目标清晰,野心不大。Lua并不想帮所有人做所有事。如果你的问题不适合用Lua来解决,我会是第一个建议你去使用其他语言的人。语言设计充满了权衡,不同的语言用不同的方式来处理这些权衡,而这对于不同的应用场景和不同的用户来说或许意味着好事,也可能是坏事。一个好的程序员应该知道用不同的工具解决不同的问题。

我们:Lua最适合解决哪种问题呢?

Roberto:我认为Lua最适合于做它创生之初就要做的事,它是真正的脚本。现在,多数人都把“脚本语言”和动态语言混为一谈。但是脚本语言有着更加具体的含义,那就是要把软件中用不同语言编写的部分协调和粘合起来。(想一想游戏的脚本或者电影的脚本就知道我说脚本时是什么意思了。)Lua一直都是以这种理念发展的。

我们:如果可以重来,你会修改Lua的什么特性呢?

Roberto:这个问题不好回答啊。在5.1版(2006年)中实现的新的vararg的机制我就不太喜欢。我经常会想老的机制更好,但是我认为我们已经回不去了。我想要用PEGs来实现模式匹配函数,不过从发展路线图来看,不太可能有机会实现一个新的了。

我们:你所见过的Lua的应用场景中最让你吃惊的是什么?

Roberto:或许是游戏。现在游戏是Lua的主要狭缝市场,不过一开始并不是这样的。我们开始几年根本就没有想过游戏的事。《冥界狂想曲》出现的时候我们都吃了一惊。看到Lua被内嵌入那么多的设备也很让人吃惊,比如键盘、打印机、路由器、相机,以及诸如此类的设备。

多么充实的一天啊!你可以给自己一些鼓励了。你实现了一个面向对象的系统以及一个类似于线程的并行API。而且你的代码还写得紧凑、模块化、易读。

让这一切得以实现的是Lua的易于组合的基本数据结构:table和协程。我们看到了table能够作为数组用,也可以当成字典用;我们还看到了Lua给我们提供了切入点来扩展table的行为。

之后,我们就开始讲协程了,这是Lua并行的方式。尽管协程暴露的API并不多,我们还是可以使用它们来构造复杂、强大的多任务系统。

明天,我们会介绍Lua如何与C++代码交互,并让它生成一些音乐。还记得我们今天写的调度器吗?我们会用它来管理组成音乐的不同声部。

找到

练习(简单)

练习(中等)

–q = Queue.new() returns a new object.   
–q:add(item) adds item past the last one currently in the queue.   
–q:remove() removes and returns the first item in the queue, or nil if the queue is empty.

练习(困难)

用法示例:

retry(
  5,

  function()
    if math.random() > 0.2 then
      coroutine.yield('Something bad happened')
    end

    print('Succeeded')
 end
)

绝大多数情况下,内部的函数会失败;retry()函数应该一直尝试直到它能够成功或者尝试满5次为止。

提示:你或许会需要创建不止一个协程。

你已经开始了历险的旅程,小心谨慎地步入了Lua之门。你已经见识过了Lua数据结构和并行方面的简洁与强大。现在,你要应用学到的知识来构建一个真实的项目了。

今天,我们要用Lua来播放音乐。Lua并没有内建的声效库,不过其他语言实现的倒有不少。我们今天要用到Lua的一个很强大的功能——Lua的C语言接口,我们会用这种方式来控制一个开源的声效库。

有一些伟大的冒险家已经走过这条路了。他们利用Lua的表意性来描述程序的逻辑,用C语言来做性能要求高的部分,并且用到了这一章即将讲到的技巧来把Lua和C黏结在一起。《Adobe lightroom》[11],《魔兽世界》[12]和《愤怒的小鸟》[13]都用到了Lua,它们或者用Lua做内部功能,或者用Lua做面向终端用户的扩展语言。

用电脑做音乐有很多种方式。今天我们用一个C++库制作MIDI(Musical Instrument Digital Interface,乐器数字接口)[14]。一开始,我们会写一些C++代码来展示Lua与其他语言的结合能力。然后我们很快就会回到Lua的世界里,把所有东西都包装到具有表意性的API里。

良友相伴

 

在用软件制作MIDI音乐这条路上有很多良友相伴。Topher Cyll在《Practical Ruby Projects》[Cyl07]这本书里使用到了类似的技巧。Giles Bowkett在Topher的基础上用Ruby和CoffeeScript写了一些算法来写歌[15]

我们的目标没有那么大,只需要能够用最简洁的Lua API播放已经存在的音乐就好了。

在开始今天的历险之前,你需要一些装备。你需要安装以下的程序,并确保它们都可以执行:适用于你的操作系统的C++编译器、用于构建C++的CMake工具[16]、Lua的C头文件和库(应该是与你的Lua一起安装的)、RtMidi声效库[17]、一个MIDI合成器软件,这样你才能听到音乐

不同的操作系统的安装方式不尽相同。下面是针对Windows、Mac和Linux的安装步骤。如果你遇到困难,可以在论坛里给我们留言[18]

Windows

Mac

Linux

下面的安装步骤是针对Ubuntu Linux的,如果你使用其他Linux系统则需要做相应的调整。

我们的目的是要创建一个叫作play的命令行程序,它可以播放我们指定给它的任何歌曲。我们即将发明一种用Lua来写的乐谱。整个系统包括3个部分:

1.一个C++程序,它会开启一个Lua解释器并执行一段由音乐家(就是你!)提供的脚本。

2.一个Lua函数,它可以给MIDI设备发送消息;用C++来写的,但是可以从Lua中调用。

3.一个Lua库,它提供的语法让谱曲变得更容易。

小小解释器

我们从Lua的解释器开始。在你项目的目录下创建一个叫作play.cpp的文件,写入以下代码:

lua/day3/a/play.cpp
extern "C"
{
#include "lua.h"
#include "lauxlib.h"
#include "lualib.h"
}

这段代码可以把Lua的运行时和辅助库引入到C++中。extern "C"为编译器和链接器指明引入的是C代码,而不是C++。

现在,添加一个main()函数,这是命令行的C程序的入口点:

lua/day3/a/play.cpp
int main(int argc, const char* argv[])
{
  lua_State* L = luaL_newstate();
  luaL_openlibs(L);

  luaL_dostring(L, "print('Hello world!')");

  lua_close(L);
  return 0;
}

我们用luaL_newstate()函数创建一个Lua解释器。默认的解释器的设计原则秉承轻量级的原则,所以要引入Lua的标准库需要调用另一个函数——luaL_openlibs()。

一旦解释器被加载之后,我们就可以通过调用luaL_dostring()给它发送一些Lua代码。在我们完工之后,传入的代码会是一段用Lua写成的歌曲。现在,我们只是向命令行打印一些文字。

构建项目

现在我们可以构建项目了。这需要两个步骤:(1)用CMake创建一个工程文件;(2)使用make或者是Visual Studio编译C程序。

只需要给CMake提供一些对你项目的描述就可以了。把下面的内容写入一个叫作CMakeLists.txt的文件里:

lua/day3/a/CMakeLists.txt
cmake_minimum_required (VERSION 2.8)
project (play)
add_executable (play play.cpp)
target_link_libraries (play lua)

如果你的Lua头文件不在系统默认位置,那你或许需要添加一行include_directories(),比如这样:

<strong>lua/day3/a/CMakeLists.txt</strong>
include_directories(/usr/local/include)

现在,在你的项目目录下执行以下命令来告诉CMake去创建工程文件:

$ cmake .

在Mac和Linux下,这条命令会创建一个Makefile,你可以通过make命令来用它构建项目。在Windows系统中,CMake会创建一个.sln文件,你可以用Visual Studio加载该文件并构建。现在就去做这一步吧。

项目构建好之后,在你的项目目录下会出现一个叫作play或者play.exe的文件。如果你使用Windows系统,这样来执行你的程序:

C:\day3> play.exe
Hello world!

在Mac和Linux系统中,执行以下命令:

$ ./play
Hello world!

你看到命令行的输出了吧?太好了!现在,我们来创作音乐吧。

首先我们需要引入RtMidi库。把以下的代码添加到C++代码顶部的extern "C"代码块的右括号后面:

lua/day3/b/play.cpp
#include "RtMidi.h"
static RtMidiOut midi;

RtMidiOut对象就是我们和MIDI生成器交互的接口。我们在这里仅仅是把它放在一个全局变量里。这种数据通常会放到Lua的注册表里,不过对于我们当前的目的来说那就有些小题大做了[28]

现在,我们把main()函数与MIDI合成器连接起来:

 lua/day3/b/play.cpp
 int main(int argc,  const char* argv[])
  {
  ➤  if (argc < 1) { return -1; }
  ➤  
  ➤  unsigned int ports = midi.getPortCount();
  ➤  if (ports < 1) { return -1; }
  ➤  midi.openPort(0);

      lua_State* L = luaL_newstate();
      luaL_openlibs(L);

  ➤  lua_pushcfunction(L, midi_send);
  ➤  lua_setglobal(L, "midi_send");
  ➤  
  ➤  luaL_dofile(L, argv[1]);

      lua_close(L);
      return 0;
   }

以上代码中高亮(译者注:加箭头的)行是新增的。首先,我们用RtMidi的API去寻找正在运行着的合成器(如果找不到就退出程序)。接着,我们启动Lua的解释器。然后,我们注册一个用来播放乐谱的C++函数。最后,我们执行Lua代码并退出解释器。

我们是如何把C和C++代码与Lua连接起来的?Lua使用一个简单的栈模型与C代码交互。我们把函数的存储地址入栈,然后调用Lua内置的lua_setglobal()函数把函数赋值给一个Lua的变量。

你或许注意到我们把luaL_dostring()换成了luaL_doFile()。这行代码可以从文件中加载Lua代码(我们从命令行获取用户输入的文件名;比如play song.lua)。这样,我们就不需要在每次Lua代码有改变的时候都去重新编译C++代码了。

让这里有声音吧

现在就要有音乐了!要播放一个音符,我们需要给MIDI合成器发送两个MIDI消息:一个Note On消息和一个Note Off消息。MIDI标准给每一个消息编了号,并且规定每个消息接受两个参数:音符和速率[29]

这就意味着midi_send()这个Lua函数要接受三个参数:消息编号,以及两个数字型参数。当执行以下Lua代码时:

midi_send(144, 60, 96)

144、60和96这三个数字会被入栈,然后开始执行C++函数。我们需要根据这些参数在栈内的位置来获取它们。在Lua里栈顶的序号是−1,对应最后入栈的那个数字,也就是96。

由于Lua是动态类型的,入栈的值有可能是任何类型的:数字、字符串、table、函数等。不过由于我们是完全能够控制.lua脚本中的代码的,所以我们不会把任何除了数字之外的值压入栈中,这样在C++代码里就可以只处理数字值了。把下面的代码加入C++文件中,放在main()函数上面:

lua/day3/b/play.cpp
int midi_send(lua_State* L)
{
    double status = lua_tonumber(L, -3);
    double data1 = lua_tonumber(L, -2);
    double data2 = lua_tonumber(L, -1);

    // ...rest of C++ function here...

    return 0;
}

如果C++函数需要把数据传递给Lua的话,我们可以把数据入栈并返回一个正数。在上面的代码中,我们返回了0来代表没有入栈任何数据。

更新工程文件

接下来要做的就是把刚才入栈的数字转换成RtMidi能够读取的格式,并把它们传递给合成器。把下面的代码写入你的midi_send()函数的返回语句前面:

lua/day3/b/play.cpp
std::vector<unsigned char> message(3);
message[0] = static_cast<unsigned char>(status);
message[1] = static_cast<unsigned char>(data1);
message[2] = static_cast<unsigned char>(data2);
midi.sendMessage(&message);

现在我们的工程文件需要链接Lua和RtMidi。把CMakeLists.txt文件中targetlink libraries()改成下面这样:

lua/day3/b/CMakeLists.txt
target_link_libraries (play lua RtMidi)

重新构建项目。趁它正在运行,我们来写一个简短的Lua测试程序来播放一个音符:中央C,时长1秒钟。把以下的代码写入one_note_song.lua:

lua/day3/b/one_note_song.lua
NOTE_DOWN    = 0x90
NOTE_UP        = 0x80
VELOCITY     = 0x7f

function play(note)
  midi_send(NOTE_DOWN, note, VELOCITY)
  while os.clock() < 1 do end
  midi_send(NOTE_UP, note, VELOCITY)
end

play(60)

试一下吧!先把MIDI合成器启动,然后执行你的程序:

./play one_note_song.lua

你应该可以听到中央C播放一秒钟。

一曲只有一个音符的歌对于Tenacious D乐队[30]来说或许还好。不过我们还是把目标定得高一些吧。

首先,写歌的时候如果能够不用MIDI音符编号会容易得多,如果能用接近音乐记谱法的方式来写就好了。我们来找一些大家都耳熟能详的乐谱吧,比如《生日快乐歌》或者是《大家早上好》(它诞生于《生日快乐歌》有版权声明之前四十多年[31]

这本书不是《七周七乐谱》,所以如何翻译乐谱的部分我们就一笔带过,直接进入歌曲中音符的部分。如果你想学习乐谱,可以参考ReadSheet这个音乐项目[32]

这首歌的前几个音符是D、E、D、G和升F,它们的长度大部分是一样的(四分之一音节),只有升F例外(二分之一音节,是其他音符的二倍长)。这首歌是用中音C演奏的,中音C在科学记谱法中是4号[33]

我们可以在Lua里用几种不同的方式表示这些音符。现在,我们先简单地用字符串来表示(比如Fs代表升F),后面跟着音度(比如4),最后是音长(比如h代表半音符)。

把下面的歌曲写入good_morning_to_all.lua:[34]

lua/day3/b/good_morning_to_all.lua
notes = {
  'D4q',
  'E4q',
  'D4q',
  'G4q',
  'Fs4h'
}

我们需要能够把这些字符串转换到MIDI的音符编号和长度。我们播放其他歌曲时也需要同样的代码,所以把以下代码写入一个新文件:

lua/day3/b/notation.lua
local function parse_note(s)
 local letter, octave, value =
    string.match(s, "([A-Gs]+)(%d+)(%a+)")

  if not (letter and octave and value) then
    return nil
  end

  return {
    note = note(letter, octave),
    duration = duration(value)
}
end

首先,我们使用Lua的string.match()函数来确保输入的字符串符合我们期望的格式。如果输入合法,我们就调用其他的helper函数来计算MIDI音符和用秒表示的长度。最后,我们返回一个包含着音符和长度的table。

第一个helper函数note()只是做简单的加法和乘法运算。把下面的代码写到notation.lua文件的最上端:

lua/day3/b/notation.lua
local function note(letter, octave)
  local notes = {
      C = 0, Cs = 1, D = 2, Ds = 3, E = 4,
      F = 5, Fs = 6, G = 7, Gs = 8, A = 9,
      As = 10, B = 11
  }

  local notes_per_octave = 12

  return (octave + 1) * notes_per_octave + notes[letter]
end

要想翻译用秒表示的音长(比如把q翻译为四分之一音节),我们需要知道歌曲的节拍。我们默认选用每分钟一百拍的节奏,如果需要的话,歌曲可以选择覆盖这个值。把以下的代码写到note()函数的后面:

lua/day3/b/notation.lua
local tempo = 100

local function duration(value)
  local quarter = 60 / tempo
 local durations = {
    h = 2.0,
    q = 1.0,
    ed = 0.75,
    e = 0.5,
    s = 0.25,
  }

 return durations[value] * quarter
end

代码中的table中有一个名为ed的元素,这是附点八分音符,它是一般八分音符的1.5倍长。我们稍后会在另一首歌里用到它。

要遍历table并播放那些音符很简单。回到good_morning_to_all.lua,把下面的函数写入其中:

 lua/day3/b/good_morning_to_all.lua
 scheduler = require 'scheduler'
 notation = require 'notation'
 function play_song()
   for i = 1, #notes do
     local symbol = notation.parse_note(notes[i])
  ➤   notation.play(symbol.note, symbol.duration)
    end
  end

现在我们需要同时用到音符编号和长度,那就得修改play()函数了。我们需要发送Note On消息,等待一段时间,然后发送Note Off消息。

我们怎么才能在等待的时候不阻塞程序的运行呢?等一下,我们在第二天不是遇到过类似的问题吗?去把我们写的调度器的代码复制到当前项目里。然后,把下面的代码写入notation.lua:

lua/day3/b/notation.lua
local scheduler = require 'scheduler'

local NOTE_DOWN = 0x90
local NOTE_UP = 0x80
local VELOCITY = 0x7f

local function play(note, duration)
  midi_send(NOTE_DOWN, note, VELOCITY)
  scheduler.wait(duration)
  midi_send(NOTE_UP, note, VELOCITY)
end

我们要写的是一个Lua模块,那我们就需要在文件结尾处把公开函数导出:

lua/day3/b/notation.lua
return {
  parse_note = parse_note,
  play = play
}

做个小结,notation.lua现在包含以下函数。

1.私有的helper函数,note()和duration()。

2.公开的parse_note()函数。

3.公开的play()函数,以及几个局部变量。

4.描述该Lua模块的返回语句。

要使用调度器的话,得给歌曲的代码加一个步骤。我们必须要在goodmorning to_all.lua的结尾处启动事件循环:

lua/day3/b/good_morning_to_all.lua
scheduler.schedule(0.0, coroutine.create(play_song))
scheduler.run()

准备好听听你的音乐了吗?

./play good_morning_to_all.lua

现在我们已经能够编写简单的歌曲了,下面我们来做些难度稍高的吧。

我们自制的Lua音乐程序看起来还挺不错的。不过有几件事在接下来给更长的歌曲编码的时候会变得很麻烦:

我们真正想要做到的是能像下面这样写歌:

song.part{
  D3q, A2q, B2q, Fs2q
}

song.part{
  D5q, Cs5q, B4q, A4q
}

song.go()

我们希望以上的歌曲定义在播放时可以让两个part同时发声。多亏了我们的调度器,我们可以处理同时播放的问题。把下面的代码写到notation.lua里,就放在最后的返回语句之前:

lua/day3/b/notation.lua
local function part(t)
 local function play_part()
    for i = 1, #t do
      play(t[i].note, t[i].duration)
 end
 end

  scheduler.schedule(0.0, coroutine.create(play_part))
end

这个函数接受一个音符数组,叫作t,该函数内定义一个叫作play_part()的函数,它可以依序播放参数数组内的音符,最后我们把它安排进调度器里,只要顶层歌曲调用run()函数就可以播放了。

那接下来就只剩下如何解决音符必须要放到括号里的问题了。如果想不写括号的话,音符就必须得是全局变量。Lua把全局变量放在一个叫作_G的table里。我们需要做的就是运用第2天学到的metatable技巧来修改table查找的方式:

 lua/day3/b/notation.lua
 local mt = {
   __index = function(t, s)
     local result = parse_note(s)
  ➤   return result or  rawget(t, s)
  end
  }

  setmetatable(_G, mt)

上面定义的函数在每次全局变量查找时都会被调用,而不仅是查找音符变量时才用到。如果我们在代码里写错变量的名字,那也会导致这个函数被调用。在查找不到音符的时候就去查找_G中的值。我们对rawget()函数的调用绕过了自定义的查找代码,这样就不会因为一个不存在的变量名而导致无限循环了。

现在剩下的就是几个工具函数了。我们需要让音乐家可以设置节奏,还需要给scheduler.run()提供一个包装,这样歌曲就无须显式加载scheduler模块了:

lua/day3/b/notation.lua
local function set_tempo(bpm)
  tempo = bpm
end

local function go()
  scheduler.run()
end

不要忘记修改模块的返回语句来让其中包含新写的公开函数:

lua/day3/b/notation.lua
return {
  parse_note = parse_note,
  play = play,
  part = part,
  set_tempo = set_tempo,
  go = go
}

现在我们就做好了写更复杂的歌曲的准备了。

你可以在Petrucci项目[35]中找到很多公开的乐谱。我选用了Pachelbel的Canon in D[36]

这是其中的一小部分:

lua/day3/b/canon.lua
song = require 'notation'

song.set_tempo(50)

song.part{
  D3s,          Fs3s,       A3s,        D4s,
  A2s,          Cs3s,       E3s,        A3s,
  B2s,          D3s,        Fs3s,       B3s,
  Fs2s,         A2s,        Cs3s,       Fs3s,
  G2s,          B2s,        D3s,        G3s,
  D2s,          Fs2s,       A2s,        D3s,
  G2s,          B2s,        D3s,        G3s,
  A2s,          Cs3s,       E3s,        A3s,
}

song.part{
  Fs4ed,             Fs5s,
  Fs5s, G5s, Fs5s, E5s,
  D5ed,              D5s,
  D5s, E5s, D5s,   Cs5s,
  B4q,
  D5q,
  D5s, C5s, B4s,   C5s,
  A4q
}

song.go()

如果你把以上代码写入canon.lua并运行./play canon.lua,你就会听到我最爱的乐曲之一,而且还是多个部分同时播放呢!

在探险Lua征途的最后一步中,我们学习了Lua干净的C API。通过把参数与返回值入栈和出栈,我们可以在Lua和C这两个世界之间交换数据。

我们把第三天的新知识和第二天学到的metatable、协程技巧结合起来,用C++和Lua写了一个简单的MIDI播放器。这正是Lua的用途:包装底层库、提供易用的接口。而这也正是我解决第一天提到的难题的方式。

找到

练习(简单)

练习(中等)

练习(困难)

很多程序员只看到Lua简洁语法的表面现象就会猜测它仅仅是另一门普通的脚本语言。我开始也是这么想的。不过我希望现在你在更深入地了解了Lua的table和协程之后,能够欣赏它的简洁之美。

我们来回忆一下Lua的目标:成为一个易用的、易于移植的、有能力把多种软件组件编织在一起的语言。这些都是一门好的配置语言该具有的特性,这就是Lua的闪光点。

Lua的源码易读,运行快速,且可以跨多种平台。Lua的一个新实现——LuaJIT,更进一步地发展了这些优势,其性能更高并且有更友好的C接口[37]

最后,把Lua引入项目很简单。只需要几个头文件和库,你就可以启动一个Lua解释器,你的程序从此就脚本化了。你甚至可以把内嵌的Lua解释器置入沙箱,比如你可以限值它访问网络和文件系统的能力。如果你要运行第三方编写的脚本,这一点极其重要[38]

Lua的好处是你可以自己构建所有东西。但是对应的坏处就是很多时候你不得不自己构建很多东西。Lua对于对象框架、控制流程的态度很是暧昧,这就意味着这些东西没有一个官方认可的、开箱即用的默认实现。

Lua比它的某些竞争对手更快,但是它在某些方面也会陷入性能问题。要把字符串处理做得高效很需要费些脑筋[39]。要想利用多核系统的性能,程序员也要颇费思量。

最后,Lua有几个和Pascal很像的奇奇怪怪的特性,会惹得有些人不喜欢。比如从1开始的数组和do/end为标识的代码块会让不熟悉的人感到吃惊。

Lua的基于原型的对象系统证明了并不是一定要有类才能构建好的对象系统。如果你对其他的基于原型的对象系统感兴趣的话,可以去了解一下Self、Io,当然还有JavaScript[40]

我永远都忘不了Lua让我学到的关键一课:代码就是数据。函数和其他的数据一样,你可以随时创建一个新的函数,可以存储函数,还可以把函数当成参数传递。这一课让我成为了一个更好的程序员,无论我使用的是哪种语言。

恭喜你完成了探索Lua的历程。我希望你这一路走得愉快,祝你在下一章学习Factor时也有好运。

[1] http://en.wikipedia.org/wiki/Rexx

[2] http://lua-users.org/wiki/BuildingLua

[3] http://lua-users.org/wiki/LuaBinaries

[4] https://docs.python.org/release/1.5.1p1/tut/keywordArgs.html

[5] http://www.lua.org/pil/7.1.html

[6] http://www.lua.org/pil/27.1.html

[7] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Details_of_the_Object_Model; https://docs.python.org/2.5/ref/ specialnames.html

[8] http://www.inf.puc-rio.br/~roberto/docs/ry08-05.pdf

[9] http://www.lua.org/pil/9.4.html

[10] http://code.openhub.net/project?pid=bQ7OKaOjyIw&prevcid=1&did=kernel%2Fsched

[11] http://www.adobe.com/devnet/photoshoplightroom.html

[12] http://www.wowwiki.com/Lua

[13] http://stackoverflow.com/a/4430719

[14] http://www.midi.org

[15] http://singrobots.com

[16] http://www.cmake.org

[17] http://www.music.mcgill.ca/~gary/rtmidi

[18] http://forums.pragprog.com

[19] http://www.visualstudio.com/en-us/products/visual-studio-express-vs.aspx

[20] http://coolsoft.altervista.org/virtualmidisynth

[21] https://developer.apple.com/downloads/index.action

[22] http://brew.sh

[23] http://notahat.com/simplesynth

[24] https://help.ubuntu.com/community/Repositories/Ubuntu

[25] http://tedfelix.com/linux/linux-midi.html

[26] http://zynaddsubfx.sourceforge.net

[27] https://wiki.ubuntu.com/PulseAudio

[28] http://www.lua.org/pil/27.3.1.html

[29] http://www.midi.org/techspecs/midimessages.php

[30] http://en.wikipedia.org/wiki/Tenacious_D

[31] http://imslp.org/wiki/File:PMLP98386-Hill-GoodMorningtoAll1893.pdf

[32] http://readsheetmusic.info/readingmusic.shtml

[33] http://en.wikipedia.org/wiki/Scientific_pitch_notation

[34]译者注:以上代码中的q代表四分之一音节,即quarter note。

[35] http://imslp.org/wiki/Main_Page

[36] http://imslp.org/wiki/File:WIMA.7c2a-PachelbelCanon.pdf

[37] http://luajit.org/luajit.html

[38] http://www.luafaq.org/#T1.32

[39] http://lua-users.org/lists/lua-l/2005-10/msg00137.html

[40]http://selflanguage.org;http://iolanguage.org;https://developer.mozilla.org/en-US/docs/Web/JavaScript


相关图书

Rust游戏开发实战
Rust游戏开发实战
JUnit实战(第3版)
JUnit实战(第3版)
Kafka实战
Kafka实战
Rust实战
Rust实战
PyQt编程快速上手
PyQt编程快速上手
Elasticsearch数据搜索与分析实战
Elasticsearch数据搜索与分析实战

相关文章

相关课程