我想诸位应该都用过结构化绑定(structure bindings)吧。或者听说过“模式匹配”这个工具(我之前的一些文章中就用过)。
今天来聊聊这个有趣的语言特性。
依然先放代码:
| |
什么是模式匹配
模式匹配并不是“字符串模式匹配”,它是一种语言特性。
依然是从各位非常熟悉的结构化绑定说起。结构化绑定可以让你便捷的解构一个结构体的值,并绑定到不同的变量上。
模式匹配则是它的升级版:我们可以使用描述化的写法判断一个值“是不是长这个样子”,如果是,就“将其中的对应部分绑定到给定变量上”,并执行对应的一段代码。
简单来说,结合了结构化绑定的数据解构能力和 cond/if 的条件分支能力。我们写 (match x [(human head body arm leg) head]) 来描述 x 是不是一个有头、身体、胳膊和腿的人,如果是,就返回他的头。
从 LOP(语言导向编程)的视角看,本质上是设计了一种 EDSL 来方便对数据形状的检查以及数据解构。
它有什么应用
想想结构化绑定有什么应用。模式匹配的应用更丰富:在写解释器等等的时候你常常需要检查数据的形状并进行解构。
更加常见的情形是处理 union type 等代数数据类型。你可能会遇到一个类型 Res<T, E> = Union<T, E>,它可以是返回值类型 T,也可能是函数执行出现问题时的异常类型 E。这时你就可以写
| |
而不是一大堆麻烦的 if 和 let。
如何实现
这个东西看上去比较神秘,其实大概可以想到是利用列表的特性递归匹配。但是直接做非常不好做,你可能会想到类似于编译出一个过程判断是否符合模式,另一个过程绑定数据。这样常数很大而且很不优雅。
正确的方法是我们先不考虑完整的 match(更像 cond)而是考虑一个弱化版的 pmatch-if:接受一个值和一个模式,如果值符合模式就进行绑定并执行第一个分支,否则执行第二个分支。
假设已经实现了这个,那么完整的 pmatch 就好实现了:只需要把一串 pmatch-if 串起来。
| |
也就是这堆东西的用处。不过注意一点:宏会将接受到的参数直接 copy 到它出现的位置。这可能导致重复求值,所以我用 pmatch 额外包了一层 let。
接下来考虑 pmatch-if,这是逻辑的核心。
我们要解构的东西是列表(scheme/racket 的核心语法骨架)。列表具备递归构造,这启示我们进行递归的模式匹配。
那么先考虑几种基础情况:模式是一个标识符、模式是一个原子数据、模式是一个用 quote 包起来的 cons pair、模式是 any/p。
这几种情况,标识符就直接绑定并匹配通过,any/p 无条件匹配(不过不绑定),原子数据或 quote 包装的就进行比较(equal?)。
然后接下来就是难点:cons pair 怎么做?
非常简单,递归匹配即可。我们的匹配条件相当于是数据是个 pair,且模式的 car 和 cdr 两个部分分别都能匹配数据的对应部分,然后模式的两部分的绑定合在一起。
这里很容易联想到 church encoding 中实现 and 的方法:
| |
也即两层 if 嵌套。那么就搞定了。
如果你这么写了就会发生 () 惨案。因为 () 会进入对原子匹配的分支,然后就会生成 (if (equal? v ()) ...) 这样的代码,产生裸的 () 然后爆掉。正确的做法是多加一层 quote:
| |
这样,原子有自求值的特性,多加一层 quote 不影响(会被运行时的 eval 弄掉)而 null(空链表 '())则能多一层 quote 而避免报错。
那个 ? 的分支是一个小的扩展,类似于 racket 标准库的 ?。语法是 (? predicate pat),意思是如果 v 满足 predicate 则匹配模式 pat,否则匹配失败。是 guard 的一种语法糖(不过这里没有实现 guard。留作习题 :P)。