在 Racket 裡頭,有種特殊的函式,可以讓你劃定一個區塊,執行完之後就煙消雲散。這是什麼呢?其實不只在 Racket,早在 Lisp,也有這樣的語法,這就是 let
的 local binding 函式:
(let ([a 10]
[b 20])
(* a b))
它的參數結構與我們昨天在 for
函式上所見極為類似,而這個宣告它本質上,其實是類似這樣的一個 lambda
宣告:
((lambda (a b)
(* a b)) 10 20)
換句話說,宣告一個 lambda
與它的參數及內容,並且馬上傳遞參數值進去。因此,你想想,在這個情況下,let
宣告的 a
與 b
彼此互相認識嗎?例如,可以這樣嗎?
(let ([a 10]
[b (+ a 5)])
(* a b))
如果不行,怎麼辦呢?
let
有許多變形,我們會試著介紹最重要的四種宣告,第二種,就是讓你的區域變數可以互相認識:
(let* ([a 10]
[b (+ a 5)])
(* a b))
這裡所用的,是指後來宣告的變數可以知道前面的變數值。如果你倒過來寫,結果會不一樣:
(let* ([a (+ b 5)]
[b 10])
(* a b))
這時 Racket 會爆出錯誤給你,告訴你 a
不認識 b
,為何會這樣呢?其實整個 let
,都是 lambda
的語法糖,這個拆開來後,會是一個 currying 的 lambda
:
((lambda (a)
((lambda (b)
(* a b)) (+ a 5))) 10)
這個結構看起來很複雜,其實我們可以先從最內層來看:(lambda (b) (* a b))
,給定一個 lambda
,它只有一個變數 b
,但是它的 body 裡頭有個 a
,這稱為自由變數,因此它會往上找,往上找那層:(lambda (a) ...)
時,發現一個 a
,並且這層傳參數給 b
的 lambda
時,是一個表示式,因此要再找 a
的值。最後在最外層裡頭,找到了 a
的值為 10
,於是進來,求 (+ a 5)
的值,最後求 (* a b)
的值。
雖然看起來很囉唆,可這是非常有用的東西,而且我們要進入更複雜的宣告函式了!
在 FP 裡頭,常見用遞迴方式來求解,例如我們昨天說到的費氏數列的例子,然而 let
的區塊裡,能不能宣告遞迴函式來用呢?我們借用昨天的 fib
:
(letrec ([fib (lambda (n)
(cond ((= n 0) 0)
((= n 1) 1)
(else (+ (fib (- n 1)) (fib (- n 2))))))]
[fact (lambda (n)
(if (= n 0)
1
(* n (fact (- n 1)))))]
[n 10])
(+ (fib n) (fact n)))
這回使用的 letrec
可以讓你定義內部遞迴的函式,甚至可以像 let*
一樣,後面宣告的內容能夠連結到前面所宣告的:
(letrec ([fib (lambda (n)
(cond ((= n 0) 0)
((= n 1) 1)
(else (+ (fib (- n 1)) (fib (- n 2))))))]
[busy-fact (lambda (n)
(if (= n 0)
1
(* (fib n) (busy-fact (- n 1)))))]
[n 10])
(busy-fact n))
let
的變形之多,各位可以看 Racket Reference [1],而讓我覺得最奇妙的,就是 let
可以有如 lambda
或 for
一樣,進行 iteration 的操作:
(let iter ([count 10])
(if (= count 0)
'()
(cons count (iter (- count 1)))))
這結果不意外,是 '(10 9 8 7 6 5 4 3 2 1)
,至於為什麼呢?我們剛剛已經說到 let
其實可以藉由 lambda
的機制進行實作,因此可以想像,這個 iter
可以說是這一段 let
expression 的別名,像是:
(define iter
(lambda (count)
(if (= count 0)
'()
(cons count (iter (- count 1))))))
這樣的一個定義宣告,並且把 10
當作參數傳入求解一般。
let
仍有許多奇妙的用法,Racket 因其語法簡單,因而發展出各樣不同作用的函式,這是這個生態系最大的特色,同理也可用在 Lisp、CommonLisp、Scheme。若是 Racket Reference 內容太長,也可以參考 Racket Guide [2],會有很簡單扼要的說明。