Lec 19 领域特定语言
在这次阅读中,我们将开始探索一个用于构建和操作音乐的小型语言的设计。核心思想是:当你需要解决一个问题时,不要只写一个程序来解决那个特定的问题,而是设计一种语言,使它能解决一系列相关的问题。
本次阅读的目标是介绍“将代码表示为数据”的概念,并让你熟悉音乐语言的初始版本。
在此过程中,我们还会介绍访问者模式(Visitor Pattern),它是将函数视为一等值(first-class values)的又一个例子,同时也是对递归数据类型执行操作的另一种实现策略。访问者模式常见于那些用递归数据类型实现的小型语言中。
将代码表示为数据
回忆一下我们在“递归数据类型”中见过的 Formula 数据类型:
Formula = Variable(name:String)
+ Not(formula:Formula)
+ And(left:Formula, right:Formula)
+ Or(left:Formula, right:Formula)我们用Formula的实例来表示命题逻辑公式,e.g.(p ∧ q), 用数据结构表示为
And(Variable("p"), Variable("q"))在语法与解析的术语中,公式是一种语言,而 Formula 是该语言的抽象语法树(AST)。但问题是:为什么要定义一个 Formula 类型?TypeScript 本身已经能表示布尔变量的逻辑与、或、非操作。比如,给定布尔变量 p 和 q,我们完全可以直接写:p && q 完成了。
答案是这样的:表达式 p && q 在程序运行时会立即被求值。而 Formula 的值(例如 And(...))则是一个一等值(first-class value),它可以被存储、传递、返回、修改,也可以在之后再求值(甚至多次求值)。
当我们定义一个ADT时,其实是在扩展TypeScript提供的内建类型宇宙,引入一个适合我们问题领域的新类型以及它的操作。这种新类型就像是一种新语言:它提供了一组新的“名词”(值)和“动词”(操作),供我们操作。当然,这些新的名词和动词本质上仍然是建立在已有抽象之上的抽象。
语言相比于普通程序更灵活,因为我们可以用它来解决一整类相关问题,而不仅仅是某一个特定问题。
这就像直接写 p && q 与设计一个 Formula 类型来表示语义等价的布尔公式之间的区别。又或者是直接写一个整数加法函数,与设计一个 IntegerExpression 类型来表示整数算术表达式(从而能够存储、操作、重排、求值等)之间的区别。
这种语言被称为领域特定语言(DSL, Domain-Specific Language),因为它只解决特定领域中的问题,而不像 TypeScript 或 Python 那样是通用语言。
DSL 又可分为两类:
- 外部DSL:具有独立于任何通用编程语言的自定义语法和语义。我们在课程中已经见过一些例子,如正则表达式、ParserLib 文法、以及第三次作业中的语言。
- 内部DSL:嵌入在某种通用语言中,利用宿主语言的语法和抽象机制来实现自己的语义,而不重新发明语法。
由于一等函数的存在,我们可以在宿主语言中创建出极为强大的内部 DSL,因为我们能够将计算模式捕捉为可重用的抽象。我们将在这门课中开发的“音乐语言”就是一个内部 DSL 的例子。
音乐语言
首先,我们会学习如何编写一个使用 MIDI 合成器播放音乐的程序。接着,我们会通过编写一个用于表示简单旋律的递归抽象数据类型来开始开发我们的音乐语言。我们还会为音乐选择一种字符串表示法(notation),并实现一个解析器(parser),将字符串解析为 Music 类型的实例。
ZIP file containing this code,你可以在提供的链接中查看这个基础音乐语言的完整源代码。解压后执行 npm install 来准备项目环境。