编译器和解释器

文摘   2024-09-30 09:41   上海  

本篇内容是根据2019年10月份Compilers and interpreters[1]音频录制内容的整理与翻译,

Thorsten Ball[2]Tim Raymond[3]Mat Ryer[4]Mark Bates[5] 一起讨论编译器和解释器。

编译器和解释器的作用是什么?他们做什么?使用 Go 编写编译器的方式和原因。

还讨论了 Thorsten 的书“Writing an Interpreter in Go”[6]和“Writing a Compiler in Go”。(译者注: 中文译作为用Go语言自制编译器[7], 用Go语言自制解释器[8])


过程中为符合中文惯用表达有适当删改, 版权归原作者所有.



Mat Ryer:大家好,欢迎收听 Go Time。今天我们将讨论 Go 语言中的编译器和解释器,非常有趣。和我一起主持节目的是我的老朋友 Mark Bates。你好,Mark。

Mark Bates:嗨,Matthew。你今天怎么样?

Mat Ryer:我很好。你呢?

Mark Bates:想到我们可能有某种亲戚关系,我有点儿慌。

Mat Ryer:好吧,我说的是“另一个母亲”,所以你根本没听清楚,这开头就不太好了。

Mark Bates:嗯,言语中还是暗示了某种关系,我不太确定对此感到舒适。

Mat Ryer:好吧,谢谢,你真好。

Mark Bates:[笑]

Mat Ryer:我们还邀请了 Tim Raymond。你好,Tim。

Tim Raymond:你好,Mat。你好吗?

Mat Ryer:很好,你呢?

Tim Raymond:我也不错,谢谢。

Mark Bates:顺便提一句,今天是 Tim 的生日,大家... [笑声]

Tim Raymond:每次我做点什么的时候,都是我的生日...

Mark Bates:所以如果你在听,请给 Tim 送上生日祝福。

Mat Ryer:顺便说一下,这是 Mark Bates 经常对 Tim Raymond 开的玩笑...

Mark Bates:我不知道你在说什么。Tim 今天 21 岁了。

Tim Raymond:每次我们去餐馆的时候... [笑声]

Mat Ryer:我们还邀请了 - 你不会相信的 - Thorsten Ball,他是《用 Go 写编译器》和《用 Go 写解释器》两本书的作者。你好,Thorsten!

Thorsten Ball:你好,你好吗?

Mat Ryer:很好,谢谢。欢迎来到节目!

Thorsten Ball:很高兴来到这里。

Mat Ryer:很好,不过我们暂时保留判断,你可以在节目结束时告诉我们感受如何。 [笑声] 那我们开始吧,今天要讨论的是编译器和解释器。首先,对于可能不知道这些是什么的人,我们可以从“什么是编译器”开始解释吗?它到底做什么,为什么它有用?有人想试着解释一下吗?

Mark Bates:我认为 Thorsten 应该来解释,因为他的书对这些理论的解释非常出色。

Thorsten Ball:谢谢。

Mark Bates:你喜欢我把你推到台前的方式吗?

Thorsten Ball:[笑] 是啊。我本来要说答案其实没那么简单,因为这要看情况;定义是模糊的。但我会说,最简单的解释是,解释器和编译器是实现编程语言的方式,是让它存在的手段。你可以想象一种编程语言,可以定义到非常详细的程度,但它仍然不存在。它可能只存在于纸上或理论中,而你需要通过实现它来让它存在。你可以通过构建解释器或编译器来实现它。这是我简短的解释。

Mat Ryer:这很棒。那么就 Go 来说,Go 的编译器当然会处理 Go 代码,进行所有的“魔法”操作,然后将其转换为二进制文件...

Thorsten Ball:对,没错。

Mat Ryer:那么二进制文件里到底是什么?二进制文件实际上是什么?

Thorsten Ball:二进制文件里是另一种编程语言,由计算机、由 CPU 解释。所以,正如人们所说,机器语言是最终的底层语言,我想...

Mat Ryer:对。

Thorsten Ball:但最终的目标是你想和计算机对话,并且希望计算机理解你。当你从工厂买到一台电脑时,它只会说一种语言,那就是机器语言,而且每台电脑的机器语言都不一样,CPU 也不一样。比如,AMD 的机器语言和 ARM CPU 的就不同。为了避免每次都直接用机器语言和计算机交流---这非常复杂且低级---你需要在机器语言之上实现另一种语言。

你可以说:“如果我给你这五个机器语言的字词,你就做这个操作。”然后你逐步构建,在机器语言之上再创造一种语言。你可以在机器语言中说:“我从磁盘上读取另一种语言,并将其转换为机器语言中的另一个版本”,这样计算机就能理解。然后你逐步构建,最终你可能会使用 Go 语言,甚至更高级的语言,比如浏览器中的 JavaScript。

Mat Ryer:你先写的书是解释器那本,对吧?

Thorsten Ball:对,没错。

Mat Ryer:在那本书里,你实现了 Monkey 语言。

Thorsten Ball:对。

Mat Ryer:能跟我们讲讲 Monkey 语言吗?

Thorsten Ball:Monkey 语言叫这个名字是因为已经有了 Tiger 语言……我需要给编程语言起个名字,我的目标是构建一种比大多数教程、博文中使用的示例语言更复杂一些的语言。大多数时候,这些示例语言会有 Lisp 的括号结构,而这就会失去 60% 的读者……接着,当他们说“我们跳过解析部分,因为我们有 Lisp,只需按括号分割字符串就行了”时,又会失去 20% 的读者。这种简化方式忽略了许多我自己想要了解的东西,比如解析器及其工作原理。

而我想在书中构建并剖析的语言应该包含更多细节,比如大括号……那时我觉得“有大括号?这才是真正的语言……不是玩具语言……” [笑声] 现在我有点改观了。但是它应该看起来像是一种真正的语言---有合适的缩进,关键字、括号、圆括号等。我把这些拼凑在一起,然后给它取名为 Monkey。

如果要描述它,它看起来有点像 JavaScript;在底层,它的运行机制也有点像 JavaScript,因为它实际上更像是 Scheme 或者 Lisp 的东西。有趣的是,JavaScript 本来也是基于这些语言的。它是一种非常小的语言,包含基本数据类型,比如整数、数组、哈希、字符串、函数、一等函数、高阶函数……我一时想不出还有什么。它在附加章节中还有一个宏系统……嗯,我给它取名为 Monkey,因为我确实喜欢猴子。

Mat Ryer:所以这不仅仅是学习解释器的好方法,而且你实际上实现了一个真正的东西,我认为这是学习的好途径。当然,这些技能是可迁移的,对吧?这才是重点。一旦你学会了如何做这件事,你就可以用这些技能去解决其他问题。

Mark,你实际上从 Thorsten 的书中学到了这些,并运用了这些技能,对吧?

Mark Bates:对。我想书的网站上甚至有我的一句推荐语……

Thorsten Ball:对。

Mark Bates:我觉得这本书很棒。我常说自己没有计算机科学学位。我不是计算机科学家,我的学位是音乐,所以解析器、词法分析器、标记、解释器这些东西对我来说完全是超纲的知识,根本无法理解。

所以当我看到这本书时,我想:“好吧,让我试试看。”当时我们在 Buffalo 项目[9]中遇到了模板的问题,我对其他一些现有的解决方案感到有些无奈……所以我希望至少能理解它们的工作原理,这样我可以为这些项目做出贡献,比如 xx项目,这是一种奇怪的 Mustache 和 Handlebars 的实现,对吧? (译者注: 都是JS模板引擎)

Thorsten Ball:对,和我没关系。

Mark Bates:对。他们喜欢用 panic 而不是 error。

Mat Ryer:不错!

Mark Bates:太棒了。所以我读了这本书,我想“我肯定理解不了,但试试看……”我喜欢这本书的一点是,它从头到尾都是小步骤,易于测试。每一步都采用 TDD 方式,贯穿整本书……到最后,我已经自信过头了,居然去构建了 Plush[10]…… [笑声] 但我从完全不了解这些概念,到至少对其理论有了基本的理解。当我们稍后谈到 PEG(解析表达式文法)时,有了 Thorsten 书中的这些知识,再去理解 PEG---这是一种对解析器进行高层抽象的工具---我至少能理解它们的原理。

所以,理解这些理论,我认为是非常重要的,即使你不打算使用这些工具。并不是所有人都需要或能够构建编程语言。就像路由器一样,我们不需要更多的路由器。但我们稍后还会聊到我正在写的一个…… [笑声]

Mat Ryer:对,我想我们会聊到……当我第一次看到这本书时,我还以为这是最小众的书了,我想“这个题材会不会太小众了”,但实际上,模板就是一个很好的例子。有几次我遇到问题时,我很想能够自己解决---但最终我只是勉强凑合着解决了问题,并没有真正深入理解。

Mark Bates:正则表达式之类的东西……

Mat Ryer:对,类似的……

Tim Raymond:配置文件也常见到,很多人自己发明的配置文件格式。

Mark Bates:对。

Mat Ryer:对,没错。Matt Layer 在 Slack 上 - 顺便说一句,如果你正在收听直播,可以加入我们的 Slack 讨论,GopherSlack 的 #gotimeFM 频道。Matt Layer 推荐了这个,他说在他读完书之后,他能够添加新功能,进行一些尝试、修改和破坏之类的事情。我认为,对于学习的精神来说,这是一个非常吸引人的做法。所以我很喜欢你实际上拥有一个属于自己的实现,然后你可以随意去玩弄它。

Mark Bates:这本书也确实把你带到了一个很好的位置。它给了你所需的知识,可以像 Matt 那样继续去探索,或者像我一样。我觉得书里可能没有涉及到的部分,或者可能需要另一本书来讨论的部分是:你写了一个解析器,但不要让它成为你实际执行代码的地方,类似这样的事情……这也是 Plush 发生的情况。它变成了一个集解析、执行、模板系统于一体的巨大系统,这给我们带来了很多问题。我们稍后会谈到这些问题,但……

Mat Ryer:嗯,很有意思。

Thorsten Ball:我可以一直听你们说读这本书有多愉快…… [大笑]

Mat Ryer:别担心,我也收集了不少批评意见。

Mark Bates:我刚刚告诉你它后面有多糟糕……

Thorsten Ball:不不不,你说的是你自己做的有多糟…… [大笑] 我没把这写进书里……

Mark Bates:是的,正是如此。

Thorsten Ball:第二本书甚至介绍了字节码,所以我猜那里有一个很好的分离点……

Mark Bates:任何认识我的人,Thorsten,都会告诉你,你必须明确跟我说“Mark,不要滥用这个技术”,因为我一定会滥用。

Thorsten Ball:哈哈,没错。

Mat Ryer:"尽量不要把这个作为一个包发布,最后成为很多 Mat Ryer 项目的依赖"…… [大笑]

Thorsten Ball:我想回到我们刚开始讨论的出发点,那就是你一开始可能会想“这真的很特定化,我怎么可能重用这些知识呢?”而对我来说 - 听起来你们也会同意 - 当你理解了解析的工作原理,比如解析器是如何工作的,突然之间你会在各处看到它;然后你会想,“哦,现在我也知道这是如何实现的。我可以看出这个是怎么工作的。”

Mark Bates:它确实帮助我更好地理解 Go 的 AST(抽象语法树),并且如何与之交互。

Thorsten Ball:是的,我的意思是,那就是实现;你可以窥视一种语言的底层结构。但例如配置语言 - 我觉得在你读完这本书后,你可以写任何解析器,比如 TOML 解析器,或者其他的。也许 YAML 除外,因为那似乎是人们想象力的深渊。

Mark Bates:它就像是配置文件中的 JavaScript。

Thorsten Ball:所以你突然间将所有这些不同的问题视为现在可以解决的问题。你的能力范围变得更广了,你可以突然实现一些你之前甚至不敢想象的东西。

再举一个例子,配置文件就是一个例子。模板语言,查询语言。如果你在开发一个数据库 - 并不是每个人都做这个……但数据库需要解析查询,将它们放入某种结构化的形式中进行解释、编译或其他处理。

比如说我在 Sourcegraph 工作,我们构建了一个代码搜索引擎,你可以搜索代码并输入查询。这些查询也需要被解析。然后你将它们发送到数据库,数据库也会解析这些查询。我查看了这个数据库是如何处理的,令人惊讶的是,它们的工作方式非常相似。最终,它就是一堆函数构建了一棵树,然后再进行解释。

在我深入研究解析和编程语言解析之前,我甚至不知道从哪里开始。这就像是你工具箱里的另一个工具,希望你在需要的时候能用上。所以这是我想给出的第一个答案。

第二个是你说的,Mat,这对学习有帮助;当你实现了一个编程语言之后,你可以添加更多的东西,就像 Matt 在 Slack 里说的。我想补充的一点是,一旦你拥有了一个可以运行的语言或解释器,添加一些小功能是一件非常有趣的事情。

听起来我在夸张,但这几乎是无尽的创造力。你可以给你的语言添加许多不同的东西。开发语言的有趣之处在于,与 Web 应用相比(这是我的背景),你不需要数据库,不需要互联网连接,不需要第二个数据库,也不需要庞大的构建流程。这真的就像凭空创造。你有一个文本文件,你在文本文件里写东西,然后在另一端你希望得到输出,或者其他结果。这是一件非常有趣的工作。

Tim Raymond:当我写我的第一个解释器时,我也有同样的感觉,因为这是一个非常好的方式来锻炼你的测试驱动开发(TDD)能力…… 它们没有依赖;你只是写出一串东西,然后可以很容易地尝试新的用例……所以你不仅可以学到如何编写解释器、编译器,还可以学到如何编写真正优秀的测试,这些测试不依赖其他东西。

Mat Ryer:是啊,这也是一个展示模糊测试(fuzzing)的好时机,对吧?因为这是字符串输入,你不希望它崩溃。你至少希望它能够报告错误,如果有问题的话,最好还能报告出错的地方。

Mark Bates:等一下,我在记笔记…… [大笑] 错误处理, 测试……

Thorsten Ball:我正要说,请不要对我们在书中写的代码运行模糊测试。那不会有好结果……我敢肯定它会在第四次尝试时崩溃。

Mark Bates:我从这本书中收获最大的,或者说我最喜欢的部分 - 也是我们一定想听 Tim 说的 - 就是关于解析器本身。无论你最终是写了一个解释器,还是编译器,或者其他什么,写解析器本身就可以在很多场景中非常有用。

例如,在 Gopher Guides[11],我们有一个我们使用的 Markdown 解析器,它可以拆分我们的 Markdown,然后我们可以对它做各种事情。我们可以重新排列它,使它更适合用来制作幻灯片,或者以不同的方式格式化它。这不是一种语言,它甚至不是一个很大的东西;它不是解释器,也不是编译器,但我们写了一个解析器,因为我们需要拆解这个文件格式。所以,拥有这种能力和理解如何工作以便写出这样简单的东西是非常有用的。

因此,在这种情况下,你不需要写一个模板包,不需要写一个编程语言;你仍然可以利用解析器来做大量的事情。

Mat Ryer:是啊,我想很多人可能都写过某种版本或类型的解析器。我自己也写过。而发生的事情是,它在我需要的特定情况下工作得非常好,然后后来我想“哦,加上这个会很棒。”如果你没有正确地结构化它---书中遵循的是经过充分测试的模式。而我做的那些并没有。我只是自己摸索着做。结果变得非常令人沮丧……我基本上想把我的笔记本电脑像飞盘一样扔进大海,情况就是这么糟糕。

所以,拥有实际的结构,正确地解析……简单的事情,比如知道你什么时候在字符串里。因为当你在字符串里时,事情会有所不同。像这样的小细节 - 如果你只是简单地拆分字符串并进行一些基本的解析,手动编写代码,这些就是变得特别棘手的地方。所以,我非常喜欢这些是经过验证的技术,现在通过 Thorsten 的书,大家都可以获得这些技巧…… [大笑]

Mark Bates:售价 19.95 美元。

Mat Ryer:我得停止推广了。能不能请 Slack 上的某个人 - 如果有人不喜欢他的书,请联系我好吗?因为这有点……我们想确保节目是平衡的。

Mark Bates:嗯,如果你想批评我也可以……我们是在尝试批评 Thorsten 吗?因为,嗯,我们可以这样做…… [大笑]

Mat Ryer:不,我们不必真的那么做。

Mark Bates:哦,那真可惜。我们可以谈谈 PEGs 吗?

Mat Ryer:什么是 PEGs?

Mark Bates: 我觉得我们应该谈谈 PEGs。我们已经谈了解析器,我觉得这是一个很好的入门话题,我也很想听听 Thorsten 对它的看法。Tim Raymond 也在这里……Tim 下周会在 Gopherpalooza 上演讲。顺便说一下,我在用 Mat 的 NPR 腔调……试图和他配合得更好。下周 Tim 会在 Gopherpalooza 上演讲,届时也是他的生日,他将谈论 PEGs,也就是解析表达式文法,对吧,Tim? (译者注: ["Parsing Expression Grammars (PEG)" by Tim Raymond – Gopherpalooza 2019](https://www.youtube.com/watch?v=a37rQdV7LE4 ""Parsing Expression Grammars (PEG)" by Tim Raymond – Gopherpalooza 2019"))

Tim Raymond:是的。

Mark Bates:为什么你不告诉我们这个节日大餐的内容呢?

Tim Raymond:PEGs,正如 Mark 所说,是解析表达式文法……它是一种自动构建解析器的语言。这是一种代码生成的方式。如果你读过其他解析相关的东西,你可能听说过一些工具,如 Bison[12] 和其他类似的解析器生成器……

我觉得 PEGs 很好的一点是,它们与手工编写的解析器非常接近。但我认为,一旦你手工写过第一个解析器---这是我推荐每个人都要做的……

Mark Bates:哦,绝对的……

Tim Raymond:在尝试 PEGs 之前,先手工写一个解析器,这样你就可以学到其中没有任何魔法……但 PEG 可以帮助你快速推进,当你尝试构建一个新语言,或者尝试解释一个语言,或者只是尝试一些新想法时,它能帮你省下很多时间。

Mat Ryer:那它是一种定义语言吗?还是一种配置语言?在实践中它是如何工作的?因为你大概需要提到“这些是关键字,我需要整数……”它看起来是什么样子的?

Tim Raymond:当你手工编写解析器时,你通常会写下语法,比如“我们有一个文档,它由多个不同的语句组成”之类的。PEGs 让你可以直接把这些字符串写出来。文档生成多个语句。当那个规则真正匹配时,你可以为那个部分运行一些自定义的 Go 代码。所以它允许你在语法匹配文本中的不同部分时插入一些小钩子。

Mark Bates:对,或许可以提供一个视觉上的描述。

Mat Ryer:Mark,这是一个播客。请记住这一点。

Mark Bates:我知道,对不起。 [大笑] 好吧,我的涂鸦显然对听众没有帮助…… [大笑] 让我们试着用心智图景来解释一下---

Mat Ryer:还有,请把衣服穿上。

Mark Bates:我们要在一个安全的环境中学习,Mat。安全的环境中学习。

Mat Ryer:好吧,那我就安静了。

Mark Bates:闭上眼睛,点上一支蜡烛,想象一下,在你的脑海中,一个空白的 .peg 文件……不,实际上你需要做的是---假设你要声明一个变量 "var a int"。我们都熟悉的 "var a int"。在那个 PEG 文件中,你会说,“好吧,我要定义一个名为 var 的关键字,并且如果它后面跟着一组字母数字字符”,比如 a 到 z,0 到 9,那么我们可以将其解释为一个标识符;然后你有一个 int 类型。所以你会写一个规则,叫做 var 规则,并且它看起来像这样。你使用 var 这个词,然后跟着我定义的标识符匹配,再跟着类型匹配,最后是一个换行符。这个规则会被匹配,然后在那个规则里你在 Go 里做一些处理。你可能会返回一些与变量声明相关的东西。

Tim Raymond:是的。你可以返回一个结构体,或者打印一些日志信息……所以如果你只是构建一个工具来高亮显示某些东西,你可以打印出匹配到的内容并加以高亮……你可以访问到 PEG 实际匹配的内容,因此在那个时候你可以随心所欲地做任何事情。

Mat Ryer:但是它们会不会变得难以维护?它们会不会很快就变得特别庞大?

Tim Raymond:确实如此,我们其实一直在为 Plush 开发一个新版……

Mark Bates:哦,我们有在做吗?我们在做吗,Tim?你真是抢了我的风头。

Tim Raymond:是的,是的,我确实抢了……我很抱歉。 [笑声] 在这个过程中,Mark 发现,尽快从 PEG(解析表达式文法)切换到实际的 Go 代码可能更好……因为生成的代码有一些问题,比如 Go 的导入语句根本不能正常工作。所以,为了使用我们习惯的 Go 代码编写方式,我们发现最好在这些代码旁边加入一些支持性的 Go 文件。

Mark Bates:不幸的是,当你编写这些 PEG 文件时,所有你得到的都是空接口。所以规则匹配后,你得到的只是一些代表匹配结果的空接口。因为你是在 .peg 文件中编写这些东西,而不是在 Go 文件中,所以你得立即处理这些空接口,这是我发现的最大问题之一。这些代码有时候会变得难以跟随……你会在某些细节中迷失。

但总体来说,每个规则本身,如果你分解开来看,都非常简单并且定义明确。例如,“如果你看到 if 后面跟着括号,再跟着括号中的一些内容,那么它是这些东西,然后你用这种方式处理它。” 正如 Tim 所说,你会惊讶于你能多快地取得进展。

Mat Ryer:所以 PEG 只输出文本,它并不知道生成的是 Go 代码。

Mark Bates:是的,PEG 本质上是文本。然后你用像 Pigeon[13] 这样的工具运行它,Pigeon(讽刺的是)解析 PEG,然后生成一个 .go 文件作为解析器。

Mat Ryer:哦……

Mark Bates:生成的代码非常庞大……

Tim Raymond:它是自动生成的代码。

Mark Bates:是的,是自动生成的代码。我听说它没有手写的解析器好,但我知道它比我自己手写的要好。 [笑声]

Mat Ryer:这得看是谁手写的。

Mark Bates:你说得对。我想 Thorsten 会同意---理解这些东西很重要,知道它们的用处也很有帮助,但我们并不是所有人都是语言设计师,也不是所有人都是解析器专家。而除非你真的想成为专家,否则你不会成为。

Thorsten Ball:是的。Tim 也说过,他建议你的第一个解析器应该自己写,我 100% 同意……当你在谷歌搜索“如何为编程语言编写解析器”时,你会发现有些人会说“别担心,用解析器生成器就行,比如 Yacc、Bison、Antlr……”

Mark Bates:这些东西我完全看不懂。

Thorsten Ball:没错,我正要说……

Mark Bates:我理解了你的书,但不理解那些东西。 [笑声]

Thorsten Ball:这些工具的输入是语法,比如 BNF(巴科斯范式),或者 EBNF(扩展的巴科斯范式)。我发现,要直接编写这样的语法,或者理解它做什么、应该做什么,是非常困难的。如果你没有自己写过解析器,你会很难理解。但一旦你写了一个递归下降解析器,你再看这些语法时,你就会明白如何从中创建解析器,然后你会珍惜这些工具所抽象出来的东西。解析理论本身是一个非常庞大的领域,我不敢深入探讨或发表意见…… [笑声]

Mark Bates:是的,但你在书中用非常通俗的语言解释得很好……

Thorsten Ball:[笑声]

Mark Bates:正如我刚才所说,除非你想成为专家,否则你不会成为专家。

Mat Ryer:不过这对狗狗来说可能不太合适。如果有狗想要学习的话……

Mark Bates:我的狗很喜欢这本书。

Thorsten Ball:我收到过几封狗写的邮件,说……

Mark Bates: Ringo 已经写了四种语言了。那只狗停不下来。他对这本书着迷不已。每晚他都会把编译器的书带进他的笼子里,然后用小爪子一页页地翻看……

Thorsten Ball:大多数狗更喜欢第二本书……

Mark Bates:是的,我同意。他明显对编译器书籍更感兴趣。我也不知道为什么,也许是因为字节码的原因……

Thorsten Ball:哦,这是个笑话吗……?

Mark Bates:谢谢大家!这是爸爸级笑话 101……

Tim Raymond:这是个好笑话。太棒了。

Mark Bates:谢谢。

Mat Ryer:那么编译器的书是解释器书的延续吗?

Thorsten Ball:是的,是续集……这对技术类读者来说其实很难解释。你读完第一本书的最后一页,然后打开第二本书,它接着讲述。

Mark Bates:说实话,第一本书的结尾猴子语言被劫持了,所以我能理解为什么大家想要续集。

Thorsten Ball:是的,是的。

Mark Bates:我也想知道猴子后来怎么样了。

Mat Ryer:Thorsten,为什么大家理解不了这一点呢?这明明是个非常简单的概念啊。 [笑声] 这是续集嘛。

Thorsten Ball:我也不知道。人们会问我“我需要先读第一本书再读第二本吗?”这……如果你看一下书名,可能不会直接告诉你它们是相互关联的……但它们确实是。它们使用相同的代码库。

你可以直接读第二本书,但这样你就得把我们在第一本书中构建的东西当作黑盒来看待……这有点违背了这些书的初衷。你可以选择忽略第一本书的解析器、AST 包、对象模型等内容,只专注于字节码和虚拟机。但我并不是这样写的,所以可能会有一些东西缺失……因此,我并不推荐你这么做。

Mark Bates:那么前传什么时候出呢?

Thorsten Ball:前传?比如“猴宝宝”? [笑声] 我不知道,我不知道……

Mark Bates:我很期待。

Thorsten Ball:我想,确实有很多东西可以加到这两本书里。正如我们之前讨论的,你可以在任何维度上扩展一门编程语言。你可以增加更多语法,添加更多功能,改进实现,让它更快、更高效,等等。但问题在于“如何在书中解释这些内容?”当我开始写第一本书时,我想,“好吧,我要在一本书里讲解解释器和虚拟机字节码。” 然后我开始写作,意识到“我根本写不完这些……”

Mark Bates:你是技术作者中的 Stephen King 吗? [笑声] 写个几千页的大作? (译者注: 美国著名小说家,以其恐怖、超自然和悬疑类作品而闻名创,作了众多畅销书,包括《闪灵》《肖申克的救赎》)

Thorsten Ball:哈哈,是的,第一部分。

Mark Bates:最后结局竟然是个巨大的蜘蛛……真是让人失望。 [笑声]

Mat Ryer:那么编译器书籍呢?编译器的过程是将文本语言(输入是文本)理解为可执行的东西……这个过程是怎样的呢?

Thorsten Ball:让我们回到我之前说的“计算机只理解一种语言”。我们的目标是让计算机理解你输入的内容。就像人与人之间的交流---我知道这个比喻有点牵强---有两种方式可以让一个人理解另一个人说的语言。你可以听对方说话,然后将其写下来,翻译成你的朋友能理解的语言,再交给他阅读……或者你可以实时翻译,对另一方的每一句话进行解释,然后立即向你的朋友复述。

如果我们将这个比喻应用到计算机上,解释就是指输入计算机不理解的另一种语言,并根据所说的内容立即执行操作,或者马上运行这个语言要求你做的事情,以计算机能理解的语言来执行。我不知道这样解释是否清楚,但这确实是正在发生的事情。

而编译则是另一回事。你实际上是在翻译。你将输入翻译成另一种语言,然后将其交给计算机。如果你把 Go 代码编译成一个二进制文件,这就是正在发生的事情。你把 Go 代码输入给编译器,它生成计算机和操作系统可以理解的机器代码。市面上有很多 Go REPL(读-求值-打印循环)。(译者注: Go REPL(Read-Eval-Print Loop)是一个交互式编程环境,允许用户在 Go 编程语言中实时输入和执行代码。这种环境非常适合快速测试代码片段、实验和学习 Go 语言的特性)

Mark Bates:确实有很多。

Thorsten Ball:是的。他们做的事情是---其实我不太确定他们是怎么实现的,但他们可能……

Mark Bates:我可以告诉你很多 REPL 是如何工作的,它们会在后端进行编译并运行。它们会接收语句,进行编译,然后执行。

Thorsten Ball:是的,这正是我不想用来解释的方式…… [笑声] 因为 Go 是一种编译型语言,虽然有一些限制,但你完全可以逐行解释它。比如“哦,下一行是 'format print line',那就打印一行,而不是把它翻译成另一条指令告诉计算机打印一行。”我猜你接下来会问我,什么时候该用哪种方式。

Mark Bates:不,我们没有要问……

Thorsten Ball:但我准备了笔记。 [笑声]

Mark Bates:既然你准备了笔记……

Thorsten Ball:好吧,谢谢你的提问。那么,什么时候该选择哪种方式呢……如果你将代码翻译成计算机可以理解的另一种语言,这意味着你可以一次性完成翻译,然后在执行程序时就不需要再进行翻译了。你只需要一次翻译,这意味着你可以将翻译的成本前置到你愿意支付的时间点。

如果你在本地编译 Go 代码,你可以接受等待几秒钟的时间,因为这会使程序在之后的运行(比如在服务器上)更快。如果你不愿意支付这个成本,比如你有一个脚本语言,或者像 Bash 这样的 shell 语言,你希望它能够立即运行,那么你就不会编译它,而是逐行解释并立即执行。当你解释性地运行代码时,你会在运行时支付翻译和优化的成本。

Mark Bates:我们还需要在这里吗?还是我们可以走了……? [笑声]

Thorsten Ball:不,不,不,我都计划好了。你们可以把麦克风静音…… [笑声] 如果你愿意提前支付这个成本,那么你就可以做一些在运行时无法实现的事情,比如优化。当你编译一个庞大的程序时---比如 10 万行代码---你可以做很多优化,比如去除重复代码、内联函数等等。这些优化需要时间和计算资源。如果你在运行时进行翻译并优化代码,则会影响程序的性能。但如果你愿意提前支付这个成本,你就可以进行这些优化。

回到 Go 编译器,我可能会说错,但我听 Go 团队的人说过,他们非常注重编译器的速度,所以他们没有添加太多的优化。这是一种权衡;他们非常清楚这种权衡。是的,我们可以为编译器添加更多的优化……这意味着代码运行得更快,但也意味着编译的过程会变慢,这是我们每个人都要付出的代价,因为我们都非常珍惜 Go 编译器的速度。

Mat Ryer:但是你可以让它成为可配置的,不是吗?你可以有一个快速编译而不进行优化的开发版本,然后当你要投入生产时再用一个慢速编译的版本……

Thorsten Ball:是的,我猜你可以这样做。

Mark Bates:你可以调整你的 JVM 设置…… [笑声]

Mat Ryer:是啊,好吧……这真是个嘲讽。

Thorsten Ball:你可以这样做,对吧?其他编译器有优化级别。我去年才了解到这一点……还有“超级编译器”,这名字就很酷---不管它是什么,光名字就很酷。

Mark Bates:它们披着斗篷,胸口有大大的字母……

Thorsten Ball:完全正确。所以,是的,它们飞得比其他编译器都快……超级编译器做的事情是---再说一遍,我可能会说错---超级编译器会花费大量时间尝试不同的优化。你可以把它当做 CI(持续集成)流程的一部分来运行。每次你推送代码时,它们会运行数小时甚至数天,尝试找到最佳的优化方式,最终会生成一个“金像”版本,并告诉你“这是我们能做到的最快版本。”

游戏公司使用这种编译器---他们称之为“金构建”(Gold Build)。当游戏完成并准备发布时,他们会将二进制文件或汇编语言输入超级编译器,超级编译器会重新排序代码、去除重复代码、尝试不同的组合,持续运行数周,最终生成一个更快的版本。

Mat Ryer:这太棒了。

Thorsten Ball:而且它们还能从眼睛里射出激光。 [笑声]

Mark Bates:我只是把我的代码给 Tim。 [笑声]

Mat Ryer:Tim 就是你的超级编译器。

Mark Bates:是的,他真的是。Tim 是我在计算机科学领域的专家。今年早些时候我们去参加 GothamGo 时,他第一次向我介绍了 PEGs。所以我总是很喜欢有 Tim 在身边,因为他真的是我的超级编译器。 [笑声] 所以能和 Thorsten 一起在这个播客上,我真的很兴奋,因为我是他的超级粉丝,尤其是 Plush 相关的东西……

Mat Ryer:没关系。 [笑声]

Mark Bates:不,不,我本来要说“和 Matthew 一起上这个播客也总是很愉快。”

Mat Ryer:哦,好吧。你这个否定句搞得我有点糊涂。

Mark Bates:顺便说一句,各位,我是唯一可以叫他 Matthew 的人。

Mat Ryer:是的,连你叫都不太合适…… [笑]

Mark Bates:是啊,抱歉,伙计。

Mat Ryer:这是个语法错误。

Mark Bates:现在已经是个事实了,抱歉。

Mat Ryer:我得写个 PEG 来解析我的名字,确保它是合法的。来吧,这是个计算机播客,这样的笑话是可以的。

Mark Bates:真的吗……? [笑声] 真的吗?所以我很高兴能和 Thorsten 和 Tim 一起录制这个播客,因为正如 Tim 刚才提到的,我们正在开发 Plush 的替代品。感谢你,Mat。

Mat Ryer:那它叫什么?

Mark Bates:叫 Lush。

Mat Ryer:聪明。它更小吗?

Tim Raymond:希望如此。

Mark Bates:嗯,算是吧……我们在使用 PEG 来实现它。

Mat Ryer:那它是不是像你走过大街时闻到的那种很浓的味道?这能理解吗?

Mark Bates:这是一个很英式的笑话……

Tim Raymond:我懂了,我懂了。

Mat Ryer:好吧。

Mark Bates:确实是很英式的。

Mat Ryer:有一家叫 LUSH 的肥皂店,闻起来真的很浓。这就是那个笑话。

Thorsten Ball:确实很浓。 [笑声]

Mark Bates:它们都很浓。所以 Lush 是 Monkey 的超集---嗯,确切地说是我们在 Plush 中使用的语言的超集。

Thorsten Ball:酷。

Mark Bates:所以它支持 Plush 的所有功能,还增加了更多。它有点像没有类型的 Go 语言的解释版,对吧?

Tim Raymond:基本上是这样。

Mark Bates:基本上是这样。但有趣的是,我们从两个不同的角度来解决同样的问题,因为 Lush 将成为一个可以嵌入的脚本语言,同时也会编译成 Go……你可以想象,如果你在应用程序中使用它,你会编写 HTML 文件,然后它们会编译成 .go 文件,这非常不错。

Mat Ryer:它是像标准库那样在运行时进行编译,还是你会以某种方式预编译它?

Mark Bates:是的,它是一个预编译步骤,我们会将 Lush 脚本转换成 .go 文件,然后逐步处理它。但正如我所说,最有趣的是,从最初编写一个解析器、词法分析器、AST,到使用 PEG 来解决问题的不同角度。如果你去 gobuffalo/lush,你甚至可以看到 PEG 文件……顺便说一下,它还没有准备好用于生产。Tim 需要让表达式部分工作得更好。

Tim Raymond:是的。

Mark Bates:从这两个不同的角度来看问题是很有趣的。一方面,这种方式就像是“我要细致地处理文件中的每一部分,理解并跟踪它们,并自己处理,这样我就能确切知道发生了什么……”而另一方面,则是使用 PEG,把这一切交给别人,交给一个代码生成工具,然后抽象地定义它的外观和行为,期望它能在另一端生成正确的东西。

如果没有先理解解析器,我觉得我不可能做到这一点。我真的这么认为。我现在用 PEG 做了很多事情,但如果你不理解解析器,我觉得……我不知道。Tim,你怎么看?

Tim Raymond:这会是一个非常缓慢的过程。就像 Thorsten 刚才说的那样,你会真正欣赏自动生成的代码为你做的事情,因为它实际上是在为你生成一个递归下降解析器,所以你要以递归下降的方式编写这些代码。

如果没有写过一个解析器的背景知识,你会花更多的时间去阅读文档,而不是直接上手。

Thorsten Ball:但你在其中也有一个编译步骤,对吧?比如如果你输出 Go 代码的话……

Mark Bates:哦,那是另一件事。那只是我们正在开发的工具的一个优点之一。这只是它的一个功能。

Thorsten Ball:但你说“如果不理解解析器,我无法写出这个东西”,我的问题是“你觉得如果你对编译一无所知,或者没有接触过这个话题,你还能写出来吗?”

Mark Bates:我对编译一无所知。

Thorsten Ball:我明白了。问题解决了。 [笑声]

Mark Bates:我还没有读你的第二本书。我在这个节目的开头就说了,所以我不觉得不好意思。我没读第二本书。所以,Lush 是一种可嵌入的脚本语言,看起来非常像 Go……因为 Monkey 也看起来非常像 Go,只是去掉了类型。而 Plush 当然也是如此。Lush 走得更远了。

所以从 Lush 生成 Go 代码并不困难。它只需要实现合适的格式化器,或者 print 包来告诉我们“好,这是一条 if 节点,我们该如何在 Go 中打印 if 节点?”而有些东西甚至比这还要简单。你可以拿一个 Lush 的映射,所需要做的只是将底层的映射通过 Sprintf 运行一遍,你就能得到正确的 Go 代码。所以这是 Lush 的一个功能,但它不是一个严格意义上的编译步骤,而是另一个代码生成步骤。

Thorsten Ball:是的。

Mark Bates:然后显然它会被编译。

Tim Raymond:我觉得大多数代码生成看起来都像是编译……尤其是在这种情况下。

Mark Bates:我觉得“转译”(transpiling)可能是你想用的词,对吧?因为你可以把它转译成 Go……对吗?

Thorsten Ball:对我来说,这都是编译器的工作。

Tim Raymond:对我们来说,这也是编译器。

Thorsten Ball:是的。我认为转译和编译的区别在任何对话中都撑不过两分钟。

Mark Bates:就像你说的,最终所有东西都必须编译或转译成计算机能理解的语言。所以 Go 最终也需要被转译或编译成那种语言……是的,我同意,我从来不知道该用哪个词。

Thorsten Ball:你其实已经触及到我想说的……你提到这只是打印和格式化东西。而我觉得这和 Mat 之前提到的解析很像,你一开始写的是针对你问题的定制解决方案,等你了解了背后的通用或抽象模式后,你就可以增强它,使其更好地处理更多的用例。

我在编译的时候也有同样的体验。一开始你会想:“哦,等等,我只需要沿着这些节点遍历并打印东西就好了?”然后你从这里开始,接着你会意识到:“哦,与其立即打印,我可能应该把它转换成另一种数据结构,这样就更容易重新排序。”然后我可以把它再转换成别的东西,只有到那时我才会输出字符串。然后突然之间,你就发明了一个中间语言,或者一个真正的编译步骤,或者别的什么东西。

Mark Bates:对。我在处理 Go 这部分时,遇到了各种各样的东西……比如 Lush(或 Plush)可以有未使用的变量,Monkey 也可以。而未使用的变量在某些语言中是没问题的,但在 Go 中就不行。那么如何在输出 Go 代码时处理这种情况呢?你不知道这个变量是否会在后面被使用。

Thorsten Ball:不错的例子。

Mat Ryer:让我猜猜,你打印变量后立刻做 _= 变量

Mark Bates:你在看这个网站吧,是不是……?

Mat Ryer:不,不,真的是这样吗?

Mark Bates:我可以看到你的眼镜反光了。 [笑声]

Mat Ryer:我希望不是。

Mark Bates: 但这正是我们做的,对吧?我们声明变量,然后立刻在下面做 _= a。所以你不能总是说“哦,我每次都通过 Sprintf 运行它”,因为正如我所说,有些情况下,如果你直接打印出来,虽然那一行的 Go 代码是有效的,但在更大的上下文中它就不一定有效了。

Thorsten Ball:是的。你是对每个遇到的变量都这样做,还是……

Mark Bates:输出 Go 代码吗?目前是的,我就是这么做的。当然,这部分还没有完全完成,但这是最终目标。

Thorsten Ball:因为我想说,如果你能检测到哪些变量未被使用,输出 _= 变量……那你就已经解决问题了,对吧? [笑声]

Mark Bates:你已经做到了,完全正确。

Tim Raymond:我们几乎可以做到,如果我们有另一个中间层的话。

Mark Bates:不……

Thorsten Ball:看,我说对了。

Tim Raymond:你可以做一些数据流分析,然后……是的。

Mat Ryer:不过,完美是进步的敌人。

Thorsten Ball:你知道该读哪本书了,对吧,Mark?

Mark Bates:是的,我听过……听过很多好评。

Thorsten Ball:不错。

Tim Raymond:知道你可以编译东西是一个问题;你会想“哦,我们可以再加一个中间层”,然后就会一直这样下去,“哦,我们还可以做更多……”

Mark Bates:我以为你要说这是和 Mark 合作的一个问题。

Tim Raymond:哦,是的…… [笑声]

Mat Ryer:这连前 50 都排不上…… [笑声]

Mark Bates:根本排不上。离那个榜单太远了……

Mat Ryer:当你看到进展时,它突然开始表现得像是有智能的时候,那种感觉有多令人兴奋?比如当你实现了递归或类似的功能,突然之间看到它表现出出乎意料的聪明行为---那种进展一定让人激动吧?你懂我的意思吗?

Mark Bates:哦,这太神奇了!

Thorsten Ball:是的,是的,我们之间说,这真是超级令人兴奋。 [笑]

Mark Bates:真的,是的。Mat,你知道,今年早些时候我在开发 Plush,当它第一次开始工作时,我不停地发短信,说“天啊,看看这个!我不敢相信我们居然实现了这个!它支持 goroutines!”因为为一个脚本语言添加 goroutines 支持是如此简单。

Mat Ryer:这太棒了。

Mark Bates:是的,整个脚本会做一个 sync.Wait(),它会自动添加并管理所有这些东西……但这一切都非常简单,尤其是用 PEG。它只是在寻找那个关键字,然后说“如果它后面跟着一个函数,做点什么。”当你开始学习解析器和 PEG 时,你能做的事情会让你大开眼界。

Thorsten Ball:对我来说,当我实现闭包时,感觉也是一样的。当你意识到“哦,它真的能工作……”这是一种逐步接近解决方案的方式。你从函数开始,然后你会想:“哦,函数是有值的,所以我可以传递它们。”接着你会想:“哦,闭包是可以传递的函数,但它们带着它们所封闭的环境。”当你实现了这一点时,你会觉得“不错。”

任何 Scheme 或 Lisp 程序员都会告诉你:“一旦你有了闭包,你就可以构建面向对象的东西。”然后你可以构建返回闭包的构造函数,这个闭包封装了一些状态,并返回其他函数,所有这些东西……这真的很酷,充满了乐趣。

Mat Ryer:这确实很酷……因为听起来很难。

Thorsten Ball:让系统自举的需求其实是非常少的,这点让我觉得很美妙。我目前在一旁还在做一个 Scheme 编译器,一个 Scheme 到 x86 的编译器。虽然还远没有完成……基本上是个无止境的项目,但它已经能做很多事情了。看到你构建的基础能不断被利用并扩展,这真的很棒。你只需加入一些内置函数或原语,比如相等比较、类型检查等,然后你就可以基于这些构造更高级的函数……突然之间,你就有了更多的功能可以使用。然后你可以用这些高级构造继续构建更多的东西。

Mark Bates:是的,完全没错。

Thorsten Ball:现在我感觉离目标已经很近了,如果我再投入更多的时间和精力,它可能就能自举并编译自己了。

Mat Ryer:哇……

Thorsten Ball:这真是…… [笑声]

Mat Ryer:这才是我们要的感觉,这很危险。

Thorsten Ball:是的,只有我们这群人能真正理解它的意义。“哦,是的……”

Mat Ryer:想象一下,如果 Monkey 语言最终变成了 Skynet。

Thorsten Ball:这就是我每天晚上梦到的事情…… [笑声]

Mark Bates:回到 PEGs,因为这是我最近一直在做的东西,你刚才提到了闭包---我发现由于递归解析器的构建,实际上处理这些上下文变得相当容易。例如,Lush 支持 var alet aa:=,它们在不同的场景下有不同的含义。我不认为在 Monkey 语言中它们有这样明确的含义,但在 Lush 中确实如此。所以它允许你在现有变量上重新赋值,或者在当前作用域中声明一个新变量。var 声明新变量,但如果该变量已经存在,它会报错。而 a:= 就像 Go 中的赋值操作一样。

而这些信息的携带,以及检查变量是在作用域内还是作用域外,变得更加容易,而且不需要花费很多精力来实现这些功能。

Mat Ryer:那你是怎么决定这个功能的呢?特别是关于 varlet 的两种不同方式。

Mark Bateslet 的存在是为了让人们可以从 Plush 迁移到 Lush。

Mat Ryer:明白了,所以是为了向后兼容。

Mark Bates:是的,它支持 let 这些东西,它也支持 Monkey 风格的 for``---无论是用括号还是方括号(取决于你在哪个国家)---它支持这种方式,但它也支持 Go 中的 range 语句。Lush 正在向一个更像动态解释的 Go 语言发展,而不是 Monkey 原来那样的语言。但我还是希望能支持那些将要被迁移的 Plush 模板。

所以,理解这些东西是很重要的,显然 a:=var 是从标准库里借用的。但 let 是一个更难的决定,我不得不四处看看其他人是如何使用类似 let 的。我最终决定,let 允许你覆盖或设置变量。而 var:= 则不允许你这么做。

Thorsten Ball:是的,是的。

Mark Bates:所以这就是我决定让 let 做这些事情的原因。而且它最符合 Plush 或 Monkey 的做法,基本上就是允许你做任何事情。

Thorsten Ball:是的。有趣的是,这些看似微小的决策实际上会带来广泛的影响……然后你突然意识到,当有人提交一个问题要求添加某个功能时,语言设计者面临的压力有多大。"如果我添加这个功能,那意味着你还可以做其他事情。"

我相信那些非常精通理论的人可以正式地说“如果我们添加这个功能,其实会带来这些影响”,但当你手动思考这些问题时,这真的很有启发性……比如“如果我允许定义两个递归调用对方的函数,那么我突然就能实现所有这些功能。”或者延迟绑定;你可以在函数中调用一个尚未声明的函数,这就是递归函数调用的本质---然后你基本上可以在另一个函数中实现循环,或者返回一个函数,等等。这真的很疯狂。

Mat Ryer:这也很有趣,因为我们在社区中谈论泛型和其他特性时,很多人只是单纯地考虑他们是否想要这个功能,而没有真正考虑“实现这个功能意味着什么?它会带来什么后果?”

Mark Bates:模板化是我非常希望能有泛型的一个地方……这是我真正需要它的少数地方之一。再看看 Plush,它有一个巨大的 switch 语句来处理输出,我根本不喜欢这种方式。

Mat Ryer:是像类型 switch 吗?

Mark Bates:是的,基本上就是这样。"如果是 fmt.Stringer,那就这样做。如果是 HTML,做这个。如果是 slice,做那个。"类似的东西。

Tim Raymond:你在处理解析器时,几乎每次都要面对空接口和空接口的切片

Mark Bates:每当你处理解析器时---Thorsten 会告诉你,基本上都是空接口。 [笑声]

Thorsten Ball:是啊……

Tim Raymond:这真的很麻烦。

Mat Ryer:但泛型会有助于解析器吗?你能把它强类型化吗?

Mark Bates:我不知道……我不知道。

Tim Raymond:我也不确定。也许吧。我不知道。

Mat Ryer:嗯。

Thorsten Ball:我觉得它会让你的 AST 的类型定义更小,减少样板代码。因为你可以简单地说“这是一个值为整数的 AST 表达式,这是一个值为字符串的 AST 表达式”,类似的东西。但我不确定它是否会让解析器本身变得更小。某些其他语言结构,比如模式匹配或解构,确实能让代码更简洁,但最终它们都是 ifelse…… [笑声]

Mark Bates:最终所有东西都是一个巨大的 if-else 语句,对吗?

Thorsten Ball:是的,是的。这就是 Go 2 的所有内容,对吧? [笑声]

Mark Bates:它会出现在 Go 2 里,会很有趣。

Thorsten Ball:Mat,你刚才说的泛型---这其实是我想到的事情……有时候我感觉人们低估了添加这种功能的后果。是的,你可以用它来实现 mapreducefold,无论如何都可以用泛型实现……但这会为未来带来什么?会出现什么样的模式?人们会如何使用这门语言?这对现有的所有东西意味着什么?标准库中的一些东西是否会变得过时,因为人们不再使用它,而是构建自己的东西?然后你该如何维护这些东西?

所有这些问题都不是轻松的事情。我不想在这个敏感话题上过于政治化,但我确实喜欢……

Mark Bates:大家准备好发推特了吗?

Thorsten Ball:[笑声] ……我喜欢 Go 团队在考虑这些权衡时的方式。

Mat Ryer: 是的,我也这么觉得。他们确实也谈到了这一点,谈到了实现这些功能的代价。我觉得很多人没有在自己的项目中考虑到这一点。公司和团队通常认为他们的任务就是不断添加功能,但你添加的每个功能都会带来后果。我觉得如果有人读过你的书,他们就会更清楚地理解这些问题,这也适用于 Go 语言。

Thorsten Ball:是的。而且我觉得在日常工作中,大多数人不会直接开发那些被系统其他部分依赖的组件。你通常是在给一个系统添加功能,而不是像 Mark 那样在开发框架或底层组件。但是一旦你开始构建那些被系统其他部分使用的基础功能时,你就会意识到任何对这些基础功能的改动都会带来巨大的影响。

举个例子,比如一个模板语言---它自带一些内置函数或功能,如果你稍微改动一点,你就会看到这些改动的放大效应。但如果你只是添加功能而不动这些基础构造,那么对系统其他部分的影响会很小……这就是一个设计良好的系统的美妙之处---添加功能不会对系统的其他部分造成太大后果。但一旦你改动了底层部分,后果就来了。

Mat Ryer:是的。我觉得每当我们构建某种抽象时,我们实际上就是在构建某种“基础”。这是我们程序员最喜欢的一天,当你实现某个功能时,想“哦,我可以用一种稍微抽象的方式来做,然后解锁很多可能性”,那种感觉真的很好。我想我们都对此上瘾了……然后当第二次出现时,虽然不完全适合,但还是够接近的;我们就会做一些小的调整,加点配置,不用担心。到了第三次,那就完全不适合了,你最终会得到一个像“弗兰肯斯坦”一样拼凑的抽象。所以,这种思维方式不仅适用于你在写解析器时,我觉得适用于所有编程领域。

Thorsten Ball:还有第二好的日子---删除代码,对吧?

Mark Bates:哦,我爱删除代码。

Thorsten Ball:也许是反过来,我更喜欢删除代码胜过抽象。

Mark Bates:我爱删除代码……

Tim Raymond:我在写解析器时发现,有时解析器会让你能够删除代码。

Thorsten Ball:没错!

Tim Raymond:我写的第一个解析器实际上是一个随意定义的语言的正式化版本,这个语言是用正则表达式解析的,它实际上也是一个模板语言……

Mark Bates:他说的是我写的一个程序……

Tim Raymond:呃,可能是……可能是……

Thorsten Ball:我本来想说,这听起来像是“世界上每个代码库里都有的东西”。

Mark Bates:是的,他在说的是我多年前写的东西。你是在说 Bry。

Tim Raymond:是的,我是在说 Bry。 [笑声]

Thorsten Ball:有点尴尬……

Mark Bates:没关系,你可以谈论我过去的错误。我肯定会这么做。我也会谈论你的错误……[笑声] 别忘了,今天是 Tim 的生日。

Tim Raymond:随着时间的推移,更多的功能被添加进来,这个模板语言的语法在不同的部分有所变化……所以通过编写一个更正式的语法,我可以解析所有这些内容,然后做一些类似 go fmt 的事情,我实际上删除了很多东西。然后我可以开始从官方解析器中删除代码,因为那些规则再也不会匹配了。

Mark Bates:是的。

Thorsten Ball:没错。

Tim Raymond:但我还可以完全控制解析器、语言,以及所有的使用方式……这与大多数编程语言不同,你不知道人们会如何使用它。但如果你能控制它的每一个用法,你就有很大的能力去改变它。

Mark Bates:你提到格式化---其实是你告诉我的,如果我使用 PEG 并按照某种方式布局代码,那么格式化就会自然而然地发生……它几乎是自动完成的。所以有一个 Lush 的 fmt 工具,它可以格式化你的 Lush 脚本。这个过程非常简单,因为你有一个很好的节点,有一个很好的解析器,这些类型知道它们应该是什么样子……就像 Go 一样,它允许你打印出一个更好、更正式的版本,清理代码,删除代码,就像你说的那样……这是非常有趣的工作。

Mat Ryer:Thorsten,如果我们的听众想要买你的书,他们应该去哪里?

Thorsten Ball:是的,可以去 InterpreterBook.com[14]CompilerBook.com[15]。如果想要纸质版的话,可以去亚马逊。

Mat Ryer:好的。

Mark Bates:亚马逊的网址是什么?是一个网站吗,还是……?

Thorsten Ball:是 http://

Mark Bates:等一下,我在做笔记,慢点,慢点……

Mat Ryer:所以它不安全,是吗,Thorsten?

Mark Bates:是正斜杠还是反斜杠?大家都说反斜杠,但它们其实是正斜杠,还是反过来?我有点困惑。好吧,我们私下再讨论这个……

Thorsten Ball:也许我们可以把它放在节目注释里……

Mark Bates:对,在节目注释里加个链接。亚马逊的网址会在节目注释里。

Thorsten Ball:是的,亚马逊。在线商店。还有 .co.uk。

Mark Bates:哦,他们在那也有?他们在扩展啊。

Thorsten Ball:是的,现在他们是全球性的。

Mark Bates:真为他们高兴。听到这种创业成功的故事真是太好了。

Thorsten Ball:是的。他们现在在法国、意大利、德国……到处都有了。

Mark Bates:到处都有。太棒了。

Mat Ryer:但书籍只有英文版吗?

Mark Bates:是书还是亚马逊?

Thorsten Ball:不止是英文版,是的。

Mat Ryer:好的。

Thorsten Ball:我不会冒险把它翻译成德文。老实说,当我用德语谈论编程时,其中 75% 都是英语单词。

Mat Ryer:对。顺便说一下,这些都是美式英语的单词。美式英语是计算机的语言。

Thorsten Ball:我在写作时小心翼翼地不偏向某一种……所以我尽量切换使用。坦白说,我写作时会忘记我用的是英式还是美式英语……我知道这会引起一些人的不满,但我很乐意这么做。

Mark Bates:你真是个反叛者……

Thorsten Ball:是的。 [笑声]

Mark Bates:你绝对是个朋克摇滚歌手,如果世上真有这种人的话。 [笑声]

Thorsten Ball:是的。

Mark Bates:有时候我会在 color 里加一个 u,有时候我不加。

Thorsten Ball:有时候我用牛津逗号,有时候我根本不用逗号。 [笑声]

Mat Ryer:对于一本关于解释和解析文本的书来说,这其实挺讽刺的。

Thorsten Ball:是的。但这也是给读者的一次很好的练习,对吧?

Mat Ryer:是的。

Mark Bates:你不能用 Monkey 的 fmt 来格式化这本书吗? [笑声]

Thorsten Ball:其实,你们不想知道我的工具链有多疯狂。

Mat Ryer:好吧,随着 Mark Bates 刚刚提出的“Monkey fmt”这个词,恐怕我们的节目也要结束了。 [笑声] Thorsten,Tim,Mark,非常感谢你们。这真是太棒了。下次节目我们会邀请 Francesc Campoy,他会和我们讨论图数据库。那一定很有趣……所以请大家到时收听。到时见,谢谢大家。

参考资料
[1]

Compilers and interpreters: https://changelog.com/gotime/107

[2]

Thorsten Ball: https://github.com/mrnugget

[3]

Tim Raymond: https://github.com/timraymond

[4]

Mat Ryer: https://github.com/matryer

[5]

Mark Bates: https://github.com/markbates

[6]

“Writing an Interpreter in Go”: https://interpreterbook.com/

[7]

用Go语言自制编译器: https://book.douban.com/subject/35909089/

[8]

用Go语言自制解释器: https://book.douban.com/subject/35909085/

[9]

Buffalo 项目: https://github.com/gobuffalo/buffalo

[10]

Plush: https://github.com/gobuffalo/plush

[11]

Gopher Guides: https://www.gopherguides.com/

[12]

Bison: https://pandolia.net/tinyc/ch13_bison.html

[13]

Pigeon: https://github.com/mna/pigeon

[14]

InterpreterBook.com: http://interpreterbook.com

[15]

CompilerBook.com: http://compilerbook.com


旅途散记
记录技术提升