CLASS 19 · 2026-04-20 · 17-19 系统与界面

小型语言

小型语言(little language)是为特定领域设计的专用语言。通过把代码当作一等数据,用递归数据类型表达语法,再配合解析器与访问者模式,我们可以在宿主语言中嵌入一个优雅的子语言。

把代码当作数据

在 6.102 中,一个 Formula 值可以存入变量、传给函数、像普通数据一样操作。这种「代码即数据」的思想是构建语言抽象的基石。

一等值(first-class value):可以存入变量、作为参数传递、作为返回值、被函数操作的语言实体。

用语言解决问题

一个语言比一个单纯的程序更灵活:它允许用户用领域术语描述问题,而不必关心实现细节。领域特定语言(DSL)分两类:

音乐语言

本课以一个音乐语言为例:设计一种能生成和播放音乐的 DSL。它展示了从数据类型定义到解析器再到播放器的完整流程。

播放 MIDI 音乐

SequencePlayer 接口提供 addNotestartMidiSequencePlayer 是它的一个实现,负责把音符变成真实的 MIDI 输出。

音乐数据类型

音乐由三种变体构成:

\[ \texttt{Music} \;=\; \texttt{Note}(\textit{duration}\!: \texttt{number},\ \textit{pitch}\!: \texttt{Pitch},\ \textit{instrument}\!: \texttt{Instrument}) \;+\; \texttt{Rest}(\textit{duration}\!: \texttt{number}) \;+\; \texttt{Concat}(\textit{first}\!: \texttt{Music},\ \textit{second}\!: \texttt{Music}) \]
interface Music {
  play(player: SequencePlayer, beat: number): number;
}
class Note implements Music { /* ... */ }
class Rest implements Music { /* ... */ }
class Concat implements Music { /* ... */ }

组合子模式

基本对象(音符、休止)与组合体(拼接)属于同一类型,客户端可以用统一的方式处理它们。这就是组合子模式(composite pattern)。

音乐记谱法

为了让人能直接书写音乐,我们引入简化的 abc 记谱法。它把音符写成字母,用数字表示时值,用 | 表示小节。

实现解析器

notes 函数把 abc 记谱字符串解析成 Music 对象。它使用递归下降:每个非终结符对应一个解析函数,彼此递归调用。

function notes(input: string): Music {
  // 递归下降:解析音符序列,遇到 '|' 分段
  // 返回 Concat(Note(...), Concat(...))
}

实现播放器

play 函数把 Music 对象交给 SequencePlayer,按节拍依次播放每个音符。

递归类型上的函数:解释器模式

操作在接口上声明,在每个变体中实现——这正是我们熟悉的解释器模式

解释器模式的缺点

  1. 代码分散在各个变体类中,难以一眼看到某个操作的完整逻辑。
  2. 新增一个操作,必须修改所有变体类。

模式匹配

函数式语言中,递归类型的操作常写成模式匹配(pattern matching):每个分支对应一种变体。TypeScript 没有原生模式匹配,但可辨识联合 + 条件判断可以模拟它。

访问者模式

为了克服解释器模式的缺点,我们引入访问者模式(visitor pattern):

interface MusicVisitor<R> {
  onNote(n: Note): R;
  onRest(r: Rest): R;
  onConcat(c: Concat): R;
}

interface Music {
  accept<R>(v: MusicVisitor<R>): R;
}

class DurationVisitor implements MusicVisitor<number> {
  onNote(n: Note) { return n.duration; }
  onRest(r: Rest) { return r.duration; }
  onConcat(c: Concat) { return c.first.accept(this) + c.second.accept(this); }
}

为什么用访问者?

当操作频繁新增而变体相对稳定时,访问者模式更合适。

关键要点

  • DSL小型语言为特定领域设计,分外部 DSL 与内部 DSL。
  • 组合子基本对象与组合体属于同一类型,统一处理。
  • 解析递归下降把记谱字符串翻译成 Music 对象。
  • 解释器操作分散在变体中,新增操作需改所有类。
  • 访问者双重分派按操作分组,新增操作更容易。