度過了兩天沒有程式碼的內容,從這一篇開始,我們寫下人生第一個 s 表示式。
我們先叫出 DrRacket,然後在上方的程式碼編輯區寫一些簡單的東西:
1 + 1
然後在右上方按下運行(Run)。有發現什麼嗎?下方的 REPL 出現了以下訊息:
1
#<procedure:+>
1
奇怪了,1 + 1
不是等於 2 嗎?
要解釋這件事之前,我們先解釋一下,程式怎麼被解讀的。
假設我們剛剛的 1 + 1
要 assign 給一個變數 a:
a = 1 + 1
然而,不管或大或小的程式,進到執行階段,都必須被轉變成為一種特殊的結構,它看起來像是一棵樹,像這樣子:
有的人的畫法是把操作動作放在另一側,像這樣:
再透過尋訪語法樹的過程,將值求出。因此,我們將上述的算式用小括號括起來:
(a = (1 + 1))
然後,這步是重點:將操作子搬到前面:
(= a (+ 1 1))
最後,因為等號在早期的語言是一種比較兩個值的操作,因此把 =
換成 define
如下:
(define a (+ 1 1))
按下運行前,先再加一行:
(define a (+ 1 1))
a ;; 加上這一行
這時你會看到下方 REPL 出現了正確的結果。
而每一種語言,都有它特殊的語法,例如 Perl 裡頭,正規表示式可以當作一般語法使用,或在 Ruby,任何一個元素都是物件,因此數字可以直接用 .
來呼叫物件內部的 method。甚至,PostgreSQL 的字串相連,使用 ||
,但在 Haskell,卻是使用 ++
。
但是,在 s 表示式裡,沒有這種東西!因為 s 表示式的語法,直接地對應到程式的語法樹,並且所有的操作全部都放在第一個位置,後面接的是參數或定義內容。因此,在閱讀這類語言時,你起初會覺得很痛苦,為什麼東西包了一層又一層,在寫的時候,會覺得很繞舌,為什麼每一個操作都要有相對應的 function。但是,一旦習慣以後,就像學會九陽神功,打通任督二脈的張無忌,你便能快速掌握每一個語言它語法與操作的背後對應到的思想。從語法、語義,到函式的各種轉換,再加上每種語言都有其特定的慣例用法,稍微了解後,就能夠快速轉換到其他語言。
Lisp 家族衍生出來的 s 表示式語言都有著這樣的特性:語法中沒有魔法,沒有約定俗成,沒有優先序。
稍微介紹完 s 表示式與程式語言的關係後,我們來介紹 define 的作用。Racket 要使用任何變數,一定要先宣告,而宣告的關鍵字,就是 define
,如下:
(define a "a")
define
的第一個參數,就是變數名稱,這是一個固定的用法。然而,在正規的程式語言詞彙裡,不叫變數名稱,叫識別子: identifier ,我們以下簡稱 id 。第二個參數,就是要賦予前面 id 的值。這個值可以是數字、字串、boolean 值,或者是函式。函式部份,我們後續會說明。
此外,Racket 是一個支援多回傳值的語言,這是我在 Java 寫這麼久以來,最想要的語法特性。回傳值目前還沒說到,但我們可以用這個機制,一次宣告多個 id。這回,是使用 define 的擴充形式:
(define-values (a b c) (values 1 2 3))
(+ a b c)
使用 define-values
,後面接 values
,可以一次 assign 多個 id 的值。但在此必須注意,在 s 表示式語言裡,()
所含括起來的範圍,稱為 form ,每一個 form 所使用的 ()
不能多,也不能少。例如不能這樣寫:
(define-values a b c (values (1 2 3)))
有個 C 語言的笑話說,當你的 C 程式無法執行時,在出錯的地方加上 *
或拿掉 *
,或用 &
,或許它就能執行了。
Lisp 家族的語言也是這樣,當你的 Racket 程式無法執行時,在出錯的地方加上 )
或拿掉 )
,或許它就能執行了。
DrRacket 下方所附的 REPL 很特別,跟你若用 Racket command line 工具所用的 REPL 不太一樣。最大的差別在於,DrRacket 的 REPL 可以讓你即時與上方寫的程式互動,但若這程式用 Racket 的 REPL 來開啟,就沒辦法這樣輕易地可以呼叫每個內部的元素。這是 DrRacket 的特點,這 REPL 會是你接下來使用這個語言的好朋友。例如當你上方已經這樣寫:
(define a (+ 1 1))
按下運行後,在下方 REPL 可以輸入:
(+ a 123)
可以直接得到:
125
如果需要即時驗證自己寫的程式,這是一個很方便的機制!