newLISP 初級教程

newLISP — 交互式教程

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

你能夠到 newLISP 官方網站 www.newLISP.org 下載和安裝這門語言.算法

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

中文版翻譯時 newLISP 的版本已經到了 10.6 這和當時撰寫文檔的時候,已經相隔甚遠。一些內置函數的名稱發生了變化,語言的功能也擴展了不少。我根據實際的狀況修改了相應的章節,這樣全部的代碼就均可以在新的版本上進行測試運行了。數據庫

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

Hello World!

在你的系統上安裝 newLISP 以後, 在 shell 命令行下輸入 newlisp 就能夠啓動 REPL (讀取,計算,打印循環).vim

在 Linux 系統中,你的界面看起來像這樣:數據結構

$ newlisp
> _

若是是 Windows 平臺,會是這個樣子:閉包

c:\> newlisp
> _

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

> _

在下面的提示中輸入以下表達式,就能夠在屏幕上打印出 "Hello World!".函數

> (println "Hello World!")

newLISP 打印出輸入在 REPL 中提示符後的表達式求值結果,並等待下一次輸入.

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

爲何會打印出兩次呢?

函數 println 的執行結果在屏幕上打印出第一行:

Hello World!

函數 println 會返回字符串 "Hello World!". 這是它最後一個參數,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 老是會將 list 做爲函數調用進行執行,除非你引用它,從而代表它只是一個字面形式的符號表達式,也就是 -- 數據。

> '(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 語句,那麼它就會返回 nil. nil 的意思根據不一樣的環境可能解釋爲空值(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 成爲一種夢幻般的腳本語言。若是你在看看 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 (empty? a-list) 0
  (+ 1 (list-length (rest a-list)))))

首先,請注意 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> _

函數 set, setq, 和 define 會在當前的 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)

函數 set, setq, 和 define 會綁定名字到當前的名字空間.

> (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 中創建了另一個屬性 radius. RECTANGLE 同時也 "繼承" 了 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 來模擬簡單的對象。

(完)

相關文章
相關標籤/搜索