软件设计

Swift 解释器模式(Interpreter Pattern):亲手打造迷你语言解释器

翻开设计模式的书,解释器模式总会在接近最后的位置出现。这是一个很容易因为“这到底什么时候会用到”而略过的模式。

4 分钟阅读
Swift 解释器模式(Interpreter Pattern):亲手打造迷你语言解释器 封面图

翻开设计模式的书,解释器模式总会在接近最后的位置出现。这是一个很容易因为“这到底什么时候会用到”而略过的模式。

不过,在开发计算器 App 时,我遇到了必须直接计算字符串表达式的时刻。那时解释器模式正好派上了用场。

本文将使用 Swift 亲手创建一个非常小的语言解释器,也就是迷你解释器。目标是读取类似 1 + 2 * 3 的表达式,并输出数字 7。

先说结论:

解释器模式把每条语法规则转换为类(或枚举),组装成树结构,再通过递归遍历得到结果。

听起来很难,但看过代码后会发现比想象中简单。让我们一起实现它。

什么是解释器模式?

解释器模式是一种创建“自己的小型语言”,并用代码表达解释该语言的规则的方法。

这里说的语言并不复杂。只要是具有固定语法的小型表达式,例如公式、搜索过滤条件或游戏规则脚本,都属于这一范畴。

核心思想只有一个。

把语法的每个元素创建为对象,并让这些对象拥有解释自身的 interpret() 方法。

例如,考虑表达式 1 + 2。其中有数字 1、数字 2,以及加法运算 +。

数字会解释为“我只返回自己的值”,加法则解释为“先解释左侧和右侧,再把两者相加”。

这些小规则组合成一棵树,只要从最顶层调用一次解释,就会沿着树向下完成计算。


用 Swift 设计表达式树

在 Swift 中,enum 非常适合这种模式。使用递归枚举可以非常简洁地表示树结构。

我一开始用协议和类实现,但改成 enum 后,代码减少了一半。

首先定义表示表达式的枚举,分为一个数字,以及将两个表达式相加或相乘的情况。

这个枚举会再次包含自身,因此必须添加 indirect 关键字。

indirect enum Expr {
    case number(Double)        // 数字字面量
    case add(Expr, Expr)       // 加法
    case multiply(Expr, Expr)  // 乘法
}

现在创建解释这棵树的函数。递归处理每种情况,就是解释器模式的核心。

下面的函数接收一个表达式,计算并返回实际的数字值。

func interpret(_ expr: Expr) -> Double {
    switch expr {
    case .number(let value):
        return value
    case .add(let l, let r):
        return interpret(l) + interpret(r)
    case .multiply(let l, let r):
        return interpret(l) * interpret(r)
    }
}

做到这里,就可以把 1 + 2 * 3 组装成树并进行计算。

像 .add(.number(1), .multiply(.number(2), .number(3))) 那样手动创建树,再传入 interpret,就会得到结果 7。

先对乘法进行分组,顺序才是正确的
先对乘法进行分组,顺序才是正确的

如何把字符串表达式转换成树?

这里可能会产生一个疑问:用户输入的不是树,而是类似 "1 + 2 * 3" 的字符串。

把这个字符串转换成树的过程称为解析(parsing),执行这项工作的代码称为解析器。

按照标准做法,解析器分为两个阶段。

  1. 词法分析器(lexer):将字符串拆分为一个个 Token(例如:1、+、2)
  2. 解析器(parser):根据语法规则将 Token 组装成树

完整实现这两个阶段会让文章太长,所以这里仅介绍概念。

关键在于:解释器模式本身只负责解释树。把字符串解析成树是另一项工作。

因此在学习阶段,我建议暂时跳过解析器,手动创建树,先完成解释器。这样才能看清模式的本质。

代码画在屏幕上,树画在笔记本上。这样画出来就很容易理解
代码画在屏幕上,树画在笔记本上。这样画出来就很容易理解

实际用在哪里?

说实话,从底层亲自实现解释器模式的机会并不多。对于复杂语言,使用已经成熟的解析器库要好得多。

即便如此,理解这个模式仍然有很多收获。

  • 可以了解正则表达式引擎在内部是如何工作的
  • 可以理解为什么 SwiftUI 的声明式语法是树结构
  • 创建搜索过滤器或表达式计算等小型 DSL(Domain-Specific Language,领域专用语言)时,可以直接应用

尤其是在计算器、条件过滤器或游戏脚本这类语法简单、范围有限的场景中,自己实现反而更简洁。

掌握这个模式后,我看 SwiftUI 的视图结构完全不同了。每个视图最终都是那棵待解释树中的一个节点。

了解这个模式后,SwiftUI 视图看起来完全不同了
了解这个模式后,SwiftUI 视图看起来完全不同了

总结

今天我们通过用 Swift 创建迷你语言解释器,了解了了解释器模式。

用递归枚举创建表达式树,再通过 interpret 函数递归解释。这两点就是全部。

刚开始可能会觉得陌生,但亲自计算 1 + 2 * 3 后,就会出现“啊,原来如此”的瞬间。请务必亲自敲一遍今天学到的代码。

延伸阅读