Quine:输出自己源代码的程序
一段代码跑起来,打印出的东西和它自己的源码一个字符不差——这种程序叫 quine。这篇讲清它的原理(一份字符串同时当数据和代码用)、名字背后的自指悖论,以及几种越玩越离谱的变体:删一个字符还能自愈的抗辐射 quine,和 128 门语言接龙的衔尾蛇程序。
有一类程序的唯一功能是:运行之后,输出自己的源代码,一个字符不差。这种程序叫 quine(自复制程序 / 自指程序)。
听起来像个无聊的杂技,但它背后连着计算理论里最深的一个定理,名字则来自一位逻辑学家和一本奇书。文中所有代码示例我都实际跑过并逐字符比对了输出——毕竟一篇讲「输出等于源码」的文章,示例要是跑不出来就太丢人了。
规则:不许偷看
先说清什么算作弊。quine 不允许接受任何输入,尤其不允许读取自己的源文件。像下面这种:
cat $0一行就把「自己」打印出来了,但这不算 quine——它只是在读文件,跟「程序在自己内部重建出自己」完全是两回事。真正的 quine 必须闭着眼睛、只靠代码本身完成自我复制。
最小示例(Python)
s = 's = %r\nprint(s %% s)'
print(s % s)就两行。运行它,输出恰好是这两行本身。
它的核心思路是所有 quine 的通用套路:把一份文本同时当「数据」和「代码」用两次。拆开看:
- 变量
s存的是整个程序的「模板」,里面留了一个洞%r; print(s % s)把s自己填进这个洞。%r会调用 Python 的repr(),输出带引号、带转义的字符串字面量——所以填进去的正好是源码第一行里s =后面那一串,引号和\n都原样保留;- 模板里的
%%转义成一个%,还原出第二行的print(s % s)。
于是输出的第一行是「s 的定义」,第二行是「打印语句」,合起来就是源码本身。%r 是这里的灵魂:它解决了「字符串里如何表示包含引号的自己」这个死结。
JavaScript 版
同样的思路,用 JSON.stringify 顶替 repr() 的角色:
s = "s = %s\nconsole.log(s.replace('%s', JSON.stringify(s)))"
console.log(s.replace('%s', JSON.stringify(s)))JS 还有一条捷径——函数的 toString() 会返回它自己的源码:
(function f(){console.log('('+f+')()')})()一行搞定。不过这个属于「借了语言反射能力的巧劲」:自我表示的活儿是引擎替你干的,纯度上比上面的模板版差点意思,但作为炫技素材它确实合法且好记。
名字由来:一个悖论的致敬
「Quine」这个名字是侯世达(Douglas Hofstadter)在 1979 年的《哥德尔、艾舍尔、巴赫》(GEB)里起的,致敬逻辑学家威拉德·范·奥曼·蒯因(Willard Van Orman Quine),因为蒯因深入研究过间接自指的结构。他最有名的构造是「蒯因悖论」:
“Yields falsehood when preceded by its quotation” yields falsehood when preceded by its quotation.
(「其前面加上它自身的引用后为假」其前面加上它自身的引用后为假。)
这个句子妙在不用「本句」这类直接自指的词,却精确谈论了自己,制造出和说谎者悖论一样的效果——为真则假、为假则真。对照上面的 Python 代码你会发现结构完全同构:引号里那段是「数据」(被引用的短语 / 字符串 s),引号外那段是「操作」(把引用接到前面 / print(s % s)),数据和操作拼起来恰好是整个句子(程序)本身。quine 程序就是蒯因悖论的可执行版。
顺带一提,这也不只是文字游戏:任何图灵完备且能输出字符串的语言里都必然写得出 quine,这是可计算性理论中克莱尼(Kleene)递归定理的直接推论。哥德尔不完备定理证明里那个「谈论自身的公式」,用的也是同一招。
变体:越玩越离谱
基本款 quine 只是入场券,真正的行为艺术在变体里。
抗辐射 quine(radiation-hardened quine)
从源码里随便删掉一个字符,程序依然能运行,并且输出的是删除前的完整原版源码——相当于程序自带纠错,被「辐射」打坏一个字节还能自愈。最有名的实现是 Yusuke Endoh(mame)用 Ruby 写的 radiation-hardened-quine,代码本体是一坨看起来像乱码的字符画,任删一字符照样吐出原文。
多语言 quine 与 multiquine
- 多语言 quine(polyglot quine):同一份源文件在多种语言里都是合法程序,且在每种语言下运行都输出这份源文件本身。难点在于要同时骗过几套完全不同的语法。
- Multiquine:一组分别用不同语言写的程序,互相「认识」——根据命令行参数,每个程序都能打印出这组里任意一个的源码。
Quine 接龙 / 衔尾蛇程序(ouroboros program)
程序 A 输出程序 B 的源码,B 又输出 A,循环成环。这个方向的天花板还是 mame 的 quine-relay:一个 Ruby 程序,生成一个 Rust 程序,Rust 生成 Scala,Scala 生成 Scheme……穿过整整 128 门语言之后,最后一个 REXX 程序输出的恰好是最初那份 Ruby 源码,首尾咬合,像一条衔尾蛇。仓库里那张 128 门语言围成一圈的图值得亲眼看一次。
结语
quine 迷人的地方在于,它用最少的代码演示了一个大问题:一个系统如何在自己内部完整地描述自己。从蒯因的悖论句,到哥德尔的自指公式,再到两行 Python——是同一个思想在不同介质里的回声。生物学里 DNA 既是「数据」(被复制的序列)又是「代码」(指导复制机器的指令),和 quine 的双重身份如出一辙——冯·诺依曼在 DNA 结构发现之前研究自复制自动机时,就已经推出了这种「描述 + 构造器」的必然结构。
想入坑的话,从改造那两行 Python 开始:试着让它输出两遍自己,或者给它加一行注释再让它恢复自指——你会立刻体会到「牵一发而动全身」是什么意思。
评论
评论加载中……