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 来模拟简单的对象。

(完)

相关文章
相关标签/搜索