老赵写字的地方

为什么可逆计算要求以 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,不是因为教条,而是因为离开了结构化,差量提取和合并就是不可计算的。

进一步阅读

zh/nop/reversible_computing_implementation/why_dsl.txt · 最后更改: 由 127.0.0.1