最近讀到馮·諾依曼的《Theory of Self-Reproducing Automata》的中譯本,被自複製自動機理論深深吸引了!生命是什麼?這本書讓我對生命有了新的認識。
熱力學第二定律是宇宙的死亡法則:系統的熵總是趨於增加,系統總是由有序趨向無序,由有形趨向混沌,最後終結於熱寂。宇宙萬物都逃不過這條法則,唯有生命例外。通過與外界交換能量,生命可以保持內在的有序。只有生命可以違抗熱力學第二定律。
熱力學與資訊理論有著內在聯絡:一個系統越有序,它包含的資訊就越多;一個系統越無序,它包含的資訊就越少。資訊總是沿著減少的方向流動。只有生命可以抗拒這個規律:生命可以“創造”出資訊。“創造”是生命之所以為生命的本質。
生命的另一個神奇之處是,他們自己包含了自己的全部資訊,他們自己可以產生一個跟自己一樣的新生命。唯有生命可以做到自己創造自己。
馮·諾依曼認為,一個簡單的系統是無法抗拒熱力學第二定律的,這樣的系統只會越來越趨向於無序,能量會向更低的方向流動,資訊會越來越少。只有當系統複雜到某一個程度的時候,不妨設為臨界點 C ,一旦越過了這個臨界點,系統可以自己創造自己,這樣系統就可以逆熱力學第二定律而上,並且變得越來越複雜。
這裡的臨界點 C 跟不少人提出的“技術奇異點”是一樣的:人們認為,當人工智慧達到了這樣一種程度,即它們可以自己創造自己的時候,這就是人工智慧真正覺醒的時候。“奇異點”就是機器可以自己造出自己的時候,“奇異點”之後,機器將進化,並且變得越來越複雜,超出人類所能理解的範圍,它們在某種程度上已經具備了生命的特質,那將是人工智慧的時代。
總而言之,生命之所以區別於世界萬物,就在於生命可以包含自己的全部資訊。所以下面就是我們的問題了:
問題(自產生程式):編寫一個程式,不讀取任何輸入,只把自己的原始碼輸出。
這個問題是個非常本質的問題,跟使用什麼程式語言無關(不要想到使用反射之類的東西)。
試想,如果要輸出自己的原始碼,那麼,顯然,程式中應該有“print …”語句。但 print 什麼出來呢?如果硬要寫的話就會變成:
1 |
print "print \"print ......\"" |
最後是一個無限迴圈。
一般地,我們知道,如果程式 A 能產生程式 B ,那麼 A 必須包含 B 的全部資訊,而且應該比 B 的資訊還多,因為還要包含額外的列印語句。也就是說,一般情況下,資訊是減少的。而這個自產生程式,自己要包含自己的全部資訊,從某種程度上已經具有生命的意味了。
下面列出一些自產生程式及其思路。
需要注意的是,使用程式語言本身的反射功能或者讀取檔案等做法都被視為 cheating ,比如這樣的 bash 指令碼:
1 2 |
#!/bin/sh cat $0 |
或者像這樣的 javascript :
1 |
function a() { console.log(a.toString(), "a()"); } a() |
因為這些程式沒有體現出自產生程式的遞迴和自指特性,或者結果嚴重依賴於程式語言的具體實現。
輸出原始碼在該語言中的轉義
Python :
1 2 |
s = "'s = ' + repr(s) + '\\nprint(' + s + ')'" print('s = ' + repr(s) + '\nprint(' + s + ')') |
Lua 5.1 :
1 2 |
s = "string.format('s = %q\\nprint(%s)', s, s)" print(string.format('s = %q\nprint(%s)', s, s)) |
另一個 Lua 版:
1 2 3 |
s = "s = %q\ print(string.format(s, s))" print(string.format(s, s)) |
Scala :
1 2 3 |
def e(s: String) = ("\"" + s.replace("\\", "\\\\").replace("\"", "\\\"") + "\"") val s = "\"\"\"def e(s: String) = (\"\\\"\" + s.replace(\"\\\\\", \"\\\\\\\\\").replace(\"\\\"\", \"\\\\\\\"\") + \"\\\"\")\"\"\" + \"\\nval s = \" + e(s) + \"\\nprintln(\" + s + \")\"" println("""def e(s: String) = ("\"" + s.replace("\\", "\\\\").replace("\"", "\\\"") + "\"")""" + "\nval s = " + e(s) + "\nprintln(" + s + ")") |
用某種方法 encode 原始碼,使之不包含引號,然後還原出原始碼
Bash :
1 2 3 4 5 |
#!/bin/sh s='\x22#!/bin/sh\ns=\x27$s\x27\necho $(echo -e $s)\x22' echo "#!/bin/sh s='$s' echo $(echo -e $s)" |
Lua 5.2 使用 load():
1 2 |
s = "a,q,b=string.char(39),string.char(34),string.char(92) return a..'s = '..q..a..'..s..'..a..q..b..'nprint('..a..'..load(s)()..'..a..')'..a" print('s = "'..s..'"\nprint('..load(s)()..')') |
Scala :
1 2 |
val s = "%22val+s+%3D+%5C%22%22+%2B+s+%2B+%22%5C%22%5Cnprintln%28%22+%2B+java.net.URLDecoder.decode%28s%2C+%22UTF-8%22%29+%2B+%22%29%22" println("val s = \"" + s + "\"\nprintln(" + java.net.URLDecoder.decode(s, "UTF-8") + ")") |
使用 eval :在 eval 的字串中引用自己
Lua load() 的另一種用法:
1 |
s = "print(string.format('s = %q load(s)()', s))" load(s)() |
js 的 eval():
1 |
s = "q = String.fromCharCode(34); console.log('s = ' + q + s + q + '; eval(s)')"; eval(s) |
使用語言中的更強的轉義機制
類似上面的第二種,但不用引號。
Lua 的 long string :
1 2 |
x = [["x = [".."["..x.."]".."]\nprint("..x..")"]] print("x = [".."["..x.."]".."]\nprint("..x..")") |
Scala 的三引號:
1 2 |
val s = """"val s = \"\"\"" + s + "\"\"\"\nprintln(" + s + ")"""" println("val s = \"\"\"" + s + "\"\"\"\nprintln(" + s + ")") |
使用 C 的巨集
先執行傳入的引數,再把引數變成字串。
gcc :
1 2 |
#define p(a) int main(){a;puts("p("#a")");return 0;} p(puts("#define p(a) int main(){a;puts(\"p(\"#a\")\");return 0;}")) |
至於它們是怎麼實現的,就留給讀者自己琢磨了。自產生程式也稱為 Quine ,可以參考 Quine Page 。