LISP 七日談

caution!! contain lisp!

起初,神創造七個公理, 於是有了 lisp。

格林斯潘第十定律

任何 C 或 Fortran 程序複雜到一定程度之後, 都會包含一個臨時開發的、不合規範的、 充滿程序錯誤的、運行速度很慢的、 只有一半功能的 Common Lisp 實現。

cons

神先造了 cons,再用 cons 造出世界萬物。

cons 是一種資料結構,也是建構函數, 有二個欄位,分別叫 car cdr。 欄位內容可以是任何東西, 例如 number list function 或另一個 cons。

(setq my-cons (cons 1 "hello")) ; (1 . "hello")
(car my-cons)  ; 1
(cdr my-cons)  ; "hello"

列表

列表由 cons 構成,有點像資料結構中的 link list, 每個 cons 在 cdr 裡存了下一個 cons,但只有單向。

而最後一個 cons 的 cdr 是一個特殊的值 nil, 用來標示列表結束。

(1 . (2 . (3 . nil)))  ; dot pair 的寫法
(1 2 3)                ; list 的寫法

而一個空表則直接表示成 nil。 想像只有一個元素的表是 (1 . nil) , 則空表就是 nil

另外這也造成對一個表取 cdr, 即是取得除首去元素的子列表。

(1 . (2 . (3 . nil)))
(1 2 3)
(car (1 . (2 . (3 . nil))))  ; (2 . (3 . nil))
(car (3 . nil))              ; nil 也就是空表

既然我們有了列表,可以在列表裡存另一個列表, 形成樹的結構。

(1
 2
 (1 2)
 (3
  (4 5)
  (5 6 7))
 5)

附錄: nil 的問題

函數

神說,函數是好的,於是便不用那算符,只剩餘函數。

括號的歧義

S 表達式

神看那程式和列表相像,就用列表把程式給表示了。

控制結構

神說 s 表達式是好的,就用 s 表達式表達控制結構。

函數是先對參數求值,再把求值結果傳入函數; 控制結構則沒有一定的做法。 這些求值順序和函數不同的,稱為 special form

同為 s 表達式,讓控制結構看起來很像函數, 但他們本質仍是不同的。

例如 and or 如果是 短路求值 的則屬 special form, 也就是先看第一個是否為 true,若有需要才對第二個求值。 if cond 也都屬 special form。

(if (= a b)
    (+ c (* a 2))
    (+ c a b))

(cond
    ((< x 10) 1)
    ((< x 20) 2)
    ((< x 30) 3)
    (t 4))

迴圈與遞迴

神說,神不需要迴圈。

一般程式語言有一種結構叫迴圈, 用來重複執行直到條件滿足。

但迴圈並不必要,遞迴可以實現迴圈的功能, 甚至更多。所以 lisp 沒有迴圈; 迴圈作為一種巨集形式存在。

(defun while (test do)
    (if (test)
        (progn 
            (do)
            (while test do))))
function fwhile(test, todo) {
    if (test()) {
        todo()
        fwhile(test, todo)
    }
}

let x = 0
fwhile(
    () => x < 10,
    () => {
        alert(x)
        x++
    }
)

遞迴與尾端遞迴

尾端遞迴是遞迴的一種特殊型式。 特色是只需要追蹤固定數量的變數,和迴圈類似。

可以最佳化成在常數空間、一次多項式時間內完成, 也就和迴圈相同。

;f1 f2 counter
(fib 0 1 5)
(fib 1 1 4)
(fib 1 2 3)
(fib 2 3 2)
(fib 3 5 1)
(fib 5 8 0)
5
let f1 = 0
let f2 = 1
for (let i=5; i>0; i--) {
    let nextF1 = f2
    let nextF2 = f1 + f2
    f1 = nextF1
    f2 = nextF2
}
(fib 5)
(+ (fib 4) (fib 3))
(+ (+ (fib 3)
      (fib 2))
   (+ (fib 2)
      (fib 1)))
(+ (+ (+ (fib 2)
         (fib 1))
      (+ (fib 1)
         (fib 0)))
   (+ (+ (fib 1)
         (fib 0))
      0))

語法樹

lisp 把程式也看成一個表。 例如 (subst (list a b c) a d) 本身是一份程式, 也能看成一個 list,其中第二個元素也是一個表。

所以在 lisp 中寫 macro 很容易。 在其它語言中,要寫 macro 要手動操作語法樹, 極為麻煩。

// sweet.js
syntax let = function (ctx) {
    let ident = ctx.next().value
    ctx.next() // eat `=`
    let init = ctx.expand('expr').value
    return #`
        (function (${ident}) {
            ${ctx} // <2>
        }(${init}))
    `
}
let x = y
alert(x+6)
f(x)
alert(x*3)

/* produce */

(function (x) {
    alert(x+6)
    f(x)
    alert(x*3)
}(y))

quote

但這裡有個問題,lisp 程式中的那些 if and map 要怎麼表示? 表示成字串好像可以,但那字串又要怎麼辦? 如果函數和變數在同一命名空間, 那 map 可以表示成函數的值, 但 if and 這些不屬函數的 special form 怎麼辦?

所以發明了 quote 這個系統, 被 quote 起來的可以視為一段 hash 值。 (在傳統 lisp 中是記憶體位置。)

(quote s)
;; 簡寫為
's
'(if (= a b)
     a
     b)

被 quote 後就不會被求值, quote 的結果可以視為一個 命名空間 中的一個名字, 可以在一個 環境 被 eval 求值。

eval

eval 是一個特殊函數,他 求值 一個 list。

quote 可以 quote 名字,也能 qoute list, 被 quote 的 list 內容不會執行, 都會視為 symbol。

(setq my-list '(concat "hello, " "world!"))
(car my-list)    ; 'concat
(eval my-list)   ; "hello, world!"
(eval (subst 'concat 'print my-list))  ; 把 my-list 中的 concat 換成 print

macro

所以對 lisp 來說,macro 就是收一個 list, 然後改啊改啊改,再回傳一個 list。

相比其它語言只有字串代換的簡單 macro, 不然就是要操作複雜的語法; lisp 的 macro 如此簡單, 就是因為 lisp 的語法節單所致。

副作用

lisp 函數內是一個或多個 expression, 一般一個就夠了, 如果有多個,也會依序執行。

(defun my-function (x y)
    (+ x y)                   ; 沒有意義的 expression
    (+ (* x 2) (* y 6))       ; 沒有意義的 expression
    (setq x 18)               ; 這個 expression 有意義,因為有副作用
    (* (/ x 2) (- 6 y)))      ; 這個有意義,因為會被 return

除非有 副作用 , 否則只有最後一個 expression 有意義, 因為其它都不會被 return。

附錄

nil 與 false

傳統 lisp 中 nil 和布林值 false 是用同一個符號表示。 但這樣有個問題,要分辨 中的 nil 是表示 false 還是空表有點麻煩。

(eq nil ()) ; t
((1 3 5)
 (t t nil)  ; 這個 nil 是表示空表還是 false?
 ((1 2)     ; () 和 nil 是同義
  (3)
  ()))

函數與變數

傳統 lisp 區分函數與變數的命名空間, 就和 C 語言差不多。 所以上面一些代碼會無法運行 :P

主流的 common lisp 依然如此, 而 scheme 則不區分。

在區分的 lisp 實作中, 通常要將函數做為值傳遞時, 需要用一種特殊的 quote 寫法, 被暱稱為 function quote

(defun square (x)
    (* x x))

(map #'square
     '(1 2 3 4 5))
;; (1 4 9 16 25)

(map (lambda (x) (* x x))
     '(1 2 3 4 5))

作用域

作用域體現在程式如何處理 自由變數 這個問題上。 自由變數在呼叫時決定是 動態作用域 , 在定義時決定是 詞法作用語

(defun user-say (sentence)
    (format "%s: %s\n" 
            user        ; user 在這個 scope 中沒有定義,
                        ; 也就是沒有被綁定某一個值,稱為自由變數。
            sentence))

傳統 lisp 是動態作用域, 主要因為傳統 lisp 多半直接把函數看成一個 list, 直接用類似 eval 的方式執行。 為了方便,當發現自由變數時, 直接在執行時找當然是最簡單的作法。

詞法作用域的作法多是在定義函數時 把定義的 環境 作一份快照, 一個函數就是由快照和函數體二部份組成。 函數執行時,遇到自由變數就直接回環境中找。

介紹文案:LISP 一日談

提起 lisp 你會想到什麼? 古董級的語言?人工智慧?大量的括號? lisp 是一門函數導向、超編程導向的語言。 函數導向意味抽象化過程, 超編程則意味強大的巨集系統。

許多人曾說,接觸 lisp 能改變你的編程觀念, 讓你重新思考什麼是語言,什麼是良好的結構。 lisp 語言古老又新穎,語法彈性強大。 百聞不如一見,想知道 lisp 究竟有什麼魔力, 歡迎親自來 CCNS 的定期聚; let's talk in lisp!

setq 與 setf

最初,lisp 只有 set 函數, set 函數設置一個符號的值, 被賦值的必須是 符號

(set 'foo "bar")        ; 注意,foo 必須被 quote 了

(set 'a 'b)         ; a 的內容是另一個符號 b
(set a "bbb")       ; 這裡 a 沒有被 quote,所以會被 eval 成 b
(print b)           ; bbb

set quote

但人們覺得每次設值都必須 quote 太麻煩了, 於是有了 setq,為一個巨集, 會自動把名稱 quote。

(setq hello "world")
;; 被改寫為
;; (set (quote hello) "world")

set field

然後,有了對物件的欄位賦值的函數 setf。 模仿 myObj.car = "my car" , 寫成 (setf (car myObj) "my car")

當然也是用巨集達成的, 而 setf 中的 (car my-cons) 並不被執行, 只是一種便於閱讀的寫法。

(setf (car my-cons) "the my cons car")
;; 相當於
;; (set-car my-cons "the my cons car")
(print (car my-cons))  ; "the my cons car"

投影片