淺談 Lisp 裡面的 Lambda

我初學 Emacs Lisp 時,一直搞不懂 Lisp 裡面的 lambda 到底是什麼東西。多年後才開竅,豁然開朗感覺真是很神奇。

以下會拿 Emacs Lisp 跟 Scheme 當例子。

匿名函數

簡單來說,Lisp 裡的 lambda 就是沒有名字的函數。精確的說,lambda 是 Lisp 中拿來表示「匿名函數」(anonymous function) 的方法

…這種說法誰聽得懂啦。那麼先來用大家在 Common Lisp / Emacs Lisp 中最熟悉的 defun 定義一個函數 double,它接受 x 一個參數,最後回傳 x * 2 的值:

1
2
3
4
5
6
7
8
9
;;;; Emacs Lisp / Common Lisp
(defun double (x)
(* x 2))

;; 來呼叫看看:
(double 7)
;; => 14

;;很簡單吧?

但如果寫過 Scheme,就會知道 Scheme 可以寫成等價的:

1
2
3
4
5
6
7
;;;; Scheme
(define (double x)
(* x 2))
;; 第二種寫法:
(define double
(lambda (x)
(* x 2)))

注意第二種寫法,他其實就是把 double 這個名稱綁到 (lambda (x) (* x 2)) 上面,所以以後我們呼叫 (double ...) 就是在呼叫 (lambda (x) (* x 2))

相較於 Scheme 的設計,Emacs Lisp 的 defun 在背後做了很多 magic,而不像 Scheme 這樣極簡主義、沒有什麼 magic 直接暴露給使用者看內部設計。其實呢…Emacs Lisp 中,defun 所做的事情跟 Scheme 的 define 本質上其實一模一樣,裡面也是一個 lambda,一樣也是把這個 lambda 綁到一個名子上:

1
2
3
4
5
6
7
8
9
10
;; 因為 defun 在 Emacs Lisp 中其實只是一個 macro,
;; 所以我們在這裡用 `macroexpand' 來展開它:
(macroexpand '(defun double (x) (* x 2)))

;; eval 過後,可以看到 defun 實際做了下面這些事情:
(defalias (quote double)
(function (lambda (x)
(* x 2))))

;; 也是一個 lambda 對吧?

呼叫 Lambda

上面我們用了 defun 或 define 給原本沒有名子的 lambda 函數取了名子 double,可以讓我們輕鬆的使用:

1
(double 7)

這種方法來呼叫實際上長成這樣的 lambda 函數:

1
(lambda (x) (* x 2))

最後回傳:

1
2
3
;; => (* x 2)
;; => (* 7 2)
14

那,我們要怎麼呼叫一個沒有名字的 lambda 函數呢?很簡單,根據 Lisp 的語法,僅僅寫一個寫 car 只會被 interpreter 解釋成「取出 car 這個變數的值」(所以身為 LISP-1 的 Scheme interpreter 會回你 #<procedure car (_)>,而身為 LISP-2 的 Emacs Lisp interpreter 則會爆炸,因為你還沒有定義 car 這個變數),但(car x) 就會變成要執行該變數。如果 car 這個變數的內容是一個 (lambda ...)表達式,就會變成執行這個 lambda。也就是說,讓你的 lambda 包上一層括弧,這個 lambda 就會被執行了

1
2
3
 (lambda (x) (* x 2))     ;; 就只是一個「沒有名字」的函數,沒有被呼叫。
((lambda (x) (* x 2)) 7) ;; 被呼叫了!得到 14
(double 7) ;; 其實上面這行跟直接呼叫 (double 7) 的意思是一樣的

看得懂嗎?GNU Emacs Lisp 文件就是這樣教你 lambda 的,但當時初學 Lisp,腦子打結完全看不懂,兩年後把才突然開竅,原來這麼簡單、又神奇。只是一眼看過去被括號混淆了視聽而造成理解障礙而已。

LISP-1 與 LISP-2?

LISP-1 是指 function 與 variable 共用同一個 namespace 的語言,所以你沒辦法讓 function 與 varianble 取同個名字。目前大部分的程式語言都是這樣設計,例如 Python、Java、JavaScript、Scheme。

LISP-2 則是 function 與 variable 各自有不同 namespace 的語言,所以你可以同時定義一個名字叫 foobar 的 function,以及一個叫 foobar 的 variable。這樣設計的語言有 Ruby、Emacs Lisp、Common Lisp、Perl。

呼叫 Lambda 的其他方式

funcallapply

因為 Emacs Lisp 是 LISP-2,不像 Scheme 那樣 LISP-1 單純,所以你沒辦法直接用 setq 來把 lambda 綁到一個變數上直接呼叫。不過還是可以用 funcallapply 來強制呼叫 variable 裡的 lambda

其實他們原本的主要用途並不是這樣啦…只是剛好這裡可以這樣做。

1
2
3
4
(setq multiply (lambda (a b) (* a b)))
(multiply 7 3) ;; => 爆炸:函數未定義 Symbol's function definition is void: multiply
(funcall multiply 7 3) ;; => 成功呼叫!得到 21
(apply multiply '(7 3)) ;; => 成功呼叫!得到 21

高階函數

Lisp 裡面是非常常用 Lambda 的,例如最常用的 mapcar

1
2
3
(mapcar (lambda (x) (* x 2))
'(1 2 3 4 5))
;; => (2 4 6 8 10)

或者也很常用的 remove-if

1
2
(remove-if (lambda (x) (> x 5))
'(1 2 3 4 5 6 7 8 9))

關於 Lambda 一些有趣的事情

Lambda calculus 又是個更難懂的東西,我自己也還沒仔細去看過。不過前陣子有大大跟我說「let 是可以很簡單的轉換成 lambda 的」:

1
2
3
4
5
6
7
8
9
10
;;; let
(let ((a 3)
(b 7))
(* a b))

;;; lambda
((lambda (a b)
(* a b)) 3 7)

;; 經過上面的解釋你應該立刻能看懂了吧?兩個都會得到 21

原來 let 的本質這麼簡單,美得看到當下真是嚇了一跳。有時間來讀一下 lambda calculus 吧。