Lv的杂货铺

我们都是阴沟里的虫子,但总还是得有人仰望星空

0%

递归定理与自打印程序

一台打印自己描述的图灵机


首先考虑这样一个问题:是否存在一台图灵机,它运行时会将它自己的描述打印到纸带上面,然后停机(可能以后会写关于图灵机编码的内容,不过现在就算不知道,也不影响~)。

如果从常规思路考虑的话,肯定包含的一个打印的逻辑,打印(它自己),而这个它自己也需要包含那一个打印的逻辑,也就是“打印(它自己)”,层层套娃,它会是这样的: 打印(XXXXX 打印 (XXX打印 (XXX打印(X……….XX)XXXXXX) XXX),显然这样是行不通的,我们需要换个角度考虑问题。

既然一步直接取得程序本身的描述似乎有些难,那不如将这个想要得到的自打印图灵机分成两部分:AB,图灵机的 A 部分打印 B 部分的描述,然后 B 部分打印出 A 部分的描述。看起来还是个死循环:A 依赖于 B 的描述,而 B 则需要 A 的描述,不过还是有办法的。

首先定义一个函数:
q:ΣΣ
也就是,输入任意串 ωq 输出一个图灵机的描述,这个图灵机它在纸带上写下 ω,然后停机。这个函数所做的事情非常简单,而且你可以使用一台图灵机模拟它。

回到刚才的问题,如果我们有了 B 的描述,那么就可以很轻松地定义 A 了:Print( B 的描述),很简单。

接着考虑 B,在 A 运行完之后,纸带上面留下了一个字符串,它就是 B 的描述,简单起见用<B>来表示。那么,当 B 看到了停留在纸带上面的一个字符串:<B>时,它把这个字符串当作函数q的输入,那么,q(<B>)就是 A 的描述了!

最后,B 把两个子部分的描述 q(<B>)<B> 组合起来,打印,然后停机,我们就得到了一个打印自己的图灵机!

递归定理


其实,上面的自打印程序只是递归定理(recursion theorem)的一个应用。递归定理的完整表述形式是:

T是计算函数 t:Σ×ΣΣ 的一个图灵机,则存在计算函数 r:ΣΣ 的一个图灵机R,使得对每一个 ω 都有 r(ω)=t(<R>,ω)

乍一看让人不明觉厉,但其实说的是,你可以构造一个图灵机 R,它获取自己的描述,然后进行还可以进行任意一些计算。例如,自打印程序的“计算”就是把它的描述打印出来。递归定理的证明也非常类似,因此就简略说下:

相应的图灵机可以这样构造:图灵机分成 ABC 三个部分,其中 A 负责将 BC 的描述,即 <B><C>打印出来,而 B 负责的工作是,根据 A 写在纸带上的内容,推导出其描述 <A>(也就是q(<B><C>)),而 C 就根据 AB 的输出内容(整个图灵机的描述)以及图灵机的输入 ω 计算即可。

事实上,递归定理赋予了任何图灵完备语言的程序引用自身的能力,或者说,实现自引用词“这个”的能力。在设计程序时,大可以在伪代码中写上“获取自身的代码,并进行blablabla的计算”而不必担心无法实现(当然了,这里讨论的是计算理论中的可能性,而不考虑实际工程学中的种种限制)

或许这样说有些不好理解,不过看完下面的实例就能明白了。

实例


根据递归定理,事实上存在着无数个这种程序,而且可以被任意一种图灵完备语言都可以构造出。网络上已经有了许许多多关于自打印程序(或者说,Quine)的例子,这些例子常常或多或少用到了其语言的一些特性,因此非常简洁;而我接下来将展示两个分别用python和C写的自打印程序,这两个例子非常不简洁:),但是它尽可能复刻了上一节递归定理的证明,并且尽我所能少的应用了具体的语言特性,所以,你可以随意在例子的后面添加你需要的程序,比如,上一节证明中的 C ,然后在 A 部分稍作修改,得到你想要的获取自身源代码并进行一些计算(例如,统计其中的单词数或者别的一些什么玩意儿)的程序。

python实例

首先是python中的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
t = "tmp = chr(116) + chr(32) + chr(61) + chr(32) + chr(34)\nlst = t.split(chr(10))\nfor i in lst:\n    tmp = tmp + i\n    tmp = tmp + chr(92) + chr(110)\ntmp = tmp[:-2]\ntmp = tmp + chr(34)\nprint(tmp)\nprint(t, end=chr(10))\n\nWE_CAN_DO_OTHER_CACULATE1=1\nWE_CAN_DO_OTHER_CACULATE2=2"
tmp = chr(116) + chr(32) + chr(61) + chr(32) + chr(34)
lst = t.split(chr(10))
for i in lst:
tmp = tmp + i
tmp = tmp + chr(92) + chr(110)
tmp = tmp[:-2]
tmp = tmp + chr(34)
print(tmp)
print(t, end=chr(10))

WE_CAN_DO_OTHER_CACULATE1=1
WE_CAN_DO_OTHER_CACULATE2=2

运行一下:

img

可以看到,使用diff命令比较程序输出与程序源代码没有不同之处。

其中,第一行的赋值语句t=XXXX就是前面例子中的 A 了,它把 BC 的描述(源代码)<B><C> “打印”出来,然后是 B,它所做的是看“纸带”上 A 打印出来的内容(读取变量t),然后推断出 A 的描述,也就是计算 q(<B><C>),就是变量tmp,最后把两部分的内容,即整个程序的源代码交给 C 进行一些“计算”。

值得注意的是, A 的内容是一整行,因此特别长;此外, BC 的部分(或者说第一行往下的部分),不能有双引号“的出现,因为如果下边出现了,那么第一行的字符串中就必须要有"的转义符的出现,否则解释器就要报错了,然而如果上面有转义符了,跟下面就不一样了,就不能正确打印 BC 的描述了,而要进行修改的话,逻辑就会更加复杂,所以为了简单起见直接 BC 的部分的所有双引号都用chr(34)代替。还有一点小技巧,就是,python中print默认最后会添加一个换行符,常规情况下去掉它需要用end=""来明确指出不需要换行符,既然不能使用双引号,那就造一个一字节的字符串,然后切掉它的第一个字符,就是一个空字符串了(我也想过直接用None,但还是不行)

或许不用我说就能明白,函数 q() 对应的就是第一行之下,print语句之上的内容,它查看“纸带” 上 A 打印出来的内容(变量t的内容),然后将它还原成原本的那条赋值语句t = XXXXXX,只不过是在不使用双引号的条件下完成的,所以略显麻烦。

如果想要进行别的一些计算的话,把最下面的WE_CAN_DO_OTHER_CACULATE两句换成想要计算的程序,然后把除第一行的内容外的代码全部复制,替换掉原第一行右边的内容,然后添加些换行符即可。

C实例

C语言的实例也是基本遵照递归定理证明来实行的,因此特别长,这里就不放出来了,直接看这个链接就好了。道理也是一样的,所以也完全可以添加自己的代码,然后把char t[4000] = XXXX那一行一下的内容全部复制,然后替换那一行右边的赋值就行啦!

需要注意的是,t,tmp,cp 三个数组的大小只有4000,如果添加的代码太多的话空间会不够,手动调到需要的大小就行了,另外,如果需要非常大空间的话,使用malloc()来动态分配也是完全可以的,就是会稍微麻烦一点(也不太麻烦),不过我懒,就没有采用动态分配233333

看一下这个的运行结果:

img

可以看到,使用diff比较,输出的内容与源代码还是一致的。

(完)