newLISP — 交互式教程

这份文档于 2006 年 5 月被 Rick Hanson ([email protected]) 做了一些修正和更新后被转换成 html 文档。2008 年 12 月被 L.M 更新到 v.10.0 版本. 版权所有 John W. Small 2004。

你可以到 newLISP 官方网站 www.newLISP.org 下载和安装这门语言。

关于这个教程的任何意见和问题请发邮件到 [email protected]

中文版翻译时,newLISP 的版本已经到了 10.6。这和当时撰写文档的时候,已经相隔甚远。一些内置函数的名称发生了变化,语言的功能也扩展了很多。我根据实际的情况修改了相应的章节,这样所有的代码就都可以在新的版本上进行测试运行了。

中文翻译:宋志泉(ssqq) QQ: 104359176 电子邮件:[email protected]

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")
> _

符号,例如上面的 x 和 three, 还有符号列表(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 总是被完全地计算,但参数 consequence 和 alternative 的表达式是惰性的。因为参数 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 > _

因此下面的符号 hello 和 world 的值分别是 "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!" > _

这次,函数 print 和 println 的副作用在屏幕上显示出来,而且 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 > _

请注意 set 和 setq 都能一次绑定多个关联的符号和值。

> (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)
> _

内置的函数 assoc 和 lookup 提供了操作关联列表的能力。

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

(函数 lookup 还有其他的用途,具体可以查询 newLISP 用户手册。)

请务必注意 assoc 和 lookup 只是返回找到的第一个键 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 > _

请注意 x 和 y 在隐式的符号表中分别绑定了 1 和 2。而 let 表达式在表达式内部范围内暂时的(动态的)再次分别绑定 x 和 y 为 3 和 4。也就是说,let 表达式处理的是一个关联列表,而且按照顺序一个一个的处理里面的绑定表达式。

函数 list 接受多个参数,并且返回这些参数被完全计算后返回的值组成的列表。

let 形式和 begin 形式很像,除了它在 let 块中有一个临时的符号表记录。因为 let 中的表达式参数是惰性的,只在 let 的 context 中被展开。如果我们在 let 块中查看符号表,它看起来就像下面的关联列表:

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

既然 lookup 从左向右查找绑定的 x 和 y 的值,那么就屏蔽了 let 表达式以外的值。当 let 表达式结束后,符号表就会恢复成下面的样子:

'((y 2) (x 1))

离开 let 表达式后,后面对 x 和 y 的计算就会按照它们以前的值进行操作了。

为了让大家看得更清楚,请比较以下的代码:

> (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 表达式,因此它会返回一个匿名的函数,并接收两个参数 1 和 2,并进行计算。

请注意以下两个表达式本质上是做了相同的事情。

> (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

在上面的代码中,参数 1 和 2 是多余的。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 的局部词法作用域中定义的,虽然在里面 x 是 1,y 是 2,但在调用它时,动态作用域机制将发挥作用。所以我们说:在 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。既然符号 x 和 y 出现在函数内部,那么它们就成了局部变量的生命。

(setq x 1 y 2)

上面的赋值语句不会覆盖 lambda 表达式外部的 x 和 y 的定义。

我们也可以用下面的代码声明局部变量来表达相同的效果:

> (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 只调用了一个参数,那么另外的 x 和 y 将分别被默认设置为 1 和2。

宏是用 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

宏会很快变得非常复杂。一个好办法就是用 list 或 println 来替代 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>

内置函数 println 在 MAIN 和 FOO 的命名空间内都能被识别。函数 println 是一种被 "导出" 到全局状态的一个名称。

下面的代码显示出:变量 MAIN:t 不能在命名空间 FOO 或 BAR 中被识别,除非被标记为全局状态。

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 中定义的符号 a 和 b 有效范围同在 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 中的命名空间是 FOO,MAIN: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 中的属性 x 和 y 到 CIRCLE 中,然后在 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,并用下面的代码重写属性 x 和 height 的值。

(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-make 和 context->string 是怎么回事了。不过还是要仔细阅读标准的 newLISP 用户手册来了解其它的一些核心函数,例如 dotree,push, join 等一些在本教程中没有涉及的重要函数。

Common Lisp 和 Scheme 都有标记词法作用域的相关函数,这样就可以构建具有函数功能的闭包。newLISP 中的函数同样可以共享一个具有词法作用域的闭包,这就是 context, 这种机制就好像一个对象的方法共享一个类变量一样。到目前为止的实例代码告诉我们 context 也同时可以容纳函数。newLISP 的手册中有好几个事例代码来演示如何用 context 来模拟简单的对象。

(完)

你可能感兴趣的:(newLISP — 交互式教程)