iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 4
0
Software Development

每天 Racket 3 分鐘系列 第 10

(let ([day 9]) (display "Let it be! — Racket 的 Local Binding"))

  • 分享至 

  • xImage
  •  

1. 關起門來寫程式

在 Racket 裡頭,有種特殊的函式,可以讓你劃定一個區塊,執行完之後就煙消雲散。這是什麼呢?其實不只在 Racket,早在 Lisp,也有這樣的語法,這就是 let 的 local binding 函式:

(let ([a 10]
      [b 20])
  (* a b))

它的參數結構與我們昨天在 for 函式上所見極為類似,而這個宣告它本質上,其實是類似這樣的一個 lambda 宣告:

((lambda (a b)
   (* a b)) 10 20)

換句話說,宣告一個 lambda 與它的參數及內容,並且馬上傳遞參數值進去。因此,你想想,在這個情況下,let 宣告的 ab 彼此互相認識嗎?例如,可以這樣嗎?

(let ([a 10]
      [b (+ a 5)])
  (* a b))

如果不行,怎麼辦呢?

2. 左右互搏之術

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,並且這層傳參數給 blambda 時,是一個表示式,因此要再找 a 的值。最後在最外層裡頭,找到了 a 的值為 10,於是進來,求 (+ a 5) 的值,最後求 (* a b) 的值。

雖然看起來很囉唆,可這是非常有用的東西,而且我們要進入更複雜的宣告函式了!

3. 纏纏繞繞纏纏繞

在 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))

4. 魔法般的語法之一

let 的變形之多,各位可以看 Racket Reference [1],而讓我覺得最奇妙的,就是 let 可以有如 lambdafor 一樣,進行 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],會有很簡單扼要的說明。


上一篇
(cond ((not (day-08?)) 'all-right) (else "我若不在寫 Racket,就在去寫 Racket 的路上 — Racket 的控制結構"))
下一篇
(lambda (day-10?) (if (day-10?) "媽!我的程式跑不停!— 遞迴與尾遞迴 (tail-recursion)" (void)))
系列文
每天 Racket 3 分鐘17
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言