開放出版:圖靈機——樊虹劍《Go語言·雲動力》樣章

暘谷發表於2012-03-23

本樣章為未經編輯的作者原稿。

作者自述

樊虹劍。15歲迷上Apple II。立志成為程式設計師。從Applesoft BASIC,到Microsoft C、C++、C#,又到Apple Objective-C,其間使用過大小十幾種程式語言,始終不得要領。不明白髮源於數學和電子學的電腦科學,為何不去追求詩歌般至簡至純的美學,而是纏繞於繁複的形式和糾結於空洞的哲學。幾欲放棄。直到偶然發現Plan 9,才知道返璞歸真的樂趣,並一路歡喜走過Inferno,快樂走入Go語言的世界。

圖靈機

Go作為高階語言,當然是圖靈完備的。我們用Go來寫一個最簡單的圖靈機,使用腦操程式語言,下面的程式可以輸出“hi”:

++++++++++[>++++++++++<-]>++++.+.

此程式語言僅有7條指令,理論上和任何圖靈完備的語言等價。但程式設計師使用什麼語言的表達能力和效率,是有云泥之分的。這也是為什麼人們總是在探索新的語言,提高表達效率。

這7條指令是:
+ 使當前資料單元的值增一
- 使當前資料單元的值減一
> 下一個單元作為當前資料單元
< 上一個單元作為當前資料單元
[ 如果當前資料單元的值為0, 下一指令在對應的]後
] 如果當前資料單元的值不為0, 下一指令在對應的[後
. 把當前資料單元的值作為字元輸出

這樣,當前單元的值加10,作為[和]的迴圈變數,>到下一單元,也加10,<-使迴圈變數減一,迴圈10遍,再加4,得到104,是字元h的UTF8編碼,輸出,再加1,輸出i。

package main

import "fmt"

var (
    a     [30000]byte
    prog  = "++++++++++[>++++++++++<-]>++++.+."
    p, pc int
)
func loop(inc int) {
    for i := inc; i != 0; pc += inc {
        switch prog[pc+inc] {
        case '[':
            i++
        case ']':
            i--
        }
    }
}
func main() {
    for {
        switch prog[pc] {
        case '>':
            p++
        case '<':
            p--
        case '+':
            a[p]++
        case '-':
            a[p]--
        case '.':
            fmt.Print(string(a[p]))
        case '[':
            if a[p] == 0 {
                loop(1)
            }
        case ']':
            if a[p] != 0 {
                loop(-1)
            }
        default:
            fmt.Println("Illegal instruction")
        }
        pc++
        if pc == len(prog) {
            return
        }
    }
}

程式一開始的變數a是我們圖靈機的資料記憶體,prog是指令記憶體,p和pc分別是這兩個記憶體的指標,代表當前資料單元和當前指令。

函式loop執行[和]指令,移動指令指標pc。這裡用到了三段式的for語句,也就是:

for 初始;判斷;增值 {

初始在迴圈開始前執行一次,通常是給迴圈控制變數一個初始值,然後每次判斷如果真,就執行大括號的語句塊一次,再執行增值的部分,再判斷、執行、增值,直到判斷為假,才跳過大括號的塊,繼續其後的語句。

switch是Go的單項選擇語句。它根據後面的值,選擇執行大括號裡的某一個case分支。同樣的for和switch語句,也出現在main函式裡。但那裡的for,沒有三段式,所以會一直迴圈,直到return結束。而那裡的switch,有一個default分支,當其它的case都不是switch的值時,選擇執行default分支。

此程式還用到了++和--語句,給變數加一和減一。例如p++就是p = p + 1,也可以寫為p += 1。這裡,函式loop的for語句的第三段增值部分的pc += inc,就是pc = pc + inc的縮寫。

注意++和--是單獨的語句,不是表示式,不可以用在其它語句裡。象*p++=*q++這種高明的C語句,在Go裡是不能用的。目的是避免語義誤導,給程式設計師少一點犯錯的機會。

而這種加減一的操作,主要用來移動陣列的下標。例如,p和pc分別是陣列a和prog的下標,這樣a[p]和prog[pc]就分別是對應下標的陣列的單元的值。

至此,我們應該可以理解此程式了。作為練習,請讀者拿出紙筆,畫一條33個格子的帶子,逐一填上prog的每個字元,這就是prog陣列,也是圖靈機的指令記憶體。再畫一條至少兩個格子的帶子,作為陣列a,也就是圖靈機的資料記憶體,然後,執行每一條指令,看資料格子是怎樣更新的。我們明白了圖靈機器,也就明白了計算機器的理論基礎。

在某些人看來,物理的宇宙也是如此執行的。一切皆是宿命、神旨、歷史規律。但還有人認為,在這個無限的宇宙裡,沒有不可能,再小的機率也一定會發生,一切都不是確定的,所以機械的圖靈機理論不適用。更何況,作為物理宇宙的取樣觀察者,人類太渺小,取樣太少,是不可能正確解讀物理宇宙的資訊的。人,還是應該先觀察明白自己身邊的小事情,看怎樣寫的指令才能體現自己和人類整體的價值,而自己又是不是改變資料的那條指令?

好了,哲學人生觀的討論對Go的程式設計毫無幫助,我們還是檢討一下都學會了哪些Go的語法:包、函式、變數、常量、賦值、型別、字串、陣列、表示式、比較邏輯算數操作符、if、else、for、switch、case、++和--。夠用了。下面我們不再用無用的例子,而是直接給出幾個完整的工具程式。

相關文章