为什么可逆计算要求以 DSL 形式存在
可逆计算的核心操作是“提取差量”和“合并差量”。但这两步操作的前提是:你能精确地说出结构 A 和结构 B 之间差了什么。
这不只是一个技术问题,而是一个本质性的认识论问题。
代码是"不透明"的
看两段 Java 代码:
// 版本 A
public void save(Entity entity) {
dao.save(entity);
sendNotification(entity);
}
// 版本 B
public void save(Entity entity) {
dao.save(entity);
sendNotification(entity);
logAudit(entity);
}
版本 B 比版本 A 多了一行 logAudit(entity)。你能看出这个差异,是因为你知道“第4行有变化”。
但如果代码被格式化了呢?如果变量名改了呢?如果方法重构成了多函数呢?两个函数之间的差异就不再是“多了一行”那么清晰了。
问题在于:通用代码的语言结构是松散的。同样的语义可以用无数种方式表达,差量提取的结果依赖于格式、命名约定、代码风格等人为因素。这不是一个稳定的、可计算的结构。
相比之下,一段结构化的 DSL 定义:
<entity name="User">
<columns>
<column name="name" />
<column name="email" />
</columns>
</entity>
每个节点、每个属性都有明确的含义和固定的位置。两个 DSL 之间的差异可以精确到“第二个 column 后面新增了一个 column”。这是结构化的、可计算的差异。
可计算的前提:结构
“可计算”意味着操作可以被算法精确执行,不需要人的判断。这要求操作对象具有严格的结构约束。
DSL 满足这个要求:
- 定义明确:每个元素有固定含义,由 XDef 元模型约束
- 位置固定:同义表达没有多种写法,减少了歧义
- 可枚举:所有合法元素的集合是可知的
通用代码不满足这个要求:
- 表达自由:同一种逻辑可以用循环、递归、函数式、流式等多种写法
- 边界模糊:一个方法从哪里开始、到哪里结束,取决于人的习惯
- 隐式依赖:全局变量、上下文、反射调用,使结构分析复杂化
可逆计算选择了 DSL,不是因为 DSL 更“高级”,而是因为只有结构化的 DSL 才能支撑程序化的差量提取和合并。
三个核心能力的来源
可逆计算的三个操作——提取差量、存储差量、合并差量——都依赖 DSL 的结构化特性。
提取差量依赖结构化
只有两个元素在同一个精确的位置上,才能说这个属性“从 X 变成了 Y”。通用代码中,同一个逻辑可以用完全不同的代码结构表达,无法精确比较。
存储差量依赖结构化
差量需要独立于原始结构存在。对于通用代码,差量就是 patch 文件——它在行级别工作,一旦原始代码的某一行因为格式化而移动,patch 就失效了。对于 DSL,差量在元素级别工作——新增字段、删除属性、替换配置,不受空格和换行的影响。
合并差量依赖结构化
多层差量叠加时,需要明确“上层覆盖下层”还是“上层追加到下层”。结构化树(XML/JSON)上的合并规则是通用的——每个节点都可以附带 x:override 指令。行级别的 patch 无法表达“删除这个元素”或“替换这个子节点”这种语义。
更直接的类比:数据库与代码
数据库也是结构化的:每一行有固定的列,每条 SQL 查询返回可预测的结果。所以数据库可以做增量同步、主从复制、数据对比——这些都是差量操作。
但如果把数据存成一个个文本文件,用 grep 和 sed 来操作呢?能做增量同步吗?不能。因为文本文件的含义不再被明确定义了。
DSL 对于可逆计算,相当于数据库表结构对于数据操作。结构是一切可计算操作的前提。
这不是一种限制,而是一种赋能
把业务逻辑用 DSL 表达,表面上多了一层抽象。但这层抽象带来的收益远大于成本:
- DSL 比通用代码更短:50 行 DSL 胜过 500 行 Java
- DSL 更容易分析:AI 操作结构化 DSL 比操作散乱代码可靠得多
- DSL 的 IDE 支持自动生成:XDef 定义后,语法提示、验证、调试全自动
- DSL 的差量机制解决软件定制化的核心难题:不修改源码即可增删改功能
可逆计算选择 DSL,不是因为教条,而是因为离开了结构化,差量提取和合并就是不可计算的。