newLISP in 21 minutes

沙棗發表於2014-05-23

newLISP — 互動式教程

這份文件於 2006 年 5 月被 Rick Hanson (cryptorick@gmail.com) 做了一些修正和更新後被轉換成 html 文件。2008 年 12 月被 L.M 更新到 v.10.0 版本. 版權所有 John W. Small 2004。

你可以到 newLISP 官方網站 www.newLISP.org 下載和安裝這門語言。

關於這個教程的任何意見和問題請發郵件到 jsmall@atlaol.net。

中文版翻譯時,newLISP 的版本已經到了 10.6。這和當時撰寫文件的時候,已經相隔甚遠。一些內建函式的名稱發生了變化,語言的功能也擴充套件了很多。我根據實際的情況修改了相應的章節,這樣所有的程式碼就都可以在新的版本上進行測試執行了。

中文翻譯:宋志泉(ssqq) QQ: 104359176 電子郵件:perlvim@gmail.com

Hello World!

在你的系統上安裝 newLISP 之後, 在 shell 命令列下輸入 newlisp 就可以啟動 REPL (讀取,計算,列印迴圈)。

在 Linux 系統中,你的介面看起來像這樣:

$ newlisp
> _

如果是在 Windows 平臺上,它會是這個樣子:

c:\> newlisp
> _

在 REPL 啟動後,newLISP 會出現一個響應輸入的提示:

> _

在下面的提示中輸入如下表示式,就可以在螢幕上列印出 "Hello World!"。

> (println "Hello World!")

newLISP 列印出輸入在 REPL 中提示符後的表示式結果,並等待下一次輸入。

> (println "Hello World!")
Hello World!
"Hello World!"
> _

為什麼會列印出兩次呢?

函式 println 的執行結果在螢幕上列印出第一行:

Hello World!

函式 println 然後返回字串“Hello World!”。這是它最後一個引數,會返回給 REPL,REPL 會把它顯示在螢幕上,這是第二行的由來。

"Hello World!"

REPL 會計算任何表示式,不單單計算函式。

> "Hello World!"
"Hello World!"
> _

如果你輸入上面的表示式 "Hello World!",它只是返回表示式本身,如果輸入數字結果也是一樣的。

> 1
1
> _

現在你可能會想:成對的括號怎麼沒用到呢?如果你以前使用主流的計算機語言,像下面的函式呼叫看起來是不是更自然一點:

println("Hello World!")

我相信過段時間你會喜歡下面的寫法:

(println "Hello World!")

而不是:

println("Hello World!")

因為一些原因,不能詳細解釋,等到你看到更多的關於處理列表和符號的 newLISP程式碼後,也許就會明白。

程式碼和資料是可以互換的

Lisp 的本意是列表處理(List Processor)。 Lisp 使用 lists 同時表示程式碼和資料,它們彼此之間是可以互相轉換的。

以前的 println 表示式是一個真正的擁有兩個元素的列表。

(println "Hello World!")

第一個元素是:

println

第二個元素是:

"Hello World!"

Lisp 總是會將列表作為函式呼叫進行執行,除非你引用它,從而表明它只是一個字面形式的符號表示式,也就是——資料。

> '(println "Hello World!")
(println "Hello World!")
> _

一個符號表示式可以再次被當成程式碼執行,比如:

> (eval '(println "Hello World!"))
Hello World!
"Hello World!"
> _

Lisp 程式可以在執行時構建資料的字面量,然後執行它們!

> (eval '(eval '(println "Hello World!")))
Hello World!
"Hello World!"
> _

通常單引號 ' 是引用 quote 簡寫形式。

> (quote (println "Hello World!"))
(println "Hello World!")
> _

你可以想象引用 quote 將它的引數當成字面量返回,也就是符號化引數。

> 'x
x
> (quote x)
' x
> '(1 2 three "four")
(1 2 three "four")
> _

符號,例如上面的 xthree, 還有符號列表(symbolic lists)在人工智慧領域起著舉足輕重的角色。這個教程不會探討人工智慧,但是一旦你學會用 Lisp 程式設計,你將能明白許多人工智慧的教科書的 Lisp 的程式碼含義了。

讓我們看看下面的例子:

> 'Hello
Hello
> "Hello"
"Hello"
> _

符號 'Hello 和字串字面量 "Hello" 不同. 現在你就會明白為什麼在 REPL 中使用雙引號來標註一個字串,這樣是為了和有著相同字母的符號進行區分。

函式的引數

println 函式可以擁有任意個數的引數。

> (println "Hello" " World!")
Hello World!
" World!"
> _

上面的程式碼中,引數一個接一個地合併後,輸出到螢幕,最後一個引數的值作為函式的返回值進行返回給 REPL。

通常,引數是從左到右進行計算的,然後將結果傳遞給函式。傳遞給函式的引數可以說被完全地計算過了,這就是大家所說的應用序求值(applicative-order evaluation)。

但是請注意,函式 quote 並不是這樣。

> (quote (println "Hello World!"))
(println "Hello World!")
> _

如果它的引數是這個:

(println "Hello World!")

如果它被完全解釋後傳遞,我們將會在螢幕上看到:

Hello World!

事實並不是這樣,函式 quote 是一種特殊的函式,通常被稱為特殊形式函式(special form)。

你可以在 newLISP 中設計自己的特殊形式函式,這種函式叫做巨集(macro), 它的引數能以字面量被呼叫。這就是正則序求值(normal-order evaluation),我們說這種順序是惰性的。也就是說,一個巨集的引數在傳遞過程中並不會被直接計算(我們將在下面瞭解具體情況)。

因此,函式 quote 將引數按字面量傳遞並返回。在某種意義上,引用 quote 代表了典型的惰性計算原則。它並不對引數做任何運算,只是單單的按照字面量返回它。

如果沒有特殊形式函式,其他函式中的流程控制,是不能在只有列表語法的語言中實現的。例如,看看下面的 if 函式:

> (if true (println "Hello") (println "Goodbye"))
Hello
"Hello"
> _

特殊形式函式 if 接受三個引數:

語法: (if condition consequence alternative)

condition(條件)        =>   true
consequence(結果)      =>   (println "Hello")
alternative(替代)      =>   (println "Goodbye")

引數 condition 總是被完全地計算,但引數 consequencealternative 的表示式是惰性的。因為引數 alternative 的表示式可能根本不需要計算。

請注意 if 這個表示式。它返回的值到底是 consequence 還是 alternative,依賴於 condition 是真還是假。在以上的例子中,alternative 表示式沒有後被計算,因為列印到螢幕“Goodbye”的副作用永遠都不會出現。

如果一個 if 表示式的條件 condition 表示式測試為假,但又沒有 alternative 語句,那麼它就會返回 nilnil 的意思根據不同的環境可能解釋為空值(void)或假(false)。

注意:在大多數主流計算機語言中,if 只是一個語句,並不會產生返回值。

如果 Lisp 缺乏這個惰性計算特性,它就無法用來實現特殊形式函式或巨集(macro)。如果沒有惰性計算,大量額外的關鍵字 and/or 語法就會不得不加入到語言中。

直到現在,你看到幾種語法?括號和引用?哦,似乎有點少!

惰性計算帶給你的就是,我們自己可以在語言中新增個性化的流程控制方式,來擴充套件這門語言,訂製自己的專用語言。函式和巨集的書寫將在本教程的後面部分。

副作用和 Contexts

沒有了副作用,REPL 就沒有什麼意思了。想知道為什麼,看看下面的例子:

> (set 'hello "Hello")
"Hello"
> (set 'world " World!")
" World!"
> (println hello world)
Hello World!
" World!"
> _

上面的函式 set 有一個副作用,就像下面的例子:

> hello
"Hello"
> world
" World!"
> _

符號 'hello'world 繫結到當前的 Context,值分別是 "Hello"" World!"

newLISP 所有的內建函式是繫結到名字叫 MAIN 的 Context。

> println
println <409040>
> set
set <4080D0>
> _

這個例子說明 println 這個符號繫結到一個名字叫 println 的函式,呼叫地址是 409040。(println 在不同的電腦可能會有不同的地址。)

預設的 Context 是 MAIN。一個 context 其實是一個名稱空間。我們將稍後學習使用者自己定義的名稱空間。

請注意符號 'hello 的字面量計算的結果就是自身。

> 'hello
hello
> _

對符號 'hello 求值將返回它在當前 Context 繫結的值。

> (eval 'hello)
"Hello"
> _

當一個符號在求值的時候還沒有繫結任何值,它就會返回 nil

> (eval 'z)
nil
> _

通常我們並不需要 eval 去獲取一個符號的值,因為一個沒有引用的符號會自動被展開成它在當前 context 所繫結的值。

> hello
"Hello"
> z
nil
> _

因此下面的符號 helloworld 的值分別是 "Hello"" World!"

> (println hello world)
Hello World!
" World!"
> _

如果我們輸入如下的內容,將會顯示什麼呢?

> (println 'hello 'world)
?

你可以先想一想。

函式 println 會在第一行立即一個接一個的顯示這些符號。

> (println 'hello 'world)
helloworld
world
> _

表示式序列

一個表示式的序列可以用函式 begin 合併成一組序列。

> (begin "Hello" " World!")
" World!"
> _

表示式 "Hello" 做了什麼? 既然一組表示式只是返回單一的值,那麼最後一個表示式的值才是最後返回的值。但實際上所有的表示式確實被一個接一個的計算求值了。只是表示式 "hello" 沒有什麼副作用,因此它的返回值被忽略了,你當然也不會看到它的執行結果。

> (begin (print "Hello") (println " World!"))
Hello World!
" World!"
> _

這次,函式 printprintln 的副作用在螢幕上顯示出來,而且 REPL 返回了最後一個表示式的值。

函式 begin 很有用,它可以將多個表示式合併成另一個獨立的表示式。我們再看看特殊形式函式 if

>[cmd]
(if true
  (begin
     (print "Hello")
     (println " newLISP!"))
  (println "So long Java/Python/Ruby!"))[cmd]

Hello newLISP!
" newLISP!"
> _

(注:在提示符後輸入多行程式碼需要在程式碼開始和結束分別加上 [cmd] 直到完成所有程式碼。)

由於 if 只接受三個引數:

syntax: (if condition consequence alternative)

對於多個表示式使用 (begin ...) ,就可以合併多個表示式為一個表示式,被當成 consequence 引數後全部被執行。

讓我們總結一下我們學到的東西,看看如何把它們整合成一個完整的程式。

最後要注意:你可以使用如下的命令來退出 REPL 求值環境。

> (exit)
$

在 Windows 上是這個樣子:

> (exit)
c:\>

你也可以使用一個可選的引數來退出。

> (exit 3)

這個特性在 shell 或 batch 命令列中報告錯誤程式碼非常有用。

現在我們可以把 hello world 的表示式寫在一個檔案中。

;  This is a comment

;  hw.lsp

(println "Hello World!")
(exit)

然後我們可以從命令列來執行它,就像這樣:

$ newlisp hw.lsp
Hello World!

Windows 上是這樣:

c:\> newlisp hw.lsp
Hello World!

可執行檔案和動態連結(Executables and Dynamic Linking)

編譯連結一個 newLISP 原始碼為一個獨立的可執行程式只需要使用 -x 命令列引數。

;; uppercase.lsp - Link example
(println (upper-case (main-args 1)))
(exit)

程式 uppercase.lsp 可以將命令列的第一個單詞轉換成大寫的形式。

要想將這段原始碼轉換成獨立的可執行檔案的步驟是:

在 OSX、Linux 或其他 UNIX 系統:

> newlisp -x uppercase.lsp uppercase
> chmod 755 uppercase # give executable permission

在 Windows 系統上目標檔案需要 .exe 字尾:

$> newlisp -x uppercase.lsp uppercase.exe

newLISP 會找到環境變數中的 newLISP 可執行檔案並將原始檔和它連結在一起。

$> uppercase "convert me to uppercase"

控制檯會列印出:

CONVERT ME TO UPPERCASE

注意並沒有什麼初始化檔案 init.lsp.init.lsp 在連結的過程中被載入。

連結到一個動態庫將遵循同樣的原則。

(Linux 版本的例項暫缺)

在 Windows 平臺上,下面的程式碼會彈出一個對話方塊。

(import "user32.dll" "MessageBoxA")

(MessageBoxA 0 "Hello World!"
"newLISP Scripting Demo" 0)

請注意 MessageBoxA 是 win32 系統中的一個 C 語言的使用者函式介面。

下面的程式碼演示瞭如何呼叫一個用 C 寫的函式(需要使用 Visual C++ 進行編譯)。

// echo.c

#include <STDIO.H>
#define DLLEXPORT _declspec(dllexport)

DLLEXPORT void echo(const char * msg)
{
printf(msg);
}

在將 echo.c 編譯到一個 DLL 檔案後,它就能被下面的程式碼呼叫了。

(import "echo.dll" "echo")

(echo "Hello newLISP scripting World!")

這種可以方便的和動態連結庫互動的能力,讓 newLISP 成為一種夢幻般的指令碼語言。如果你在看看其他的關於套接字程式設計和資料庫連線的程式碼和模組,你就會確信這一點。

繫結(Binding)

上面介紹過,函式 set 用於將一個值繫結到一個符號上。

(set 'y 'x)

在這個例子中的值 'x, 一個符號本身,被繫結到一個名字為 y 的變數中。

現在看看下面的繫結。

(set y 1)

既然沒有引用, y 被展開為 'x,緊接著 1 並繫結到變數名為 x 的變數中。

> y
x
> x
1
> _

當然,變數 y 依然繫結的是 'x 這個值。

函式 setq 讓你每次少寫一個引用。

(setq y 1)

現在名稱為 y 的變數重新繫結的值為 1

> y
1
> _

函式 define 完成了相同的工作。

> (define y 2)
2
> y
2
> _

請注意 setsetq 都能一次繫結多個關聯的符號和值。

> (set 'x 1 'y 2)
2
> (setq x 3 y 4)
4
> x
3
> y
4
> _

(你應當同我們一起驗證這些程式碼,並記住這些寫法)

不像 setq 函式 define 只能一次繫結一個關聯的值。但 define 還會有另外的用處,稍後會講到。

很顯然,函式 set, setq, 和 define 都有副作用,並且返回一個值。而副作用就是在當前的名稱空間裡的一個隱含的符號表中,建立變數和值的關聯。

我們可以把這個隱含的符號表想象成一個關聯表。

> '((x 1) (y 2))
((x 1) (y 2))
> _

上面的關聯表是一個列表的列表。巢狀的列表都有兩個元素,也就是鍵值對。第一個元素代表名字,而第二個元素代表的是它的值。

> (first '(x 1))
x
> (last '(x 1))
1
> _

關聯表的第一組內容描述了一個符號和值的關聯。

> (first '((x 1) (y 2)))
(x 1)
> _

內建的函式 assoclookup 提供了操作關聯列表的能力。

> (assoc 'x '((x 1) (y 2) (x 3)))
(x 1)
> (lookup 'x '((x 1) (y 2) (x 3)))
1
> _

(函式 lookup 還有其他的用途,具體可以查詢 newLISP 使用者手冊。)

請務必注意 assoclookup 只是返回找到的第一個鍵 a 所關聯的列表本身或它所關聯的值。這一點非常重要,這是我們將要講到的符號表和相應展開的話題的一個基礎。

List 是一種遞迴結構

任何包含關聯表或巢狀表的列表都可以被認為是遞迴的資料結構。一個定義列表都有一個頭元素,尾列表和最後一個元素。

> (first '(1 2 3))
1
> (rest '(1 2 3))
(2 3)
> (last '(1 2 3))
3

但看看下面的程式碼:

> (rest '(1))
()
> (rest '())
()
> (first '())
nil
> (last '())
nil

一個空列表或者只有一個元素的列表的 rest 部分同樣是個空列表。空列表的第一個元素和最後一個元素始終是 nil. 請注意 nil 和空列表完全不同,只有不存在的元素才用 nil 來表示。

(請注意 newLISP 對列表的定義和 Lisp 和 scheme 其他方言對列表的定義是有區別的。)

一個列表可以用一個遞迴的演算法進行處理。

例如,使用遞迴的演算法計算一個列表的長度可能是這樣的:

(define (list-length a-list)
(if (first a-list)
  (+ 1 (list-length (rest a-list)))
  0))

首先,請注意 define 不但可以定義變數,也能定義函式。我們函式的名字是 list-length 而且它接受一個叫 a-list 的引數. 所有定義的引數,就是在函式內預先宣告的變數定義。

你可以使用許多字元來做符號的名字,這種允許多種風格對變數進行定義的能力,在一些主流語言中是沒有的。若想了解完整的命名規則,請檢視 newLISP 使用者手冊。

函式 if 在測試條件的時候,除非結果是 nil 或者一個空表例如 '(), 都將返回真. 這樣我們就可以單單用 if 測試一個列表就能知道它是不是空表了。

(if a-list
    ...

只要列表還有頭元素,那麼計數器就可以繼續將函式 list-length 最後的結果加 1,並繼續處理剩餘的尾列表。既然空表的第一個元素為 nil, 那麼當計算到最後時,可以返回零來退出這個巢狀的呼叫函式 list-length 的棧。

我們說一個列表是一個遞迴的資料結構是因為它的定義是遞迴的而不是說只是因為它可以使用遞迴的演算法進行處理。

一個遞迴的列表的定義可以用下面的 BNF 語法進行描述:

type list ::=  empty-list | first * list

一個列表既可以是一個空列表,也可以是包含一個頭元素和本身是一個列表的尾列表的組合。

既然計算一個列表的長度是如此常用,newLISP 當然就會有一個內建的函式來做這件事情:

> (list-length '(1 2 5))
3
> (length '(1 2 5))
3
> _

我們稍後會回到使用者定義函式的討論中。

一個隱式的符號表可以被看成是已經被計算過的一個關聯列表。

> (set 'x 1)
1
> (+ x 1)
2
> _

因為副作用通常會影響輸出流或隱式的 context 。一個關聯列表只是描述這個隱式符號表的一種形式。

假設我們想隨時改變一個變數所繫結的值,而又不改變它以前的值:

> (set 'x 1 'y 2)
2
>
(let  ((x 3) (y 4))
  (println x)
  (list x y))

3
(3 4)
> x
1
> y
2
> _

請注意 xy 在隱式的符號表中分別繫結了 12。而 let 表示式在表示式內部範圍內暫時的(動態的)再次分別繫結 xy34。也就是說,let 表示式處理的是一個關聯列表,而且按照順序一個一個的處理裡面的繫結表示式。

函式 list 接受多個引數,並且返回這些引數被完全計算後返回的值組成的列表。

let 形式和 begin 形式很像,除了它在 let 塊中有一個臨時的符號表記錄。因為 let 中的表示式引數是惰性的,只在 let 的 context 中被展開。如果我們在 let 塊中檢視符號表,它看起來就像下面的關聯列表:

'((y 4) (x 3) (y 2) (x 1))

既然 lookup 從左向右查詢繫結的 xy 的值,那麼就遮蔽了 let 表示式以外的值。當 let 表示式結束後,符號表就會恢復成下面的樣子:

'((y 2) (x 1))

離開 let 表示式後,後面對 xy 的計算就會按照它們以前的值進行操作了。

為了讓大家看得更清楚,請比較以下的程式碼:

> (begin (+ 1 1) (+ 1 2) (+ 1 3))
4
> (list (+ 1 1) (+ 1 2) (+ 1 3))
(2 3 4)
> (quote (+ 1 1) (+ 1 2) (+ 1 3))
(+ 1 1)
> (quote (2 3 4))
(2 3 4)
> (let () (+ 1 1) (+ 1 2) (+ 1 3))
4

注意 quote 只處理一個引數。(我們稍後會了解到它為什麼會忽略剩餘的引數。)一個沒有動態繫結引數的 let 表示式的行為就像 begin 一樣。

現在可以想想下面的表示式會返回什麼呢?(隨後就會有答案)

> (setq x 3 y 4)
> (let ((x 1) (y 2)) x y)
?
> x
?
> y
?

> (setq x 3 y 4)
> (begin (set 'x 1 'y 2) x y)
?
> x
?
> y
?

答案是:

> (setq x 3 y 4)
> (let ((x 1) (y 2)) x y)
2
> x
3
> y
4

> (setq x 3 y 4)
> (begin (set 'x 1 'y 2) x y)
2
> x
1
> y
2

讓我們這次來點難度高點的:

> (setq x 3 y 4)
> (let ((y 2)) (setq x 5 y 6) x y)
?
succeeding> x
?
> y
?

答案:

> (setq x 3 y 4)
> (let ((y 2)) (setq x 5 y 6) x y)
6
> x
5
> y
4

下面的資料結構可能會幫助你理解這樣的解答是怎麼來的:

'((y 2) (y 4) (x 3))

上面的關聯列表顯示,當符號表進入 let 表示式內部後,符號 y 被立即擴充套件後的內容。

在以下的程式碼執行完後:

(setq x 5 y 6)

擴充套件的符號表看起來像下面這樣:

'((y 6) (y 4) (x 5))

當從 let 表示式出來後,符號表會變成這樣:

'((y 4) (x 5))

因此 set, setq, 和 define 會給符號重新繫結一個新值,如果這個符號已經存在的話,如果不存在,就在符號表的前面增加一個新的繫結關聯。我們將在看看函式的話題後稍後回來繼續討論這個話題。

函式(Functions)

使用者定義函式可以被 define 定義(就像我們早先討論的)。下面的函式 f 返回了兩個引數的和。

(define (f x y) (+ x y))

這種寫法實際上是以下這些寫法的縮寫:

(define f (lambda (x y) (+ x y)))

(setq f (lambda (x y) (+ x y)))

(set 'f (lambda (x y) (+ x y)))

lambda 表示式定義了一個匿名的函式,或者說是一個沒有名字的函式。lambda 表示式的第一個引數是一個形式引數的列表,而接下來的表示式組成了一個惰性的表示式序列,用來描述整個函式的計算過程。

> (f 1 2)
3
> ((lambda (x y) (+ x y)) 1 2)
3
> _

重新呼叫這個個沒有引起的列表,會呼叫一個函式,它的引數已經準備就緒。這個列表第一個元素是一個 lambda 表示式,因此它會返回一個匿名的函式,並接收兩個引數 12,並進行計算。

請注意以下兩個表示式本質上是做了相同的事情。

> (let ((x 1) (y 2)) (+ x y))
3
> ((lambda (x y) (+ x y)) 1 2)
3
> _

lambda 表示式相比 let 表示式唯一的不同就是,它是惰性的,直到傳入引數被呼叫的時候,才會被計算。傳入的實際引數會被依次繫結到形式引數的相應符號上,而且有獨立的函式作用域。

下面的表示式將會返回什麼值呢?

> (setq x 3 y 4)
> ((lambda (y) (setq x 5 y 6) (+ x y)) 1 2)
?
> x
?
> y
?

請記住 lambda 和 let 表示式在本質上對符號表的操作行為是相同的。

> (setq x 3 y 4)
> ((lambda (y) (setq x 5 y 6) (+ x y)) 1 2)
11
> x
5
> y
4

在上面的程式碼中,引數 12 是多餘的。lambda 表示式外面傳遞進來的形參 y 的定義被遮蔽,因為 x 等於 5 是在表示式內部唯一起作用的定義。

高階函式

函式在 Lisp 是第一類值。所以它可以像資料一樣被動態的建立,而且可以被當成引數傳遞到其他的函式中而構建高階函式。請注意雖然在 C 語言中函式的指標(或是 Java、C# 中的 listeners)並不是第一類值,儘管它們可以被當成引數傳遞到函式中,但永遠不能動態的被建立。

也許最常被使用的高階函式就是 map (在面嚮物件語言中被稱為 collect 的東西就是最初從 Lisp 和 Smalltalk 中獲得的靈感)。

> (map eval '((+ 1) (+ 1 2 3) 11))
(1 6 11)
> _

上面的這個例子,函式 map 把列表中的每個元素都進行 eval 的求值。請注意函式 + 可以跟隨多個引數。

這個例子可以寫得更簡單:

> (list (+ 1) (+ 1 2 3) 11)
(1 6 11)
> _

map 其實可以做其它很多奇妙的操作:

> (map string? '(1 "Hello" 2 " World!"))
(nil true nil true)
> _

函式 map 同時也能操縱多個列表。

> (map + '(1 2 3 4) '(4 5 6 7) '(8 9 10 11))
(13 16 19 22)
> _

在第一個迭代中,函式 + 被新增到每個列表的第一個元素,並進行了運算。

> (+ 1 4 8)
13
> _

讓我們看看哪些元素是偶數:

> (map (fn (x) (= 0 (% x 2))) '(1 2 3 4))
(nil true nil true)
> _

fn 是 lambda 的縮寫。

> (fn (x) (= 0 (% x 2)))
(lambda (x) (= 0 (% x 2)))
> _

上面程式碼中的操作符 % 用於判斷一個數字是否可以被 2 整除,是取模的意思。

函式 filter 是另外一個經常用到的高階函式(在一些物件導向的語言的函式庫中叫 select)。

> (filter (fn (x) (= 0 (% x 2))) '(1 2 3 4))
(2 4)
> _

函式 index 可以用於返回列表中符合條件的元素位置資訊。

> (index (fn (x) (= 0 (% x 2))) '(1 2 3 4))
(1 3)
> _

函式 apply 是另外一個高階函式。

> (apply + '(1 2 3))
6
> _

為什麼不寫成 (+ 1 2 3)

因為有時候我們並不知道要載入哪個函式給列表:

> (setq op +)
+ <40727D>
> (apply op '(1 2 3))
6
> _

這種方法可以實現動態的方法呼叫。

lambda 列表

我們先看看下面的函式定義:

> (define (f x y) (+ x y z))
(lambda (x y) (+ x y z))
> f
(lambda (x y) (+ x y z))
> _

函式定義是一種特殊形式的列表,叫 lambda 列表。

> (first f)
(x y)
> (last f)
(+ x y z)
> _

一個已經 "編譯" 到記憶體中的函式可以在執行時檢查自己。事實上它甚至能在執行時改變自己。

> (setf (nth 1 f) '(+ x y z 1))
(lambda (x y) (+ x y z 1))
> _

(你可以在 newLISP 使用者手冊中看看函式 nth-set 的定義。)

函式 expand 在更新含有 lambda 表示式的列表時非常有用。

> (let ((z 2)) (expand f 'z))
(lambda (x y) (+ x y 2 1))
> _

函式 expand 接受一個列表,並將剩下的符號引數所對應的這個列表中的符號替換掉。

動態範圍(Dynamic Scope)

先看看下面的函式定義:

>
(define f
(let ((x 1) (y 2))
  (lambda (z) (list x y z))))

(lambda (z) (list x y z))
> _

我們注意到 f 的值只是一個 lambda 表示式而已。

> f
(lambda (z) (list x y z))
> (setq x 3 y 4 z 5)
5
> (f 1)
(3 4 1)
> (let ((x 5)(y 6)(z 7)) (f 1))
(5 6 1)

儘管 lambda 表示式是在 let 的區域性詞法作用域中定義的,雖然在裡面 x1y2,但在呼叫它時,動態作用域機制將發揮作用。所以我們說:在 newLISP 中 lambda 表示式是動態作用域。(而 Common Lisp 和 Scheme 是詞法作用域。)

Lambda 表示式中的自由變數在呼叫時動態的從周圍的環境中獲取,沒有從引數傳遞進來而直接使用的變數就是自由變數。

我們可以使用之前講過的函式 expand 將一個 lambda 表示式中所有的自由變數進行強制繫結,從而讓這個匿名函式被“關閉”。

>
(define f
(let ((x 1) (y 2))
  (expand (lambda (z) (list x y z)) 'x 'y)))

(lambda (z) (list 1 2 z))
> _

注意現在這個 lambda 表示式已經沒有任何自由變數了。

使用函式 expand "關閉"一個 lambda 表示式和 Common Lisp 和 Scheme 中的詞法作用域的 lambda 閉包不同,實際上,newLISP 有詞法閉包,這個問題我們稍後會講到。

函式引數列表

一個 newLISP 的函式可以定義任意多數量的引數(沒有理由)。

>
(define (f z , x y)
(setq x 1 y 2)
(list x y z))

(lambda (z , x y) (setq x 1 y 2) (list x y z))
> _

函式 f 的 4 個形參是:

z , x y

請注意逗號也是一個引數(參照使用者手冊的符號命名規則)。它被用在這裡別有用意。

其實真正的引數只有一個 z

如果函式的形式引數的個數多於傳入函式的實際引數的個數,那麼那些沒有匹配的形式引數就會被初始化為 nil

> (f 3)
(1 2 3)
> _

而這些引數怎麼辦呢?

, x y

這些引數都被初始化為 nil。既然符號 xy 出現在函式內部,那麼它們就成了區域性變數的生命。

(setq x 1 y 2)

上面的賦值語句不會覆蓋 lambda 表示式外部的 xy 的定義。

我們也可以用下面的程式碼宣告區域性變數來表達相同的效果:

>
(define (f z)
(let ((x 1)(y 2))
  (list x y z)))

(lambda (z) (let ((x 1)(y 2)) (list x y z)))
> _

逗號緊跟著不會用到的引數是一種在 newLISP 中經常被使用到的一種生命區域性變數的編碼方式。

函式通常在呼叫時,會被傳遞多於定義的形參的個數,這種情況下,多出的引數將被忽略。

而多餘的形參則被視為可選的引數。

(define (f z x y)
(if (not x) (setq x 1))
(if (not y) (setq y 2))
(list x y z))

上面的例子中,如果函式 f 只呼叫了一個引數,那麼另外的 xy 將分別被預設設定為 12

巨集

巨集是用 lambda-macro 定義的函式,巨集的引數不會像普通的 lambda 函式那樣被求值。

(define-macro (my-setq _key _value)
    (set _key (eval _value)))

既然 _key 沒有被求值,那麼它還是一個符號, 也就是引起的狀態,而它的 _value 也是符號,但因為有 eval, 就必須求值。

> (my-setq key 1)
1
> key
1
> _

下劃線是為了防止變數名稱衝突,我們來看下面的例子:

> (my-setq _key 1)
1
> _key
nil
> _

發生了什麼呢?

語句 (set _key 1) 只是將 _key 設定為區域性變數。我們說變數 _key 被巨集的擴充套件所佔用。Scheme 有“健康”巨集可以有效的保證不會發生變數的衝突。通常使用帶下劃線的變數名稱可以有效的阻止這種問題的發生。

函式 define-macro 是另外一種書寫巨集的更簡潔的寫法:

(define my-setq
    (lambda-macro (_key _value)
        (set _key (eval _value))))

上面的寫法和以前的 my-setq 的寫法是等價的。

除了惰性計算,巨集也可以接受許多的引數。

(define-macro (my-setq )
    (eval (cons 'setq (args))))

函式 cons 將一個新的元素置於一個列表的頭部,也就是成為列表的第一個元素。

> (cons 1 '(2 3))
(1 2 3)
> _

現在 my-setq 的定義更加完善了,可以同時允許多個繫結。

> (my-setq x 10 y 11)
11
> x
10
> y
11
> _

函式 (args) 呼叫後會返回所有的引數給巨集,但並不進行求值計算。

這樣巨集 my-setq 第一次構造了以下的符號表示式:

'(setq x 10 y 11)

這個表示式然後就會被求值。

巨集主要的用途是擴充套件語言的語法。

假設我們將增加一個 repeat until 流程控制函式作為語言的擴充套件:

(repeat-until condition body ...)

下面的巨集實現了這個功能:

(define-macro (repeat-until _condition )
(let ((body (cons 'begin (rest (args)))))
(eval (expand (cons 'begin
  (list body
    '(while (not _condition) body)))
  'body '_condition))))

repeat-until

(setq i 0)
(repeat-until (> i 5)
    (println i)
    (inc i))
; =>
0
1
2
3
4
5

巨集會很快變得非常複雜。一個好辦法就是用 listprintln 來替代 eval 來看看你要擴充套件的表示式擴充套件後是什麼樣子。

(define-macro (repeat-until _condition )
    (let ((body (cons 'begin (rest (args)))))
    (list (expand (cons 'begin
      (list body
       '(while _condition body)))
     'body '_condition))))

現在我們可以檢查一下這個巨集擴充套件開是什麼樣子:

> (repeat-until (> i 5) (println i) (inc i))
    ((begin
    (begin
    (println i)
    (inc i))
    (while (> i 5)
    (begin
        (println i)
        (inc i)))))
    > _

Contexts

程式開始預設的 Context 是 MAIN

> (context)
MAIN

Context 是一個名稱空間。

> (setq x 1)
1
> x
1
> MAIN:x
1
> _

可以用包含 Context 名稱的完整的名稱標識一個變數。 MAIN:x 指向 Context 為 MAIN 中名稱為 x 的變數。

使用函式 context 可以建立一個新的名稱空間:

> (context 'FOO)
FOO
FOO> _

上面的語句建立了一個名稱空間 FOO, 如果它不存在,那麼就會切入這個空間。提示符前面的內容會告訴你當前的名稱空間,除非是預設的 MAIN

使用函式 context? 可以判斷一個變數是否繫結為一個 context 名稱。

FOO> (context? FOO)
true
FOO> (context? MAIN)
true
FOO> (context? z)
nil
FOO> _

函式 setsetqdefine 會在當前的 context 也就是名稱空間中繫結一個符號的關聯值。

FOO> (setq x 2)
2
FOO> x
2
FOO> FOO:x
2
FOO> MAIN:x
1
FOO> _

在當前的 context 中繫結變數並不需要宣告完整的名稱如 FOO:x

切回到 context MAIN (或其他已經存在的 context ) 只需要寫 MAIN,當然寫 'MAIN 也行。

FOO> (context MAIN)
> _

或者:

FOO> (context 'MAIN)
> _

只有在建立一個新的 context 的時候,才必須使用引起符號 '

context 不能巢狀 —— 他們都住在一起,之間是平等的。

注意下面的程式碼中的變數名 y,是在 MAIN 中定義的,在 context FOO 中不存在這個名稱的變數。

> (setq y 3)
3
> (context FOO)
FOO
FOO> y
nil
FOO> MAIN:y
3
FOO> _

下面這個程式碼說明除了 MAIN,別的 context 也可以作為預設的 context。MAIN 並不知道變數 z 的定義。

FOO> (setq z 4)
4
FOO> (context MAIN)
MAIN
> z
nil
> FOO:z
4

所有內建的函式名稱都儲存在一個全域性的名稱空間中,就像是在 MAIN context 中定義的一樣。

> println
println <409040>

> (context FOO)
FOO
FOO> println
println <409040>

內建函式 printlnMAINFOO 的名稱空間內都能被識別。函式 println 是一種被 "匯出" 到全域性狀態的一個名稱。

下面的程式碼顯示出:變數 MAIN:t 不能在名稱空間 FOOBAR 中被識別,除非被標記為全域性狀態。

FOO> (context MAIN)
MAIN
> (setq t 5)
5
> (context 'BAR)
BAR
BAR> t
nil
BAR> (context FOO)
FOO
FOO> t
nil
FOO> (context MAIN)
MAIN
> (global 't)
t
> (context FOO)
FOO
FOO> t
5
FOO> (context BAR)
BAR
BAR> t
5

只有在 MAIN 中才可以定義全域性狀態的變數。

區域性作用域(Lexical Scope)

函式 setsetqdefine 會繫結名字到當前的名字空間。

> (context 'F)
F
F> (setq x 1 y 2)
2
F> (symbols)
(x y)
F> _

請注意:函式 symbols 會返回當前名稱空間所有繫結的符號名稱。

F> (define (id z) z )
(lambda (z) z)
F> (symbols)
(id x y z)
F> _

當前 context 定義的符號的作用域的範圍會一直到下一個 context 的切換為止。既然如此,你可以稍後返回原來的 context 繼續擴充套件你的程式碼,但這樣會讓原始檔產生碎片。

F> (context 'B)
B
B> (setq a 1 b 2)
2
B>

我們說的區域性範圍,指的是在程式碼中變數定義的有效範圍。在 context F 中定義的符號 ab 有效範圍同在 context B 中定義的符號 a and b 是不同的。

所有的 lambda 表示式都定義了一個獨立的變數範圍。當 lambda 表示式結束後,這個範圍就被關閉了。

下面的 lambda 表示式不但處在 MAIN 的名字空間內,同時也在一個獨立的詞法空間 (let ((x 3)) ...) 定義的表示式內。

> (setq x 1 y 2)
2
>
(define foo
 (let ((x 3))
  (lambda () (list x y))))

(lambda () (list x y))
> (foo)
(1 2)
> _

回撥這個 lambda 表示式通常在一個動態的範圍內。這裡特別要注意:這個 lambda 呼叫好像只處在 MAIN 範圍內,而並不存在於 let 表示式內,即使是在詞法作用域內定義的函式,也好像是是當前名稱空間內定義的函式,只要函式的名稱不是詞法範圍內的。

繼續上面的例項,我們可以看到這個混和了詞法和動態作用域的機制在同時起作用。

> (let ((x 4)) (foo))
(4 2)
> _

詞法作用域的名稱空間在 let 表示式內可以呼叫動態的變數。

如果我們在另外一個名稱空間呼叫這個函式,會怎麼樣呢?

> (context 'FOO)
FOO
FOO> (let ((x 5)) (MAIN:foo))
?

先仔細想一下: 上面的 let 表示式真的能夠動態的擴充套件 FOO 而不是 MAIN 詞法範圍的詞法範圍嗎?

FOO> (let ((x 5)) (MAIN:foo))
(1 2)
FOO> _

發生了什麼呢?原來 MAIN:foo 的動態範圍只是限定於名稱空間 MAIN 中. 既然在表示式 let 中的名稱空間是 FOOMAIN:foo 就不會把 FOO:x => 5 拿過來用。

下面的程式碼是不是給你點啟發呢?

FOO> MAIN:foo
(lambda () (list MAIN:x MAIN:y))
FOO> _

當我們在 MAIN 空間中呼叫 foo 時,並沒有使用名稱限定符 MAIN

> foo
(lambda () (list x y))
> _

所以儘管在空間 FOO 中的 lambda 表示式有一個自由變數 FOO:x,我們可以看到現在 MAIN:foo 只會在主名稱空間中查詢自由變數的繫結,就再也找不到這個自由變數了。

下面的這個表示式執行的結果是什麼呢?

FOO> (let ((MAIN:x 5)) (MAIN:foo))
?

如果你的回答是下面的結果的話,就對了。

FOO> (let ((MAIN:x 5)) (MAIN:foo))
(5 2)
FOO> _

我們說名稱空間是詞法閉包,在其中定義的所有函式都是在這個詞法作用域內,即使有自由變數,這些函式也不會受其他環境的影響。

理解 newLISP 的名稱空間對明白這門語言的轉換和求值是至關重要的。

每個頂級表示式都是被一個接一個的解析,而頂級表示式中的子表示式的解析順序則不一定是這樣,在語法解析時,所有的沒有標記名稱空間名稱的變數都會被預設當成當前名稱空間的變數進行繫結。因此一個名稱空間的相關表示式只是建立或切換到指定的名稱空間。這是一個非常容易引起誤會的地方,稍後會解釋。

(context 'FOO)
(setq r 1 s 2)

上面的例子中,所有的表示式都是頂級表示式, 雖然隱含了一個新的結構。但第一個表示式將首先被解析執行,這樣 FOO 成了當前繫結變數的名稱空間。一旦語句被解釋執行,名稱空間的轉換在 REPL 模式就會看的很清楚。

> (context 'FOO)
FOO>

現在 newLISP 會根據新的環境來解析剩下的表示式:

FOO> (setq r 1 s 2)

現在當前的 context 成了 FOO

我們來看看如下的程式碼:

> (begin (context 'FOO) (setq z 5))
FOO> z
nil
FOO> MAIN:z
5
FOO> _

到底發生了什麼呢?

首先這個單獨的頂級表示式:

(begin (context 'FOO) (setq z 5))

在名稱空間 MAIN 中解釋所有的子表示式,因此 z 被按照下面的意思進行求值:

(setq MAIN:z 5)

在解析 begin 這組語句的時候,當名稱空間切換的時候,變數 z 已經被繫結到預設的 'MAIN:z' 中並賦予 5. 當從這組表示式返回時,名稱空間已經切換到了 FOO

你可以想象成當 newLISP 處理名稱空間的相關子表示式時,是分兩個階段進行處理的:先解析,後執行。

利用 context 的特性,我們可以組織資料、函式的記錄,甚至結構、類和模組。

(context 'POINT)
(setq x 0 y 0)
(context MAIN)

上例中的 context POINT 可以被當成一個有兩個屬性(槽)的結構。

> POINT:x
0
> _

context 同樣可以被克隆,因此可以模擬一個簡單的類或原型。下面程式碼中的函式 new,會建立一個名字為 p 的新的 context ,如果它不存在的話;同時它會將找到的 context POINT 中的符號表合併到這個新的名稱空間中。

> (new POINT 'p)
p
> p:x
0
> (setq p:x 1)
1
> p:x
1
> POINT:x
0

上面的程式碼表明: context p 在複製完 POINT 的符號表後和 context POINT 是彼此獨立的。

下面的程式碼演示瞭如何用 context 來模擬一個簡單的結構的繼承特性:

(context 'POINT)
(setq x 0 y 0)
(context MAIN)

(context 'CIRCLE)
(new POINT CIRCLE)

merely (setq radius 1)merely (context MAIN)

(context 'RECTANGLE)
(new POINT RECTANGLE)
(setq width 1 height 1)
(context MAIN)

new 合併 POINT 中的屬性 xyCIRCLE 中,然後在 CIRCLE 中建立了另外一個屬性 radiusRECTANGLE 同時也 "繼承" 了 POINT 所有的屬性。

下面的巨集 def-make 讓我們可以定義一個命名的名稱空間的例項,並初始化。

(define-macro (def-make _name _ctx )
(let ((ctx (new (eval _ctx) _name))
    (kw-args (rest (rest (args)))))
(while kw-args
  (let ((slot (pop kw-args))
    (val (eval (pop kw-args))))
    (set (sym (term slot) ctx) val)))
ctx))

例如你可以用名為 r 的變數例項化一個 RECTANGLE,並用下面的程式碼重寫屬性 xheight 的值。

(def-make r RECTANGLE x 2 height 2)

下面的函式將一個名稱空間的名字轉換成字串:

(define (context->string _ctx)
(let ((str (list (format "#S(%s" (string _ctx)))))
(dotree (slot _ctx)
  (push (format " %s:%s" (term slot)
    (string (eval (sym (term slot) _ctx))))
    str -1))
  (push ")" str -1)
(join str)))

現在我們可以驗證一下,輸入引數 r

> (context 'r)
> (setq height 2 width 1 x 2 y 0)
> (context->string 'r)
"#S(r height:2 width:1 x:2 y:0)"
> _

你一定注意到許多字元甚至 "->" 都可以用於一個識別符號的名字。

現在你已經知道足夠多的關於 newLISP 的知識來看明白 def-makecontext->string 是怎麼回事了。不過還是要仔細閱讀標準的 newLISP 使用者手冊來了解其它的一些核心函式,例如 dotree, push, join 等一些在本教程中沒有涉及的重要函式。

Common Lisp 和 Scheme 都有標記詞法作用域的相關函式,這樣就可以構建具有函式功能的閉包。newLISP 中的函式同樣可以共享一個具有詞法作用域的閉包,這就是 context, 這種機制就好像一個物件的方法共享一個類變數一樣。到目前為止的例項程式碼告訴我們 context 也同時可以容納函式。newLISP 的手冊中有好幾個事例程式碼來演示如何用 context 來模擬簡單的物件。

(完)

相關文章