1 入門
1.1 何謂程式(program)?
程式就是一串說明如何執行運算的指令,那麼無可避面的我們會有進一步的問題:何謂運算?
1.2 何謂運算(computation)?
運算可以是各種各樣的東西,1+1 是運算嗎?是的,1+1 當然是一種運算,並且我們知道它的結果是 2。事實上,判斷也是一種運算,1+1 = 2 的等於生成真(true)或假(false)作為結果。根據長年來學習數學的經驗,我相信大多數人可以回答 2+3 = 5 為真。但為什麼 2+3=5 呢?因為我們約定好了 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 等「符號」組成的東西叫做「數字」,且說好了 + 這個符號應該做出什麼樣的「運算」。換句話說,雖然運算中間發生了什麼我們不關心,但我們可以通過約定好的「運算規則」知道其結果。
根據我們選定的抽象層面,我們也有可能可以關注到本來無法觀察的運算過程。舉個例子,方才我們描述的東西叫做自然數加法,皮亞諾公理(Peano axioms)可以幫助我們觀察某方面的真實。
皮亞諾公理規定了自然數可以由兩個方式得到
0 是自然數
如果 n 是自然數,則 succ(n) 是自然數(succ 也經常寫成 s 或 suc,是 successor 的縮寫)
且
succ n = succ(m) 當且僅當 n = m
對任何 n 來說 succ(n) 都不可能為 0
剩下的 axioms 是關於 equality 的
reflexive
symmetric
transitive
closed under equality
上面的形式化裡面可以只讀懂前兩條就好,剩下的公理都是為了保證這跟我們直覺理解到的自然數真的是同一個東西而定義的,對接下來要說明的事物沒有關係。自然數常見的爭論是 0 算不算自然數,不過為了 0+n = n 的特性(稱為 additive identity),我們選擇了 0 是自然數的這邊。歸納公理被省略,不過你可以認為這條公理的重點是為了說明我們在討論合法的函數(對所有輸入皆有輸出,又稱 totality)。
於是我們可以開始討論為何這些允許我們更仔細的觀察加法,我們把加法定義為
n + 0 = n
n + succ(m) = succ(n + m)
這是遞迴定義,但我們知道無論 m 為何,最終運算都會結束(因為 m 會歸零)。不過我們可以觀察到此運算的性質為「加法的結果是兩數中其中一個的減一加另一數再加一」或「兩數之一為零則結果為另一數」。
作為練習,你可以試著手動一步步展開 succ(succ(0)) + succ(succ(0)) 並檢查結果是否是 succ(succ(succ(succ(0))))。
這是個重要的觀察,重點不是加法的另一種定義方式,而是通過轉換觀點可以用各種角度觀察運算的不同「真實」。而這些取決於我們要站在什麼地方理解他們,因此我們也可以說「交易」是一種運算,由可以收錢跟可以付錢的客體完成,其重點是付款金額與收款金額應該相同,我們可以基於剛才的數字加減法完成金額的計算。或是選擇某金額的所有權由付款方移到收款方這樣的方式定義我們的運算。第二種方式看似有點多餘,但其實引入了我們可以在未來追蹤這筆交易的機會,種種計算方式往往沒有優劣之分。重點在衡量什麼是「合適」的層級,這往往需要知道我們要完成什麼以及我們有哪些限制需要遵守,並不僅僅只是運算問題。
現在讓我們面對抽象是什麼這個問題。
1.3 何謂抽象(abstraction)?
抽象是我們整天都在做的事情,例如我現在不擔心我現在使用的編輯軟體為什麼可以動,因為我的目的是要把文章寫出來(除非我的編輯軟體一直當掉)。而我們也不需要擔心為什麼超市買得到食物(除非買不到了)等等,因為我們注意力有限,因而一次只能注意少數事務來進行工作,把關心的部分拉出來的行為就是抽象。
1.4 寫程式這份工作
在說明運算時,我說問題有時不只是運算,這是因為程式涉及到解決問題,因此我們需要
系統化地理解/闡述問題
創意思考(像是在下雪的日子裡穿拖鞋騎腳踏車上山)
正確的表達解法
,偶爾我們也需要進行一些科學實驗:
觀察複雜系統
建立假說
預測行為並驗證
這些是我們設計程式的種種過程與不同面向,經常涉及到關於人的問題,例如老闆覺得重寫系統不夠好玩,怎麼不順便放點新功能呢?不幸的是即便現實世界無比複雜我們還是得找出方式解決問題,萬幸的是即便解決不了問題,我們通常也能通過解決人來收尾(誤)。
在瞎扯這麼多之後,我們可以來看程式到底應該有些什麼。
1.5 程式
很多人誤以為寫程式很難,或是「寫」程式非常重要,這些其實都是錯誤的觀念。不過事實上,「寫」程式是非常簡單的,只要我們不求這些程式可以解決我們關心的問題的話,例如下面這段程式(看不懂也沒有關係,之後會再做介紹)
#lang racket/base (displayln "hello, world")
隨便建立一個檔案,像是 a.rkt 把程式貼進去,用 racket a.rkt 執行就可以看到印出了 hello, world 這串字。而接下來也是程式
#lang racket/base (displayln "hello, world") (displayln "hello, world") (displayln "hello, world")
事實上我們要重複幾行 (displayln "hello, world") 都無所謂,這些都是程式,只是沒有解決我們可能關心的問題而已。我們關心的是問題有沒有被解決,寫程式只是其中一種辦法而不是最重要的部分。而程式具備的重要特性之一,不單是可以執行,而是可以被閱讀。一個方法可以通過被閱讀理解後傳承,稍作修改就能解決新的問題,或是在這之上解決更複雜的問題,不需要反覆手動執行一次流程,才是程式的價值所在。
既然程式可以被書寫閱讀(雖然是通過鍵盤與螢幕),那麼它就需要有符號、文法與語義,各種語言的細節常常在此愚弄初來乍到者,誤以為程式語言俱是龐然大物,然而,程式語言該有的核心其實亦常簡單:
輸入輸出
簡單數學運算
條件執行
重複
輸入輸出是很容易理解的,無論如何最終程式都是為了人們製作的,所以會有各式各樣的輸入輸出裝置存在,例如鍵盤、滑鼠、VR 眼鏡、螢幕、開關等等。數學運算很不幸的沒有好的解釋,不過簡單數學在很多問題裡面都會用到,或許是個理解它為何存在的方向。條件執行是因為我們經常需要根據狀況做出不同的行為,例如掃地機器人撞到東西時會轉向等。重複執行的使用方式變化多端,不過大抵上來說是因為我們希望可以描述「一直做到完成」這類型的抽象,否則我們就得苦哈哈的坐在那邊不斷重新執行程式直到滿足某個條件為止。
現在你應該已經對程式能夠與不能夠完成的事情有了一點概念,也了解到運算跟抽象定義的威力,然而我們終究還是要學習一門實際的語言來進行寫程式這個行為,因此接下來轉入介紹 Racket 這個程式語言。