把一段始终返回 100 的简单循环函数,编译成几百行层层嵌套的 SQL 子查询,丢进 PostgreSQL 17 里直接跑出结果。开发者 Sandy Maguire 在 2025 年 10 月 14 日交出了这样一个看似荒诞的开源项目 catlang(原网页抓取时间常被误读为 2026 年,那其实是网站页脚的版权年份)。它不是 PRQL 或 Malloy 那种帮数据分析师少写括号的新型查询语言,而是一门真正的通用编程语言,编译器的唯一产物就是一条巨型 SELECT 语句。
这件事最值得看清的地方在于它的跳板。作者在 GitHub 仓库(isovector/catlang)里把这门语言定义为面向范畴的箭头化语言,甚至塞进了一个 brainfuck.arr 示例;他选 SQL 当后端纯粹是为了找个足够离谱的靶子,去测试编译器内部那套纯范畴论中间语言(IL)。这套实验在 Hacker News 上拿到了 39 个赞和 3 条评论(顺带一提,Reddit 上早年出现过另一个同名项目,后来改名叫 qat,和这个 SQL 编译器毫无关系)。
变量是如何被彻底抹掉的
常规编程语言离不开变量名,但在 catlang 的例子里,那个接收参数 x、在循环里不断给变量加 1 直到满 100 的 count 函数,进入中间层后连一个字母变量都没剩下。

这背后的理论源头是 Conal Elliott 早年提出的编译到范畴(Compiling to Categories)。编译器在脱糖阶段做了一次外科手术,把所有具名变量全部消解,替换成无变量的栈投影(stack projections)。你在源码里写的变量读取,到了中间层全部变成从当前数据元组里按位置取值的投影算子 prj₁ 或 prj₂。
经过这一步清洗,整段程序被压成了四个基础代数原语:组合、积、余积和投影。
这套符号看着像天书,工程逻辑却异常干净。当一门语言的中端不再关心变量作用域和寄存器分配,只保留纯粹的代数结构时,编译后端的门槛就被瞬间踏平了。任何稀奇古怪的目标平台,只要能勉强凑出这几个基础算子的对应实现,就能直接充当这门语言的运行引擎。
范畴论原语与 SQL 的硬核映射表
理解了这层代数抽离,再看那段让人头皮发麻的 SQL 代码,里头的章法就全清楚了。现代关系型数据库在引入递归公共表表达式(WITH RECURSIVE CTE)之后早就具备了图灵完备性,Sandy Maguire 做的事情,就是给范畴论算子和 SQL 语法建了一张一对一的翻译字典:

- 组合(
A ⨟ B).意思是先执行 A 再把结果喂给 B,到 SQL 里直接变成层层包裹的嵌套子查询。 - 积分支(Product forks):代表同时计算两个分支并把结果拼成元组,在 SQL 里被翻译成直白的
CROSS JOIN。 - 余积消除(Coproduct elimination):也就是源码里的
case...of条件分支(inl和inr),SQL 里没有原生的代数数据类型,编译器就用UNION把左右两条分支拼起来,再靠WHERE "f0" IS NOT NULL这类空值检查来筛选真正激活的分支。 - 循环原语(
cochoice):类型签名是(Either a c ~> Either b c) -> (a ~> b),它以Left a启动,每次算出Right c就塞回循环体继续跑,直到吐出Left b才停下;这套反馈机制在 SQL 后端直接映射成了WITH RECURSIVE递归查询。
- 结论.只要目标平台能拼出组合、积、余积和递归这四块积木,哪怕是关系型数据库,也能被强行改造成通用计算虚拟机。
纯粹数学撞上关系数据库的那道裂痕
如果故事只讲到这里,这会是一篇完美的编译器架构童话。但我更在意的是那段截断的 SQL 代码里藏着的一处脏活:作者在递归 CTE 里偷偷塞进了一个 clock_timestamp() AS step,并在外层用 ORDER BY step DESC LIMIT 1 来抓取最终结果。

为什么最纯粹的范畴论代数,最后要靠物理世界的时间戳来救场?
纯粹的代数能消除所有变量,却消不掉底层引擎的物理脾气。
问题出在底层语义的根本冲突上。范畴论里的 cochoice 是一个干净的状态机,跑完循环只交出最后那个结果;但 SQL 的本质是无序的集合代数,WITH RECURSIVE 在迭代时会把每一次循环产生的中间行全部攒在结果集里。为了从一堆历史轨迹里单挑出最后一次迭代的产物,作者不得不放弃代数的纯粹性,引入 clock_timestamp() 这个依赖物理时间的非纯函数,给每一轮递归打上时间标签,再倒序截出最后一条。
作者自己也坦白这是一个取巧的 工程 Hack。
- 风险.一旦某个原语在单步计算里产生了多行数据,
LIMIT 1就会直接把有效数据截断,整套建立在严密数学之上的编译大厦就会当场算错。
这桩小事特别像十九世纪末数学家试图用纯逻辑公理彻底框住算术系统,越往底层走,越躲不开物理载体本身的脾气。古人讲「大道至简」,可真到了写编译器后端的泥潭里,往往是道越简,术越繁。
没人会在生产环境里用几百行 CROSS JOIN 和 UNION 去算一个加减法,从数据库工程的角度看,这段生成的 SQL 慢得毫无道理。但对做领域特定语言(DSL)和编译器中端的工程师来说,catlang 的价值根本不在数据库里。它用最极端的方式证明了,只要把中间表示收拢到足够小的范畴论内核,同一套前端代码明天就可以用同样的手法编译成硬件电路、GPU 着色器或者分布式数据流图。
至于那个用来凑数的 clock_timestamp(),它反而是整场实验里最真实的注脚。它提醒所有迷恋抽象架构的工程师,你可以把变量从源码里消得干干净净,却永远消不掉底层引擎那点固执的物理边界。
