跳转到内容

newLISP/print 简介

来自维基教科书,开放世界的开放书籍

newLISP 简介

[编辑 | 编辑源代码]

欢迎使用 newLISP

[编辑 | 编辑源代码]

欢迎阅读本 newLISP 简介!您会发现 newLISP 易于学习且功能强大,它将经典 LISP 的一些强大功能和优雅性与现代脚本语言的功能(如正则表达式、网络功能、Unicode 支持、多任务处理等)结合在一起。

本书是对该语言基础知识的简单直观的描述。您应该熟悉使用文本编辑器,理想情况下,您之前应该做过一些脚本编写,但不需要之前的编程经验或 LISP 知识。我希望这足以让您入门,并准备好开始探索 newLISP 的真正力量。

我在 MacOS X 系统上编写了这篇文章,但如果您使用的是 Linux、Windows 或 newLISP 支持的众多其他平台之一,它不应该有任何区别。这些示例旨在在新版本 10 中运行。

这是一份非官方文档 - 有关 newLISP 的官方和权威描述,请参阅与软件一起安装的出色参考手册。

  • 主要的 newLISP 网站,newlisp.org,提供有用的论坛、代码示例和文档以及最新的 newLISP 软件
  • 约翰·斯莫尔(John Small)在 newLISP in 21 minutes 上发布了精彩的 21 分钟介绍,newLISP in 21 minutes
  • 精美的 newLISP 蜻蜓标志由布莱恩·格雷勒斯(Brian Grayless)设计,由 11 对括号构成(fudnik.com

感谢所有为本文档早期版本做出贡献的人,他们提出了建议或发现了错误。继续加油。

基础知识

[编辑 | 编辑源代码]

下载和安装

[编辑 | 编辑源代码]

安装说明可在 newlisp.org 中找到。

安装完 newLISP 后,您可以通过多种方式运行它。有关完整详细信息,请参阅 newLISP 文档。最直接的方式是通过在命令行(在控制台或终端窗口中)键入 newlisp 命令来运行 newLISP 解释器。

$ newlisp
newLISP v.x on OSX IPv4 UTF-8, execute 'newlisp -h' for more info.

>

这对于尝试短表达式、测试想法和调试非常有用。您可以在此环境中通过将代码行括在 [cmd][/cmd] 之间来编写多行代码。在新版本中,您只需在空白行上使用 回车键 来开始和结束多行块。

>[cmd]
(define (fibonacci n)
 (if (< n 2) 
  1
  (+ (fibonacci (- n 1))
     (fibonacci (- n 2)))))
[/cmd]
>(fibonacci 10)
89
>

newLISP 的可视化界面 newLISP-GS 为 newLISP 应用程序提供了一个图形工具包,并且还为您提供了用于编写和测试代码的开发环境:newLISP 编辑器。在 Windows 上,它安装为桌面图标和“程序启动”菜单中的文件夹。在 MacOS X 上,应用程序包和图标安装在“应用程序”文件夹中。newLISP-GS 编辑器为您提供多个选项卡式窗口、语法着色和用于查看运行代码结果的监视器区域。

the newLISP-GS Editor

您还可以从命令行运行 newLISP-GS 编辑器:您可以在 C:/Program Files/newlisp/newlisp-edit(Windows)或 /usr/bin/newlisp-edit(在 Unix 上)找到该文件。(这是一个 newLISP 源文件,所以您也可以查看代码。)

您可以在您最喜欢的文本编辑器中编辑 newLISP 脚本。在 MacOS X 上,您可以使用 BBEdit、TextWrangler 或 TextMate 运行 newLISP 脚本,或者您可以使用预安装的 Unix 文本编辑器,例如 vim 或 emacs。在 Windows 上,您可以使用 UltraEdit、EditPlus 或 NotePad++,仅举几例。如果您使用的是 Linux,那么您比我更了解文本编辑器,并且您可能已经有了自己的偏好。

newLISP 网站在 http://newlisp.org/index.cgi?Code_Contributions 上托管了一些流行编辑器的配置文件。

在 Unix 上,newLISP 脚本的第一行应该是

#!/usr/local/bin/newlisp

#!/usr/bin/env newlisp

如果您希望在外部文本编辑器中运行 newLISP,您必须使用更多 println 函数来查看每个函数或表达式返回的值。

通常,您使用 exit 函数结束 newLISP 脚本或控制台会话

(exit)

newLISP 的三大基本规则

[编辑 | 编辑源代码]

您只需要学习三条基本规则就可以用 newLISP 编程。以下是第一条规则

规则 1:列表是元素的序列

[edit | edit source]

列表是括号中包含的一系列元素。

(1 2 3 4 5)              ; a list of integers
("the" "cat" "sat")      ; a list of strings
(x y z foo bar)          ; a list of symbol names 
(sin cos tan atan)       ; a list of newLISP functions
(1 2 "stitch" x sin)     ; a mixed list
(1 2 (1 2 3) 3 4 )       ; a list with a list inside it
((1 2) (3 4) (5 6))      ; a list of lists

列表是newLISP中的基本数据结构,也是编写程序代码的方式。但现在先不要输入这些示例 - 还有两条规则需要学习!

规则 2:列表中的第一个元素很特殊

[edit | edit source]

当newLISP看到列表时,它将第一个元素视为函数,然后尝试使用剩余的元素作为函数所需的信息。

(+ 2 2)

这是一个包含三个元素的列表:名为 + 的函数,后面跟着两个数字。当newLISP看到这个列表时,它会对其进行评估并返回 4 的值(当然)。请注意,第一个元素被newLISP视为函数,而其余元素被解释为该函数的参数 - 函数期望的数字。

以下是一些更多示例,演示了这两条规则。

(+ 1 2 3 4 5 6 7 8 9)

返回 45。 + 函数将列表中所有数字加起来。

(max 1 1.2 12.1 12.2 1.3 1.2 12.3)

返回 12.3,列表中最大的数字。同样,列表的长度没有(合理的)限制:如果一个函数可以接受 137 个项目(max+ 都可以),那么你可以传递 137 个项目给它。

(print "the sun has put his hat on")
"the sun has put his hat on"

打印字符 the sun has put his hat on 的字符串。(它也返回字符串,这就是为什么,当你在控制台中工作时,有时你会看到事物重复出现两次)。 print 函数可以打印单个字符字符串,或者你可以提供一系列元素来打印。

(print 1 2 "buckle" "my" "shoe")
12bucklemyshoe

打印两个数字和三个字符串(尽管格式不是很好,因为你还没有遇到 format 函数)。


directory 函数

(directory "/")

生成指定目录的列表,在本例中是根目录 "/"

("." ".." ".DS_Store" ".hotfiles.btree" ".Spotlight-V100" 
".Trashes"".vol" ".VolumeIcon.icns" "Applications" 
"automount" "bin" "cores" "Desktop DB" "Desktop DF" 
"Desktop Folder" "dev""Developer" "etc" "Library" 
"mach" "mach.sym" "mach_kernel" "Network" "private" 
"sbin" "System" "System Folder" "TheVolumeSettingsFolder" 
"tmp" "User Guides And Information" "Users" "usr" 
"var" "Volumes")

如果你没有指定目录,它将列出当前目录。

(directory)
("." ".." "2008-calendar.lsp"  "allelements.lsp" "ansi.lsp" 
"binary-clock.lsp" ... )

有一个 read-file 函数可以读取文本文件的内容。

(read-file "/usr/share/newlisp/modules/stat.lsp")

这里函数需要一个参数 - 文件名 - 并将文件的内容以字符串形式返回给你。

这些是newLISP代码构建块的典型示例 - 一个包含函数调用的列表,后面可能跟着函数所需的任何额外信息。newLISP有超过 380 个函数,你可以参考优秀的newLISP参考手册,详细了解所有函数以及如何使用它们。

你可以尝试这些示例。如果你在终端使用newLISP,只需输入它们即可。如果你将这些行输入文本编辑器并将其作为脚本运行,除非将表达式括在 println 函数中,否则你不会看到函数调用的结果。例如,键入

(println (read-file "/usr/share/newlisp/modules/stat.lsp"))

以打印 read-file 函数的结果。

每个newLISP表达式都会返回一个值。即使 println 函数也会返回一个值。你可以说打印操作实际上只是一个副作用,它的主要任务是返回一个值。你可能注意到,当你在控制台窗口中交互使用 println 时,你会看到返回值两次:一次是在打印时,另一次是在将值返回给调用函数(在本例中是最顶层)时。

在你遇到第三条规则之前,还有一件有用的事情要看看。

嵌套列表

[edit | edit source]

你已经发现了一个列表嵌套在另一个列表中。这是一个例子

(* (+ 1 2) (+ 3 4))

当newLISP看到这一点时,它会这样 思考

嗯,让我们从第一个内部列表开始。我可以做到

(+ 1 2)

很容易。它的值为 3。我也可以做第二个列表

(+ 3 4)

很容易。它评估为 7。

所以,如果我用这些值替换这两个内部列表,我得到

(* 3 7)

这真的很容易。我将为这个表达式返回 21 的值。

(* (+ 1 2) (+ 3 4))
(* 3 (+ 3 4))
(* 3 7)
21

看到第一行末尾的两个右括号,紧随其后是 4 吗?两者都是必不可少的:第一个括号结束 (+ 3 4 列表,第二个括号结束以 (* 开始的乘法运算。当你开始编写更复杂的代码时,你会发现你将列表放在列表中,再放在列表中,再放在列表中,你可能会用六个右括号结束一些更复杂的定义。一个好的编辑器会帮助你跟踪它们。

但你无需担心空白、行终止符、各种标点符号或强制缩进。而且因为所有数据和代码都以相同的方式存储在列表中,所以你可以随意混合它们。稍后将详细介绍。

有些人第一次看到LISP代码时,会担心括号泛滥。其他人称它们为 指甲剪 或者说LISP代表 大量烦人的愚蠢括号。但我更愿意将括号视为包含newLISP 思想 的小 把手

grabbing the handles of a newLISP thought

当你在好的编辑器中编辑newLISP代码时,你可以通过 抓住把手 来轻松地移动或编辑一个想法,并且可以使用平衡括号命令轻松地选择一个想法。你很快就会发现括号比你最初想象的更有用!

引用阻止评估

[edit | edit source]

现在你可以学习使用newLISP编程的第三条规则。

规则 3:引用阻止评估

[edit | edit source]

要阻止newLISP评估某件事,请对其进行引用。

比较这两行

(+ 2 2)
'(+ 2 2)

第一行是一个包含函数和两个数字的列表。在第二行中,列表被引用 - 在前面有一个单引号或撇号(')。你不需要在结束括号后面再加一个引号,因为一个就够了。

> (+ 2 2)
4
> '(+ 2 2)
(+ 2 2)
>

对于第一个表达式,newLISP像往常一样工作,并且热心地评估列表,返回数字 4。但对于第二个表达式,一旦它看到引号,newLISP甚至不会考虑通过加法来评估列表;它只是返回列表,没有评估。

这个引号在newLISP中起着与英文书写中开合引号相同的作用 - 它们通知读者,该词或短语不要按正常方式解释,而要以某种特殊方式处理:非标准或讽刺的含义,也许是另一个人说的话,或者是不应字面理解的东西。

那么,为什么你想要阻止newLISP评估事物呢?你很快就会遇到一些示例,其中你引用事物是为了阻止newLISP认为列表中的第一项是函数。例如,当你将信息存储在列表中时,你不希望newLISP以通常的方式评估它们。

(2006 1 12)                 ; today's year/month/date
("Arthur" "J" "Chopin")     ; someone's full name

你不希望newLISP寻找名为 2006"Arthur" 的函数。此外,2006 不是有效的函数名,因为它以数字开头,函数名不能以双引号开头,因此无论哪种情况,你的程序都会停止并出现错误。因此,你需要 引用 列表,以阻止它们的第一个元素被用作函数而不是数据。

'(2006 1 12)                 ; evaluates to (2006 1 12)
'("Arthur" "J" "Chopin")     ; evaluates to ("Arthur" "J" "Chopin")

newLISP将表达式视为数据 - 以及将数据视为表达式 - 的能力将在稍后详细讨论。

使用垂直撇号(ASCII 代码 39)来引用列表和符号。有时,文本编辑器或其他程序会将这些简单的垂直撇号更改为花括号引号。这些引号不起作用,因此你必须将任何 智能引号 更改为垂直撇号。

符号和引用

[edit | edit source]

符号是newLISP 事物,它有一个名称。你在代码中定义某件事并为其分配一个名称。然后,你可以使用名称而不是内容来引用该事物。例如,在输入以下内容后

(set 'alphabet "abcdefghijklmnopqrstuvwxyz")

现在有一个名为 alphabet 的新符号,其值为由字母表 26 个字母组成的字符串。 set 函数将从 a 到 z 的字符存储在 alphabet 符号中。现在这个符号可以在其他地方使用,并且在使用时会评估为字母表。每当你想要使用字母表 26 个字母时,请使用这个符号,不要对其进行引用。例如,这是 upper-case 函数

(upper-case alphabet)
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"

我使用符号而不引用它,因为我想要newLISP使用符号的值,而不是它的名称。我实际上对将 alphabet 这个词大写并不感兴趣,而是对字母表本身感兴趣。newLISP在本例中没有永久改变符号的值,因为 upper-case 总是创建并返回一个新的字符串,保留存储在符号中的那个字符串不变。

符号对应于其他编程语言中的变量。事实上,newLISP 使用符号的频率并不像其他语言使用变量那样频繁。部分原因是值会不断地由表达式返回,并直接输入到其他表达式中,而不会被存储。例如,在以下代码中,每个函数都将结果直接传递给下一个**包含**函数。

(println (first (upper-case alphabet)))
"A"

**upper-case** 将返回值直接传递给 **first**,**first** 将返回值直接传递给 **println**,**println** 既打印该值,又将它打印的字符串作为返回值提供给你。因此,减少了临时存储值的必要性。但是,在很多其他地方,你确实需要用到符号。

以下再举两个关于符号引用的示例。

(define x (+ 2 2 ))
(define y '(+ 2 2))

在第一个示例中,我没有引用 (+ 2 2) 列表 - newLISP 将其计算为 4,然后将 4 赋值给符号 xx 计算为 4。

x
;-> 4

在第二个示例中,我引用了该列表。这意味着符号 y 现在持有的是列表而不是数字。每当 newLISP 遇到符号 y 时,它都会返回该列表,而不是 4。(当然,除非你也先引用 y!)

y
;-> (+ 2 2)
'y
;-> y

顺便说一下,在本文件中

; the semicolon is the comment character
;-> that ";->" is my way of saying "the value is"

以及 newLISP 解释器打印的输出通常以以下方式显示

like this

设置和定义符号

[edit | edit source]

有几种方法可以创建和设置符号的值。你可以使用 **define** 或 **set**,如下所示。

(set 'x (+ 2 2))
;-> 4
(define y (+ 2 2))
;-> 4

**set** 期望后面跟着一个符号,但会先计算第一个参数。因此,你应该引用符号以防止其被计算(因为它可能计算为除符号以外的值),或者提供一个计算为符号的表达式。**define** 不期望参数被引用。

你也可以使用 **setf** 和 **setq** 来设置符号的值。这些函数期望第一个参数为符号或符号引用,因此你不必引用它。

(setf y (+ 2 2))
;-> 4
(setq y (+ 2 2))
;-> 4

这两个函数(具有相同的作用)可以设置符号(变量)、列表、数组或字符串的内容。习惯上,在设置符号时使用 **setq**,在设置列表或数组元素时使用 **setf**。

**define** 也用于定义函数。请参阅 创建自己的函数

破坏性函数

[edit | edit source]

一些 newLISP 函数会修改其操作的符号的值,而另一些函数会创建值的副本并返回该副本。从技术上讲,那些修改符号内容的函数被称为 **破坏性** 函数 - 尽管你经常会使用它们来创建新数据。在本文件中,我将描述诸如 **push** 和 **replace** 之类的函数为破坏性函数。这仅仅意味着它们更改了某事的值,而不是返回一个修改后的副本。

控制流

[edit | edit source]

有许多不同的方法来控制代码的流程。如果你使用过其他脚本语言,你可能在这里会找到你喜欢的功能,以及更多其他功能。

所有控制流函数都遵循 newLISP 的标准规则。每个函数的一般形式通常是一个列表,其中第一个元素是关键字,后面跟着一个或多个要计算的表达式。

(keyword expression1 expression2 expression3 ...)

测试:if...

[edit | edit source]

也许你在任何语言中能写出的最简单的控制结构就是一个简单的 **if** 列表,它包含一个测试和一个动作。

(if button-pressed? (launch-missile))

第二个表达式,对 launch-missile 函数的调用,只有在符号 button-pressed? 计算为 **true** 时才被计算。1 是 true。0 是 true - 毕竟它是一个数字。-1 是 true。newLISP 已知的绝大多数事物都是 true。newLISP 知道只有两件事是 false 而不是 true:**nil** 和空列表 ()。此外,newLISP 不知道值的任何事物都是 false。

(if x 1)
; if x is true, return the value 1

(if 1 (launch-missile))
; missiles are launched, because 1 is true

(if 0 (launch-missile))
; missiles are launched, because 0 is true

(if nil (launch-missile))
;-> nil, there's no launch, because nil is false

(if '() (launch-missile))
;-> (), and the missiles aren't launched

你可以使用任何计算为 true 或 false 的内容作为测试。

(if (> 4 3) (launch-missile))
;-> it's true that 4 > 3, so the missiles are launched

(if (> 4 3) (println "4 is bigger than 3"))
"4 is bigger than 3"

如果符号计算为 **nil**(也许是因为它不存在或未被赋值),newLISP 将其视为 false,并且测试将返回 **nil**(因为没有提供替代操作)。

(if snark (launch-missile))
;-> nil ; that symbol has no value

(if boojum (launch-missile))
;-> nil ; can't find a value for that symbol

(if untrue (launch-missile))
;-> nil ; can't find a value for that symbol either

(if false (launch-missile))
;-> nil
; never heard of it, and it doesn't have a value

你可以添加第三个表达式,它是 **else** 操作。如果测试表达式计算为 **nil** 或 **()**,则计算第三个表达式,而不是第二个表达式,第二个表达式将被忽略。

(if x 1 2)
; if x is true, return 1, otherwise return 2

(if 1
 (launch-missile)
 (cancel-alert))   
; missiles are launched

(if nil
 (launch-missile) 
 (cancel-alert))
; alert is cancelled

(if false
 (launch-missile) 
 (cancel-alert))
; alert is cancelled

以下是一个典型的实际三部分 **if** 函数,格式化以便尽可能清楚地显示结构。

(if (and socket (net-confirm-request)) ; test
    (net-flush)                            ; action when true
    (finish "could not connect"))          ; action when false

虽然测试后有两个表达式 - (net-flush)(finish ...) - 但只计算其中一个。

如果你不集中精力,你可能会在其他语言中找到的诸如 **then** 和 **else** 之类的熟悉 **指示词** 的缺失会让你措手不及!但是,你可以轻松地添加注释。

你可以将 **if** 与任意数量的测试和操作一起使用。在这种情况下,**if** 列表由一系列测试-操作对组成。newLISP 会依次处理这些对,直到其中一个测试成功,然后计算该测试对应的操作。如果可以,请将列表格式化为多列,以使结构更清晰。

(if
 (< x 0)      (define a "impossible")
 (< x 10)     (define a "small")
 (< x 20)     (define a "medium")
 (>= x 20)    (define a "large")
 )

如果你使用过其他 LISP 方言,你可能会认出这与 **cond**(条件函数)是一个简单的替代方法。newLISP 也提供了传统的 **cond** 结构。请参阅 选择:if、cond 和 case

你可能想知道如果测试成功或不成功,如何执行两个或多个操作。有两种方法可以做到这一点。你可以使用 **when**,它类似于没有 'else' 部分的 **if**。

(when (> x 0)
  (define a "positive")
  (define b "not zero")
  (define c "not negative"))

另一种方法是定义一个表达式块,这些表达式构成一个单一表达式,你可以在 **if** 表达式中使用它。我将在 块:表达式组 中简要讨论如何做到这一点。

之前,我说过当 newLISP 遇到一个列表时,它会将第一个元素视为一个函数。我还应该提到,它会在将第一个元素应用于参数之前先计算第一个元素。

(define x 1)
((if (< x 5) + *) 3 4) ; which function to use, + or *?
7 ; it added

这里,表达式 (if (< x 5) + *) 的第一个元素,根据将 x 与 5 进行比较的测试结果,返回一个算术运算符。因此,整个表达式要么是加法,要么是乘法,具体取决于 x 的值。

(define x 10)
;-> 10

((if (< x 5) + *) 3 4)
12 ; it multiplied

这种技巧可以帮助你编写简洁的代码。与其写成这样:

(if (< x 5) (+ 3 4) (* 3 4))

你可以写成这样:

((if (< x 5) + *) 3 4)

计算结果如下:

((if (< x 5) + *) 3 4)
((if true + *) 3 4)
(+ 3 4)
7

注意每个表达式如何将值返回给包含函数。在 newLISP 中,**每个** 表达式都会返回某个值,即使是 **if** 表达式。

(define x (if flag 1 -1)) ; x is either 1 or -1

(define result
 (if 
     (< x 0)     "impossible" 
     (< x 10)    "small"
     (< x 20)    "medium" 
                 "large"))

x 的值取决于 **if** 表达式返回的值。现在符号 result 包含一个字符串,具体取决于 x 的值。

循环

[edit | edit source]

有时你希望重复执行一系列操作多次,在一个循环中循环执行。有多种可能性。你可能希望对

  • 列表中的每个项目
  • 字符串中的每个项目
  • 一定次数
  • 直到发生某事
  • 在某些条件成立的情况下

newLISP 为所有这些情况(以及更多情况)提供了解决方案。

遍历列表

[edit | edit source]

newLISP 程序员喜欢列表,因此 **dolist** 是一个非常有用的函数,它将一个局部循环符号(变量)依次设置为列表中的每个项目,并在每个项目上运行一系列操作。将循环变量的名称和要扫描的列表放在括号中,放在 **dolist** 之后,然后在后面添加操作。

在以下示例中,我在定义局部循环变量 i 之前,还设置了另一个名为 counter 的符号,i 将保存由 **sequence** 函数生成的数字列表中的每个值。

(define counter 1)
(dolist (i (sequence -5 5))
 (println "Element " counter ": " i)
 (inc counter))                        ; increment counter by 1
Element 1: -5
Element 2: -4
Element 3: -3
Element 4: -2
Element 5: -1
Element 6: 0
Element 7: 1
Element 8: 2
Element 9: 3
Element 10: 4
Element 11: 5


请注意,与 **if** 不同的是,**dolist** 函数和许多其他控制词允许你在一个接一个地编写一系列表达式:这里 **println** 和 **inc**(增量)函数都针对列表中的每个元素被调用。

有一个访问系统维护的循环计数器的有用捷径。我刚刚使用了一个计数器符号,每次循环都进行增量,以跟踪我们在列表中遍历到了什么位置。但是,newLISP 会自动为你维护一个循环计数器,它在一个名为 $idx 的系统变量中,因此我可以省略计数器符号,只需每次循环都检索 $idx 的值。

(dolist (i (sequence -5 5))
 (println "Element " $idx ": " i))
Element 0: -5
Element 1: -4
Element 2: -3
Element 3: -2
Element 4: -1
Element 5: 0
Element 6: 1
Element 7: 2
Element 8: 3
Element 9: 4
Element 10: 5


在某些情况下,你可能更喜欢使用映射函数 **map** 来处理列表(稍后介绍 - 请参阅 应用和映射:将函数应用于列表)。**map** 可以用于将函数(现有的函数或临时定义)应用于列表中的每个元素,而无需使用局部变量遍历列表。例如,让我们使用 **map** 来生成与上述 **dolist** 函数相同的输出。我定义了一个由两个表达式组成的临时 **print and increase** 函数,并将此函数应用于由 **sequence** 生成的列表中的每个元素。

(define counter 1)
(map (fn (i)
       (println "Element " counter ": " i)
       (inc counter))
  (sequence -5 5))

经验丰富的 LISP 程序员可能更熟悉 **lambda**。**fn** 是 **lambda** 的同义词:使用哪一个都无所谓。

你可能还会发现 **flat** 对遍历列表很有用,因为它通过复制来将包含嵌套列表的列表展平,以便更轻松地处理。

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

例如,从

(1 2 3 4 5 6)

变为

例如。请参阅 flat

要遍历传递给函数的参数,可以使用 **doargs** 函数。请参阅 参数:args

遍历字符串

[edit | edit source]

(define alphabet "abcdefghijklmnopqrstuvwxyz")
(dostring (letter alphabet)
    (print letter { }))
97 98 99 100 101 102 103 104 105 106 107 108 109 
110 111 112 113 114 115 116 117 118 119 120 121 122


可以使用类似于 **dolist** 的 **dostring** 来遍历字符串中的每个字符。

一定次数

[edit | edit source]

如果您想执行某个操作固定次数,请使用 dotimesfordotimes 会在列表主体中执行指定次数的操作。您应该为局部变量提供一个名称,就像您在 dolist 中所做的那样。

(dotimes (c 10)
 (println c " times 3 is " (* c 3)))
0 times 3 is 0
1 times 3 is 3
2 times 3 is 6
3 times 3 is 9
4 times 3 is 12
5 times 3 is 15
6 times 3 is 18
7 times 3 is 21
8 times 3 is 24
9 times 3 is 27


您必须为这些形式提供一个局部变量。即使您没有使用它,也必须提供一个。

请注意,计数从 0 开始,并持续到 n - 1,永远不会真正达到指定的值。程序员认为这是合理的和合乎逻辑的;非程序员只需要习惯从 0 开始计数,并指定 10 来获得 0 到 9。

记住这一点的一种方法是想想生日。您在完成第一年时庆祝您的第一个生日,在这段时间里您是 0 岁。您在开始庆祝您的第 10 个生日时完成了您的前 10 年生活,这在您停止成为 9 岁时开始。newLISP 函数 first 获取索引号为 0 的元素……

当您知道重复次数时使用 dotimes,但是当您想要 newLISP 根据开始、结束和步长值计算出应该执行多少次重复时使用 for

(for (c 1 -1 .5)
 (println c))
1
0.5
0
-0.5
-1


这里 newLISP 足够聪明,可以算出我想以 0.5 的步长从 1 降到 -1。

为了再次提醒您 从 0 开始计数,请比较以下两个函数

(for (x 1 10) (println x))
1
...
10
(dotimes (x 10) (println x))
0
...
9

提供了一个逃生路线

[edit | edit source]

fordotimesdolist 喜欢循环,一遍又一遍地执行一组操作。通常,重复会持续下去,直到达到限制 - 最后一个数字或列表中的最后一个项目。但是您可以在测试形式中指定一条紧急逃生路线,该路线将在下一个循环开始之前执行。如果此测试返回 true,则下一个循环不会启动,并且表达式比平时更早结束。这为您提供了一种在正式的最后迭代之前停止的方法。

例如,假设您想要将列表中的每个数字减半,但(出于某种原因)如果其中一个数字是奇数,则想要停止。比较此 dolist 表达式的第一个和第二个版本

(define number-list '(100 300 500 701 900 1100 1300 1500))
; first version
(dolist (n number-list)
 (println (/ n 2)))
50
150
250
350
450
550
650
750
; second version
(dolist (n number-list (!= (mod n 2) 0)) ; escape if true
 (println (/ n 2)))
50
150
250


如果对 n 是否为奇数的测试 (!= (mod n 2) 0) 返回 true,则第二个版本将停止循环。

请注意这里使用了仅限整数的除法。我在示例中使用了 / 而不是浮点除法运算符 div。如果您想要另一个运算符,请不要使用其中一个!

您也可以为 fordotimes 提供逃生路线测试。

对于更复杂的流程控制,您可以使用 catchthrowthrow 将表达式传递给上一个 catch 表达式,该表达式将完成并返回表达式的值。

(catch
 (for (i 0 9)
 (if (= i 5) (throw (string "i was " i)))
 (print i " ")))

输出是

0 1 2 3 4

并且 catch 表达式返回 i was 5

您也可以使用布尔函数设计流程。请参阅 块:表达式组

直到某事发生,或当某事为真时

[edit | edit source]

您可能有一个用于测试某个情况的测试,当发生有趣的事情时,该测试将返回 nil 或 (),否则将返回一个 true 值,您对此不感兴趣。要重复执行一系列操作,直到测试失败,请使用 untildo-until

(until (disk-full?)
 (println "Adding another file")
 (add-file)
 (inc counter))

(do-until (disk-full?)
 (println "Adding another file")
 (add-file)
 (inc counter))

这两者的区别在于何时执行测试。在 until 中,首先进行测试,然后如果测试失败,则评估主体中的操作。在 do-until 中,首先评估主体中的操作,然后进行测试,以查看是否可以进行另一个循环。

这两个代码片段中的哪一个正确?好吧,第一个在添加文件之前测试磁盘的容量,但是第二个使用 do-until 的代码片段,直到添加文件后才检查可用磁盘空间,这不是很谨慎。

whiledo-whileuntildo-until 的互补对立,只要测试表达式保持为真,就重复一个块。

(while (disk-has-space)
 (println "Adding another file")
 (add-file)
 (inc counter))

(do-while (disk-has-space)
 (println "Adding another file")
 (add-file)
 (inc counter))

选择每个的 do- 变体,以便在评估测试之前执行操作块。

块:表达式组

[edit | edit source]

许多 newLISP 控制函数允许您构建一个操作块:一组表达式,这些表达式会一个接一个地依次评估。构造是隐式的:您无需执行任何操作,只需按正确的顺序和位置写入它们即可。查看上面的 whileuntil 示例:每个示例都有三个将依次评估的表达式。

但是,您也可以使用 beginorand 函数显式地创建表达式块。

begin 在您想要显式地将表达式组合在一起形成一个列表时很有用。每个表达式都会依次评估

(begin
 (switch-on)
 (engage-thrusters)
 (look-in-mirror)
 (press-accelerator-pedal)
 (release-brake)
 ; ...
)

等等。您通常只在 newLISP 期望一个表达式时使用 begin。您不需要在 dotimesdolist 构造中使用它,因为这些构造已经允许多个表达式。

块中每个表达式的结果无关紧要,除非它糟糕到足以完全停止程序。返回 nil 是可以的

(begin
 (println "so far, so good")
 (= 1 3)           ; returns nil but I don't care
 (println "not sure about that last result"))
so far, so good
not sure about that last result!


begin 的情况下,块中每个表达式的返回值都会被丢弃;只有最后一个表达式的值会被返回,作为整个块的值。但是对于另外两个 block 函数 andor,返回值很重要且有用。

and 和 or

[edit | edit source]

and 函数遍历表达式块,但如果其中一个表达式返回 nil(false),则立即完成该块。要到达 and 块的末尾,每个表达式都必须返回一个 true 值。如果一个表达式失败,则块的评估将停止,newLISP 会忽略其余表达式,返回 nil,以便您知道它没有正常完成。

这是一个测试 disk-item 是否包含一个有用目录的 and 示例

(and
 (directory? disk-item) 
 (!= disk-item ".") 
 (!= disk-item "..")
 ; I get here only if all tests succeeded
 (println "it looks like a directory")
 )

disk-item 必须通过所有三个测试:它必须是一个目录,它不能是 . 目录,并且它不能是 .. 目录(Unix 术语)。当它成功通过了这三个测试后,评估将继续,并且消息被打印出来。如果其中一个测试失败,则块将完成而不会打印消息。

您也可以对数字表达式使用 and

(and
 (< c 256)
 (> c 32)
 (!= c 48))

这将测试 c 是否在 33 到 255(含)之间,并且不等于 48。这将始终返回 true 或 nil,具体取决于 c 的值。

在某些情况下,and 可以产生比 if 更简洁的代码。您可以使用 and 代替上一页的这个示例

(if (number? x)
 (begin
   (println x " is a number ")
   (inc x)))

使用 and 代替

(and
 (number? x)
 (println x " is a number ")
 (inc x))

您也可以在这里使用 when

(when (number? x)
 (println x " is a number ")
 (inc x))

or 函数比其对应的 and 函数更容易满足。表达式序列会依次评估,直到其中一个返回 true 值。然后会忽略其余表达式。您可以使用它来遍历重要条件列表,其中任何一个失败都足以放弃整个工作。或者,反过来,使用 or 来遍历一个列表,其中任何一个成功都是继续的充分理由。无论如何,请记住,一旦 newLISP 获得一个非 nil 结果,or 函数就会完成。

以下代码设置了一系列条件,每个数字都必须避免满足这些条件 - 只需一个 true 答案,它就不会被打印出来

(for (x -100 100)
 (or  
   (< x 1)              ; x mustn't be less than 1
   (> x 50)             ; or greater than 50
   (> (mod x 3) 0)      ; or leave a remainder when divided by 3
   (> (mod x 2) 0)      ; or when divided by 2
   (> (mod x 7) 0)      ; or when divided by 7
   (println x)))
42                      ; the ultimate and only answer


歧义:amb 函数

[edit | edit source]

您可能会或可能不会找到 amb 的好用途 - 歧义函数。给定列表中的一系列表达式,amb 将选择并评估其中一个表达式,但您事先不知道哪个表达式

> (amb 1 2 3 4 5 6)
3
> (amb 1 2 3 4 5 6)
2
> (amb 1 2 3 4 5 6)
6
> (amb 1 2 3 4 5 6)
3
> (amb 1 2 3 4 5 6)
5
> (amb 1 2 3 4 5 6)
3
>...

使用它随机选择替代操作

(dotimes (x 20)
 (amb
   (println "Will it be me?")
   (println "It could be me!")
   (println "Or it might be me...")))
Will it be me?
It could be me!
It could be me!
Will it be me?
It could be me!
Will it be me?
Or it might be me...
It could be me!
Will it be me?
Will it be me?
It could be me!
It could be me!
Will it be me?
Or it might be me...
It could be me!
...


选择:if、cond 和 case

[edit | edit source]

要测试一系列替代值,您可以使用 ifcondcasecase 函数允许您根据切换表达式的值执行表达式。它由一系列值/表达式对组成

(case n
  (1       (println "un"))
  (2       (println "deux"))
  (3       (println "trois"))
  (4       (println "quatre"))
  (true    (println "je ne sais quoi")))

newLISP 依次遍历这些对,查看 n 是否与 1、2、3 或 4 中的任何值匹配。一旦一个值匹配,表达式就会被评估,并且 case 函数会完成,返回表达式的值。最好将一个最终对与 true 和一个通配表达式一起放在一起,以应对完全没有匹配的情况。如果 n 是一个数字,它将始终为真,因此将此放在最后。

潜在匹配值不会被评估。这意味着你不能写这个

(case n
  ((- 2 1)     (println "un"))
  ((+ 2 0)     (println "deux"))
  ((- 6 3)     (println "trois"))
  ((/ 16 4)    (println "quatre"))
  (true        (println "je ne sais quoi")))

即使从逻辑上来说应该可以:如果 n 是 1,你会期望第一个表达式 (- 2 1) 匹配。但是,该表达式没有被评估 - 没有任何求和被评估。在这个例子中,true 操作 (println "je ne sais quoi") 被评估。

如果你更喜欢使用评估其参数的 case 版本,那么在 newLISP 中很容易实现。请参阅

之前我提到过 condif 的更传统版本。newLISP 中的 cond 语句具有以下结构

(cond
   (test action1 action2 etc) 
   (test action1 action2 etc)
   (test action1 action2 etc)
; ...
   )

其中每个列表都包含一个测试,以及一个或多个表达式或操作,如果测试返回 true 则会评估这些表达式或操作。newLISP 会执行第一个测试,如果测试为 true 则执行操作,然后忽略剩余的测试/操作循环。测试通常是一个列表或列表表达式,但它也可以是一个符号或一个值。

一个典型的例子如下

(cond
  ((< x 0)       (define a "impossible") )
  ((< x 10)      (define a "small")      )
  ((< x 20)      (define a "medium")     )
  ((>= x 20)     (define a "large")      )
 )

这与 if 版本基本相同,只是每对测试-操作都用括号括起来。以下是用于比较的 if 版本

(if
  (< x 0)        (define a "impossible")
  (< x 10)       (define a "small")
  (< x 20)       (define a "medium")
  (>= x 20)      (define a "large")
 )

对于更简单的函数,使用 if 可能更容易。但是,当编写较长的程序时,cond 可能更易读。如果你希望特定测试的操作评估多个表达式,cond 的额外括号可以使你的代码更短

(cond
 ((< x 0)    (define a -1)      ; if would need a begin here
             (println a)        ; but cond doesn't
             (define b -1))
 ((< x 10)   (define a 5))
 ; ...

控制结构中的局部变量

[编辑 | 编辑源代码]

控制函数 dolistdotimesfor 涉及临时局部符号的定义,这些符号在表达式持续期间存在,然后消失。

类似地,使用 letletn 函数,你可以定义仅在列表内部存在的变量。它们在列表之外无效,并且在列表评估完成后,它们会失去其值。

let 列表中的第一个项目是一个子列表,它包含变量(不需要引用)以及用于初始化每个变量的表达式。列表中的剩余项目是可访问这些变量的表达式。最好将变量/起始值对对齐

(let
   (x (* 2 2)
    y (* 3 3)
    z (* 4 4)) 
   ; end of initialization
 (println x)
 (println y)
 (println z))
4
9
16

此示例创建了三个局部变量 xyz,并为每个变量分配了值。主体包含三个 println 表达式。在这些表达式完成后,xyz 的值将不再可访问 - 尽管整个表达式返回了最后一个 println 语句返回的值 16。

如果你将 let 列表想象成在一行上,那么它的结构很容易记住

(let ((x 1) (y 2)) (+ x y))

如果你想在第一个初始化部分中的其他地方引用局部变量,请使用 letn 而不是 let

(letn
   (x 2
    y (pow x 3)
    z (pow x 4))
 (println x)
 (println y)
 (println z))

y 的定义中,你可以引用 x 的值,我们刚刚将其定义为 2。letnlet 的嵌套版本,它允许你执行此操作。

我们对局部变量的讨论引出了函数。

创建你自己的函数

[编辑 | 编辑源代码]

define 函数提供了一种方法来将表达式列表存储在一个名称下,适用于以后运行。你定义的函数可以使用与 newLISP 的内置函数相同的方式。函数定义的基本结构如下

(define (func1)
 (expression-1)
 (expression-2) 
; ...
 (expression-n)
)

当你不想向函数提供任何信息时,或者当你需要提供信息时,可以使用以下结构

(define (func2 v1 v2 v3)
 (expression-1)
 (expression-2) 
; ...
 (expression-n)
)

你可以像调用任何其他函数一样调用新定义的函数,如果你的定义需要参数,则在列表中向其传递值

(func1) ; no values expected
(func2 a b c) ; 3 values expected

我说预期,但 newLISP 很灵活。你可以向 func1 提供任意数量的参数,newLISP 不会抱怨。你也可以向 func2 提供任意数量的参数 - 在这种情况下,如果提供的参数不足以定义 abc,它们在开始时将被设置为 nil

当函数运行时,主体中的每个表达式都会按顺序进行评估。最后一个被评估的表达式的值将作为函数的值返回。例如,此函数根据 n 的值返回 truenil

(define (is-3? n)
 (= n 3))
> (println (is-3? 2))
nil
> (println (is-3? 3))
true

有时,你可能希望通过在末尾添加一个评估为正确值的表达式来显式指定要返回的值

(define (answerphone)
 (pick-up-phone)
 (say-message)
 (set 'message (record-message))
 (put-down-phone)
 message)

末尾的 message 评估为 (record-message) 收到的并返回的消息,并且 (answerphone) 函数返回此值。如果没有它,函数将返回 (put-down-phone) 返回的值,这可能只是一个 true 或 false 值。

要使函数返回多个值,你可以返回一个列表。

在函数的参数列表中定义的符号对函数来说是局部的,即使它们之前在函数之外存在

(set 'v1 999)
(define (test v1 v2)
  (println "v1 is " v1) 
  (println "v2 is " v2)
  (println "end of function"))
(test 1 2)
v1 is 1
v2 is 2
end of function
> v1
999

如果符号在函数体中定义如下

(define (test v1)
  (set 'x v1)
  (println x))
(test 1)
1
> x
1

它也可以从函数外部访问。这就是你为什么要定义局部变量的原因!请参阅 局部变量

newLISP 足够智能,不会担心你是否提供了超过所需的信息

(define (test)
  (println "hi there"))
(test 1 2 3 4) ; 1 2 3 4 are ignored
hi there

但它不会为你填补空白

(define (test n)
  (println n))
>(test)                                  ; no n supplied, so print nil
nil
> (test 1)
1
> (test 1 2 3)                            ; 2 and 3 ignored<
1

局部变量

[编辑 | 编辑源代码]

有时,你希望函数改变代码中其他地方的符号的值,有时你希望函数不改变 - 或者不能改变。以下函数在运行时会更改 x 符号的值,该符号可能在代码中的其他地方定义,也可能没有定义

(define (changes-symbol)
 (set 'x 15)
 (println "x is " x))
 
(set 'x 10)
;-> x is 10

(changes-symbol)
x is 15

如果你不希望发生这种情况,请使用 letletn 来定义一个局部 x,它不会影响函数外部的 x 符号

(define (does-not-change-x)
 (let (x 15)
    (println "my x is " x)))
 
(set 'x 10)
> (does-not-change-x)
my x is 15
> x
10


x 在函数外部仍然是 10。函数内部的 x 与函数外部的 x 不同。当你使用 set 更改函数内部的局部 x 的值时,它不会更改任何函数外部的 x

(define (does-not-change-x)
 (let (x 15)            ; this x is inside the 'let' form
  (set 'x 20)))
>(set 'x 10)             ; this x is outside the function
10
> x
10
> (does-not-change-x)
> x
10

你可以使用 local 函数来代替 letletn。它与 letletn 相似,但你不需要在首次提及局部变量时为它们提供任何值。它们只是 nil,直到你设置它们

(define (test)
 (local (a b c)
   (println a " " b " " c)
   (set 'a 1 'b 2 'c 3)
   (println a " " b " " c)))
(test)
nil nil nil
1 2 3


还有其他方法可以声明局部变量。当你定义自己的函数时,你可能会发现以下技术更容易编写。注意逗号

(define (my-function x y , a b c)
; x y a b and c are local variables inside this function
; and 'shadow' any value they might have had before
; entering the functions

逗号是一个巧妙的技巧:它是一个普通的符号名称,如 cx

(set ', "this is a string")
(println ,)
"this is a string"

- 它只是不太可能被用作符号名称,因此它在参数列表中作为视觉分隔符很有用。

默认值

[编辑 | 编辑源代码]

在函数定义中,你可以在函数参数列表中定义的局部变量可以分配默认值,如果你在调用函数时没有指定值,将使用这些默认值。例如,这是一个具有三个命名参数 abc 的函数

(define (foo (a 1) b (c 2))
 (println a " " b " " c))

如果你在函数调用中没有提供值,则符号 ac 将分别取值 1 和 2,但 b 将为 nil,除非你为它提供值。

> (foo)         ; there are defaults for a and c but not b

1 nil 2

 (foo 2) ; no values for b or c; c has default

2 nil 2

  > (foo 2 3) ; b has a value, c uses default

2 3 2

  > (foo 3 2 1) ; default values not needed

3 2 1

 > 


参数:args

[编辑 | 编辑源代码]

你可以看到,newLISP 对函数参数采用了非常灵活的方法。你可以编写接受任意数量参数的定义,从而为你(或你的函数的调用者)提供最大的灵活性。

args 函数返回传递给函数的任何未使用的参数

(define (test v1)
 (println "the arguments were " v1 " and " (args)))

(test)
the arguments were nil and ()
(test 1)
the arguments were 1 and ()
(test 1 2 3)
the arguments were 1 and (2 3)
(test 1 2 3 4 5)
the arguments were 1 and (2 3 4 5)

请注意,v1 包含传递给函数的第一个参数,但任何剩余的未使用的参数都包含在 (args) 返回的列表中。

使用 args,你可以编写接受不同类型输入的函数。请注意以下函数如何在没有参数的情况下调用,使用字符串参数调用,使用数字调用,或者使用列表调用

(define (flexible)
 (println " arguments are " (args))
 (dolist (a (args))
  (println " -> argument " $idx " is " a)))

(flexible)
arguments are ()
(flexible "OK")
 arguments are ("OK")
 -> argument 0 is OK
(flexible 1 2 3)
 arguments are (1 2 3)
 -> argument 0 is 1
 -> argument 1 is 2
 -> argument 2 is 3
(flexible '(flexible 1 2 "buckle my shoe"))
 arguments are ((flexible 1 2 "buckle my shoe"))
 -> argument 0 is (flexible 1 2 "buckle my shoe")

args 允许你编写接受任意数量参数的函数。例如,newLISP 非常乐意让你向一个定义合适的函数传递一百万个参数。我试过了

(define (sum)
  (apply + (args)))

(sum 0 1 2 3 4 5 6 7 8
; ...
 999997 999998 999999 1000000)
; all the numbers were there but they've been omitted here
; for obvious reasons...
;-> 500000500000

实际上,newLISP 对此感到满意,但我的文本编辑器不满意。

doargs 函数可以代替 dolist 用于遍历 args 返回的参数。你可以将 flexible 函数编写为

(define (flexible)
 (println " arguments are " (args))
 (doargs (a)                            ; instead of dolist
  (println " -> argument " $idx " is " a)))

newLISP 还有更多方法可以控制代码执行的流程。除了 catchthrow(它们允许你处理和捕获错误和异常)之外,还有 silent,它就像 begin 的静默版本。

如果您想要更多,您可以使用 newLISP 宏编写您自己的语言关键字,这些宏可以用与使用内置函数相同的方式使用。请参阅

考虑这个函数

(define (show)
  (println "x is " x))

请注意,此函数引用了某个未指定的符号 x。此符号在定义或调用函数时可能存在也可能不存在,并且可能存在值也可能不存在。当此函数被评估时,newLISP 会查找称为 x最近符号,并找到它的值

(define (show)
  (println "x is " x))

(show)
x is nil
(set 'x "a string")
(show)
x is a string
(for (x 1 5)
  (dolist (x '(sin cos tan))
    (show))
  (show))
x is sin
x is cos
x is tan
x is 1
x is sin
x is cos
x is tan
x is 2
x is sin
x is cos
x is tan
x is 3
x is sin
x is cos
x is tan
x is 4
x is sin
x is cos
x is tan
x is 5


(show)
x is a string


(define (func x)
  (show))

(func 3)
x is 3
(func "hi there")
x is hi there


(show)
x is a string


您可以看到 newLISP 如何通过动态跟踪哪个 x 是活动的来始终为您提供当前 x 的值,即使可能存在其他x潜伏在后台。for 循环开始时,循环变量 x 接管为当前 x,但随后该 x 立即被列表迭代变量 x 取代,该变量取几个三角函数的值。在每组三角函数之间,循环变量版本的 x 会短暂地弹出。经过所有这些迭代后,字符串值将再次可用。

func 函数中,还有一个 x 对函数是本地的。调用 show 时,它将打印此局部符号。对 show 的最后一次调用将返回 x 最初拥有的第一个值。

虽然 newLISP 不会对所有这些不同的 x感到困惑,但您可能会感到困惑!因此,最好使用更长、更具解释性的符号名称,并使用局部变量而不是全局变量。如果您这样做,您犯错或在以后的日期误读代码的可能性会更小。一般来说,除非您确切地知道符号的来源以及如何确定其值,否则在函数中引用未定义的符号不是一个好主意。

这种动态跟踪符号当前版本的进程称为动态作用域。当您查看上下文(上下文)时,将有更多关于此主题的信息。它们提供了一种组织类似名称符号的替代方法 - 词法作用域。

列表在 newLISP 中无处不在 - LISP 代表列表处理 - 因此,存在许多用于处理列表的有用函数并不奇怪。将它们全部组织成一个逻辑的描述性叙述相当困难,但这里介绍了其中大多数。

newLISP 的一个好处是,许多在列表上工作的函数也适用于字符串,因此您将在下一章中遇到很多这些函数,它们将应用于字符串。

构建列表

[编辑 | 编辑源代码]

您可以直接构建列表并将其分配给符号。引用列表以停止立即对其进行评估

(set 'vowels '("a" "e" "i" "o" "u"))
;-> ("a" "e" "i" "o" "u") ; an unevaluated list

列表通常由其他函数为您创建,但您也可以使用以下函数之一构建自己的列表

  • list 根据表达式创建新列表
  • append 将列表粘合在一起以形成一个新列表
  • cons 将元素添加到列表的开头或创建列表
  • push 在列表中插入新成员
  • dup 复制元素

list、append 和 cons

[编辑 | 编辑源代码]

使用 list 根据表达式序列构建列表

(set 'rhyme (list 1 2 "buckle my shoe" 
    '(3 4) "knock" "on" "the" "door"))
; rhyme is now a list with 8 items"
;-> (1 2 "buckle my shoe" '(3 4) "knock" "on" "the" "door")

请注意,(3 4) 元素本身就是一个列表,它嵌套在主列表中。

cons 接受正好两个表达式,并且可以执行两项工作:将第一个元素插入现有列表的开头,或构建一个新的两个元素列表。在这两种情况下,它都会返回一个新的列表。newLISP 会根据第二个元素是列表还是不是自动选择要执行的工作。

(cons 1 2)                           ; makes a new list
;-> (1 2)

(cons 1 '(2 3))                      ; inserts an element at the start
;-> (1 2 3)

要将两个或多个列表粘合在一起,请使用 append

(set 'odd '(1 3 5 7) 'even '(2 4 6 8))
(append odd even)
;-> (1 3 5 7 2 4 6 8)

请注意 listappend 在您连接两个列表时的区别

(set 'a '(a b c) 'b '(1 2 3))

(list a b)
;-> ((a b c) (1 2 3))                   ; list makes a list of lists

(append a b)
;-> (a b c 1 2 3)                       ; append makes a list

list 在创建新列表时保留源列表,而 append 使用每个源列表的元素创建一个新列表。

要记住这一点:List 保留源列表的 List 性,但 aPPend 会将元素挑选出来,并将它们全部打包在一起。

append 还可以将一堆字符串组装成一个新的字符串。

push:将项目推入列表

[编辑 | 编辑源代码]

push 是一个强大的命令,您可以使用它来创建新列表或将元素插入现有列表的任何位置。在列表的开头推送元素会将所有内容向右移动一位,而在列表的末尾推送元素只会将其附加并创建一个新的最后一个元素。您还可以在列表的中间的任何位置插入元素。

尽管它的性质是建设性的,但从技术上讲它是一个破坏性函数,因为它会永久更改目标列表,因此请谨慎使用。它返回插入的元素的值,而不是整个列表。

(set 'vowels '("e" "i" "o" "u"))
(push (char 97) vowels)
; returns "a"
; vowels is now ("a" "e" "i" "o" "u")

当您引用列表中元素的位置时,您使用的是基于零的编号,如果您是一位经验丰富的程序员,您会期望这样做

index numbering of list elements

如果您没有指定位置或索引,push 会在开头推送新元素。使用第三个表达式来指定新元素的位置或索引。-1 表示列表的最后一个元素,1 表示从 0 开始从前面数的列表的第二个元素,依此类推

(set 'vowels '("a" "e" "i" "o"))
(push "u" vowels -1)
;-> "u"
; vowels is now ("a" "e" "i" "o" "u")

(set 'evens '(2 6 10))
(push 8 evens -2)                       ; goes before the 10
(push 4 evens 1)                        ; goes after the 2

; evens is now (2 4 6 8 10)

如果您提供的符号作为列表不存在,push 会有用地为您创建它,因此您不必先声明它。

(for (c 1 10)
 (push c number-list -1)                ; doesn't fail first time!
 (println number-list))
(1)
(1 2)
(1 2 3)
(1 2 3 4)
(1 2 3 4 5)
(1 2 3 4 5 6)
(1 2 3 4 5 6 7)
(1 2 3 4 5 6 7 8)
(1 2 3 4 5 6 7 8 9)
(1 2 3 4 5 6 7 8 9 10)


顺便说一句,还有很多其他方法可以生成一个无序数字列表。您还可以执行许多随机交换,如下所示

(set 'l (sequence 0 99))
(dotimes (n 100)
  (swap (l (rand 100)) (l (rand 100)))))

虽然使用 randomize 会更容易

(randomize (sequence 1 99))
;-> (54 38 91 18 76 71 19 30 ...

(这就是 newLISP 的好处之一 - 更优雅的解决方案只需重新编写!)

push 有一个相反的函数 pop,它会破坏性地从列表中删除元素,并返回删除的元素。我们将在后面介绍 pop 和其他列表手术函数。请参阅 列表手术

这两个函数,与许多其他 newLISP 函数一样,适用于字符串和列表。请参阅 push 和 pop 也适用于字符串

dup:构建重复元素的列表

[编辑 | 编辑源代码]

一个称为 dup 的有用函数允许您通过重复元素给定次数来快速构建列表

(dup 1 6)             ; duplicate 1 six times
;-> (1 1 1 1 1 1)

(dup '(1 2 3) 6)
;-> ((1 2 3) (1 2 3) (1 2 3) (1 2 3) (1 2 3) (1 2 3))

(dup x 6)
;-> (x x x x x x)

有一个技巧可以使 dup 返回一个字符串列表。由于 dup 也可以用于将字符串复制成一个较长的字符串,因此您在列表的末尾提供额外的 true 值,newLISP 会创建一个字符串列表,而不是一个字符串的字符串。

(dup "x" 6)           ; a string of strings
;-> "xxxxxx"

(dup "x" 6 true)      ; a list of strings
;-> ("x" "x" "x" "x" "x" "x")

使用整个列表

[编辑 | 编辑源代码]

获得列表后,就可以开始处理它。首先,让我们看一下对列表作为单位进行操作的函数。之后,我将介绍允许您执行列表手术的函数 - 对单个列表元素的操作。

使用和处理列表

[编辑 | 编辑源代码]

dolist 遍历列表中的每个项目

(set 'vowels '("a" "e" "i" "o" "u"))
(dolist (v vowels)
  (println (apply upper-case (list v))))
A
E
I
O
U


在这个例子中,apply 函数需要一个函数和一个列表,并使用列表中的元素作为函数的参数。因此,它重复地将upper-case 函数应用于循环变量在v 中的值。由于upper-case 函数适用于字符串,但apply 函数需要一个列表,所以我不得不使用list 函数将每次迭代中v 的当前值(一个字符串)转换为列表。

一个更好的方法是使用map 函数。

(map upper-case '("a" "e" "i" "o" "u"))
;-> ("A" "E" "I" "O" "U")

map 函数将指定的函数(本例中为upper-case 函数)依次应用于列表中的每个项目。map 函数的优点是它可以在一次遍历中同时遍历列表并将函数应用于列表中的每个项目。结果也是一个列表,这在后续处理中可能更有用。

关于dolistapply 函数的更多信息,请参见其他地方(请参见 遍历列表Apply 和 map: 将函数应用于列表)。

reverse

[edit | edit source]

reverse 函数的作用与你预期的一样,它会反转列表。它是一个破坏性函数,会永久改变列表。

(reverse '("A" "E" "I" "O" "U"))
;-> ("U" "O" "I" "E" "A")

sort 和 randomize

[edit | edit source]

在某种程度上,randomize 函数和sort 函数是互补的,尽管sort 函数会改变原始列表,而randomize 函数会返回原始列表的无序副本。sort 函数会将列表中的元素按升序排列,根据类型和值进行组织。

以下是一个示例:创建一个字母列表和一个数字列表,将它们组合在一起,随机排列结果,然后再次排序。

(for (c (char "a") (char "z"))
 (push (char c) alphabet -1))

(for (i 1 26) 
 (push i numbers -1))

(set 'data (append alphabet numbers))

;-> ("a" "b" "c" "d" "e" "f" "g" "h" "i" "j" "k" "l" "m" "n" "o" "p"
; "q" "r" "s" "t" "u" "v" "w" "x" "y" "z" 1 2 3 4 5 6 7 8 9 10 11 12
; 13 14 15 16 17 18 19 20 21 22 23 24 25 26)

(randomize data)

;-> ("l" "r" "f" "k" 17 10 "u" "e" 6 "j" 11 15 "s" 2 22 "d" "q" "b" 
; "m" 19 3 5 23 "v" "c" "w" 24 13 21 "a" 4 20 "i" "p" "n" "y" 14 "g" 
; 25 1 8 18 12 "o" "x" "t" 7 16 "z" 9 "h" 26)

(sort data)

;-> (1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
; 25 26 "a" "b" "c" "d" "e" "f" "g" "h" "i" "j" "k" "l" "m" "n" "o"
; "p" "q" "r" "s" "t" "u" "v" "w" "x" "y" "z")

比较data 在随机化之前和排序之后的差异。sort 命令会根据数据类型和值对列表进行排序:整数在字符串之前,字符串在列表之前,依此类推。

默认排序方法为<,它会将值排列成每个值都小于下一个值。

若要更改排序方法,可以提供一个 newLISP 内置的比较函数,例如>。当比较函数对相邻的每对都为真时,认为相邻的元素是按正确顺序排序的。

(for (c (char "a") (char "z")) 
  (push (char c) alphabet -1)) 

alphabet
;-> ("a" "b" "c" "d" "e" "f" "g" "h" "i" "j" "k" "l" "m" "n" 
; "o" "p" "q" "r" "s" "t" "u" "v" "w" "x" "y" "z")

(sort alphabet >)
;-> ("z" "y" "x" "w" "v" "u" "t" "s" "r" "q" "p" "o" "n" 
; "m" "l" "k" "j" "i" "h" "g" "f" "e" "d" "c" "b" "a")

你可以提供一个自定义排序函数。这是一个函数,它接受两个参数,如果它们按正确的顺序(即第一个应该在第二个之前),则返回 true,否则返回 false。例如,假设你想要对一个文件名列表进行排序,以便最短的文件名出现在最前面。定义一个函数,如果第一个参数比第二个参数短,则返回 true,然后将这个自定义排序函数与sort 函数一起使用。

(define (shorter? a b)        ; two arguments, a and b 
 (< (length a) (length b)))

(sort (directory) shorter?)
;->
("." ".." "var" "usr" "tmp" "etc" "dev" "bin" "sbin" "mach" ".vol" 
"Users" "cores" "System" "Volumes" "private" "Network" "Library" 
"mach.sym" ".Trashes" "Developer" "automount" ".DS_Store" 
"Desktop DF" "Desktop DB" "mach_kernel" "Applications" "System Folder" ...)


经验丰富的 newLISP 用户通常会编写一个无名函数,并将其直接提供给sort 函数。

(sort (directory) (fn (a b) (< (length a) (length b))))

这与前面的方法一样,但可以节省大约 25 个字符。你可以使用fnlambda 定义内联函数或匿名函数。

unique

[edit | edit source]

unique 函数会返回一个列表的副本,其中删除了所有重复项。

(set 'data '( 1 1 2 2 2 2 2 2 2 3 2 4 4 4 4))
(unique data)
;-> (1 2 3 4)

还有一些用于比较列表的有用函数。请参见 使用两个或更多个列表

flat 函数在处理嵌套列表时很有用,因为它可以显示嵌套列表的样子,而无需复杂的层次结构。

(set 'data '(0 (0 1 2) 1 (0 1) 0 1 (0 1 2) ((0 1) 0))) 
(length data)
;-> 8
(length (flat data))
;-> 15
(flat data)
;-> (0 0 1 2 1 0 1 0 1 0 1 2 0 1 0)

幸运的是,flat 函数是非破坏性的,因此你可以使用它而无需担心丢失嵌套列表的结构。

data
;-> (0 (0 1 2) 1 (0 1) 0 1 (0 1 2) ((0 1) 0)) ; still nested

transpose

[edit | edit source]

transpose 函数设计用于处理矩阵(一种特殊的列表类型:请参见 矩阵)。它对普通的嵌套列表也有用。如果你将列表的列表看作一张表格,它会为你翻转行和列。

(set 'a-list 
 '(("a" 1) 
   ("b" 2) 
   ("c" 3)))

(transpose a-list)

;->
(("a" "b" "c")
 ( 1 2 3))

(set 'table 
'((A1 B1 C1 D1 E1 F1 G1 H1)
  (A2 B2 C2 D2 E2 F2 G2 H2)
  (A3 B3 C3 D3 E3 F3 G3 H3)))

(transpose table)

;->
((A1 A2 A3)
 (B1 B2 B3)
 (C1 C2 C3)
 (D1 D2 D3)
 (E1 E2 E3)
 (F1 F2 F3) 
 (G1 G2 G3) 
 (H1 H2 H3))

以下是一个 newLISP 代码示例:

(set 'table '((A 1) (B 2) (C 3) (D 4) (E 5)))
;-> ((A 1) (B 2) (C 3) (D 4) (E 5))

(set 'table (transpose (rotate (transpose table))))
;-> ((1 A) (2 B) (3 C) (4 D) (5 E))

每个子列表都被反转了。当然,你也可以这样做:

(set 'table (map (fn (i) (rotate i)) table))

这更短,但速度略慢。

explode

[edit | edit source]

explode 函数可以让你将列表分解。

(explode (sequence 1 10))
;-> ((1) (2) (3) (4) (5) (6) (7) (8) (9) (10))

你也可以指定每个部分的大小。

(explode (sequence 1 10) 2)
;-> ((1 2) (3 4) (5 6) (7 8) (9 10))

(explode (sequence 1 10) 3)
;-> ((1 2 3) (4 5 6) (7 8 9) (10))

(explode (sequence 1 10) 4)
;-> ((1 2 3 4) (5 6 7 8) (9 10))

列表分析:测试和搜索

[edit | edit source]

通常你不知道列表中包含什么,并且你想要一些取证工具来了解更多关于列表内部的信息。newLISP 提供了很好的选择。

我们已经接触过length 函数,它可以查找列表中元素的数量。

starts-with 函数和ends-with 函数可以测试列表的开头和结尾。

(for (c (char "a") (char "z")) 
 (push (char c) alphabet -1))

;-> alphabet is ("a" "b" "c" "d" "e" "f" "g" "h" "i" "j" "k" "l"
; "m" "n" "o" "p" "q" "r" "s" "t" "u" "v" "w" "x" "y" "z")

(starts-with alphabet "a")             ; list starts with item "a"?
;-> true

(starts-with (join alphabet) "abc")    ; convert to string and test
;-> true

(ends-with alphabet "z")               ; testing the list version
;-> true

这些函数也可以用于字符串(它们接受正则表达式)。请参见 测试和比较字符串

contains 函数呢?实际上,newLISP 没有一个单独的函数可以完成这项工作。相反,你拥有findmatchmemberreffilterindexcount 函数等。你使用哪个函数取决于你想对问题“这个列表是否包含这个项目?”得到什么答案,以及列表是嵌套列表还是扁平列表。

如果你想要一个简单的答案,并且只需要快速地进行顶层搜索,请使用find 函数。请参见 find 函数。

如果你还想得到项目和列表的剩余部分,请使用member 函数。请参见 member 函数。

如果你想要第一个匹配项的索引号(即使列表包含嵌套列表),你可以使用ref 函数。请参见 ref 和 ref-all 函数。

如果你想要一个包含所有与你的搜索项匹配的元素的新列表,请使用find-all 函数。请参见 find-all 函数。

如果你想要知道列表是否包含某个元素模式,请使用match 函数。请参见 匹配列表中的模式

你可以使用filtercleanindex 函数找到满足某个函数(内置函数或自定义函数)的所有列表项。请参见 过滤列表:filter、clean 和 index 函数。

exists 函数和for-all 函数检查列表中的元素,查看它们是否通过了测试。

如果你只是为了将列表中的元素更改为其他东西,而不是为了查找它们,那么不要先查找它们,直接使用replace 函数即可。请参见 替换信息:replace 函数。你也可以使用set-ref 函数查找和替换列表元素。请参见 查找和替换匹配的元素 函数。

如果你想要知道列表中某个项目的出现次数,请使用count 函数。请参见 使用两个或更多个列表 函数。

让我们看一些这些函数的示例。

find 函数会在列表中查找一个表达式,并返回一个整数或nil。整数表示搜索项在列表中第一次出现的索引。find 函数可能返回 0,如果列表以该项开头,则它的索引号为 0,但这并不成问题——你可以在if 测试中使用此函数,因为 0 会被评估为 true。

(set 'sign-of-four 
 (parse (read-file "/Users/me/Sherlock-Holmes/sign-of-four.txt")
 {\W} 0))

(if  (find "Moriarty" sign-of-four)        ; Moriarty anywhere?
  (println "Moriarty is mentioned")
  (println "No mention of Moriarty"))
No mention of Moriarty 


(if (find "Lestrade" sign-of-four)
  (println "Lestrade is mentioned") 
  (println "No mention of Lestrade"))
Lestrade is mentioned.
(find "Watson" sign-of-four)
;-> 477

这里,我解析了亚瑟·柯南·道尔的《四签名》(你可以从古腾堡计划下载),并测试了生成的字符串列表是否包含各种名称。返回的整数表示该字符串元素在列表中第一次出现的索引。

find 函数允许你使用正则表达式,因此你可以在列表中找到与某个字符串模式匹配的任何字符串元素。

(set 'loc (find "(tea|cocaine|morphine|tobacco)" sign-of-four 0)) 
(if loc
  (println "The drug " (sign-of-four loc) " is mentioned.")
  (println "No trace of drugs"))
The drug cocaine is mentioned.

这里,我正在寻找夏洛克·福尔摩斯波西米亚式生活方式中任何化学放纵的痕迹:"(tea|cocaine|morphine|tobacco)" 表示茶、可卡因、吗啡或烟草中的任何一种。

这种形式的find 函数允许你在列表的字符串元素中查找正则表达式模式。当我们探索字符串时,你将会再次遇到正则表达式。请参见 正则表达式

(set 'word-list '("being" "believe" "ceiling" "conceit" "conceive" 
"deceive" "financier" "foreign" "neither" "receive" "science" 
"sufficient" "their" "vein" "weird"))

(find {(c)(ie)(?# i before e except after c...)} word-list 0)
;-> 6                                   ; the first one is "financier"

这里,我们正在查找词语列表中与我们的模式匹配的任何字符串元素(一个c 紧跟着ie,这是古老且不准确的拼写规则“i before e except after c”。

这里的正则表达式模式(包含在括号中,括号是字符串分隔符,与引号的功能大致相同)是 (c) 紧跟着 (ie)。然后是一个注释,以(?# 开头。正则表达式中的注释在事情变得难以理解时很有用,因为它们经常会变得难以理解。

find 函数还可以接受一个比较函数。请参见 搜索列表 函数。

find 函数只查找列表中的第一个匹配项。若要查找所有匹配项,你可以重复find 函数,直到它返回nil。词语列表每次都会变短,找到的元素会被添加到另一个列表的末尾。

(set 'word-list '("scientist" "being" "believe" "ceiling" "conceit" 
"conceive" "deceive" "financier" "foreign" "neither" "receive" "science" 
"sufficient" "their" "vein" "weird"))

(while (set 'temp 
 (find {(c)(ie)(?# i before e except after c...)} word-list 0))
   (push (word-list temp) results -1)
   (set 'word-list ((+ temp 1) word-list)))

results
;-> ("scientist" "financier" "science" "sufficient")

但在这种情况下,使用filter 函数要容易得多。

(filter (fn (w) (find {(c)(ie)} w 0)) word-list)
;-> ("scientist" "financier" "science" "sufficient")

- 请参见 过滤列表:filter、clean 和 index 函数。

或者,你可以使用ref-all 函数(请参见 ref 和 ref-all 函数)来获取索引列表。

如果你不使用正则表达式,你可以使用count 函数,它在给定两个列表的情况下,会遍历第二个列表,并计算第一个列表中的每个项目在第二个列表中出现的次数。让我们看看主要人物的名字出现了多少次。

(count '("Sherlock" "Holmes" "Watson" "Lestrade" "Moriarty" "Moran")
 sign-of-four)
;-> (34 135 24 1 0 0)

count 函数产生的结果列表显示了第一个列表中的每个元素在第二个列表中出现的次数,因此在这个故事中,Sherlock 被提及了 34 次,Holmes 被提及了 135 次,Watson 被提及了 24 次,而可怜的莱斯特雷德警探只被提及了一次。

值得注意的是,find 函数只会对列表进行表面检查。例如,如果列表包含嵌套列表,则应使用 ref 而不是 find,因为 ref 会查看子列表内部。

(set 'maze 
 '((1 2)
   (1 2 3)
   (1 2 3 4)))

(find 4 maze)
;-> nil                           ; didn't look inside the lists

(ref 4 maze)
;-> (2 3)                         ; element 3 of element 2

member

[edit | edit source]

member 函数返回源列表的剩余部分,而不是索引号或计数。

(set 's (sequence 1 100 7))             ; 7 times table?
;-> (1 8 15 22 29 36 43 50 57 64 71 78 85 92 99)

(member 78 s)
;-> (78 85 92 99)

匹配列表中的模式

[edit | edit source]

有一个强大的复杂函数叫做 match,它在列表中寻找模式。它接受通配符 *、? 和 +,用于定义元素的模式。+ 表示一个或多个元素,* 表示零个或多个元素,? 表示一个元素。例如,假设你想要在一个包含 0 到 9 之间的随机数字列表中寻找模式。首先,生成一个包含 10000 个随机数的列表作为源数据。

(dotimes (c 10000) (push (rand 10) data))
;-> (7 9 3 8 0 2 4 8 3 ...)

接下来,你决定要寻找以下模式

 1 2 3

在列表中的任何地方,即任何东西后面跟着 1,然后是 2,然后是 3,然后是任何东西。像这样调用 match

(match '(* 1 2 3 *) data)

这看起来很奇怪,但它只是一个列表中的模式规范,后面跟着源数据。列表模式

(* 1 2 3 *)

表示任何原子或表达式的序列(或空),后面跟着 1,然后是 2,然后是 3,后面跟着任意数量的原子或表达式(或空)。这个 match 函数返回的答案是另一个列表,包含两个子列表,一个对应第一个 *,另一个对应第二个 *。

((7 9 3 8  . . .  0 4 5)  (7 2 4 1 . . . 3 5 5 5))

而你所寻找的模式第一次出现在这些列表之间的间隙(事实上,它在列表后面的部分也出现了半打)。match 也可以处理嵌套列表。

要查找模式的所有出现,而不仅仅是第一个,你可以在 while 循环中使用 match。例如,要查找并删除所有紧跟另一个 0 的 0,请对新版本的列表重复 match,直到它不再返回非空值。

(set 'number-list '(2 4 0 0 4 5 4 0 3 6 2 3 0 0 2 0 0 3 3 4 2 0 0 2))

(while (set 'temp-list (match '(* 0 0 *) number-list))
  (println temp-list)
  (set 'number-list (apply append temp-list)))
((2 4) (4 5 4 0 3 6 2 3 0 0 2 0 0 3 3 4 2 0 0 2))
((2 4 4 5 4 0 3 6 2 3) (2 0 0 3 3 4 2 0 0 2))
((2 4 4 5 4 0 3 6 2 3 2) (3 3 4 2 0 0 2))
((2 4 4 5 4 0 3 6 2 3 2 3 3 4 2) (2))
> number-list
;-> (2 4 4 5 4 0 3 6 2 3 2 3 3 4 2 2)


你不必先查找元素,然后才能替换它们:只需使用 replace,它在一个操作中完成查找和替换。你还可以使用 match 作为比较函数来搜索列表。参见 替换信息:replace搜索列表

find-all

[edit | edit source]

find-all 是一个功能强大的函数,具有多种不同的形式,适合于搜索列表、关联列表和字符串。对于列表搜索,你需要提供四个参数:搜索键、列表、操作表达式和函数,函数是你想要用于匹配搜索键的比较函数。

(set 'food '("bread" "cheese" "onion" "pickle" "lettuce"))
(find-all "onion" food (print $0 { }) >)
;-> bread cheese lettuce

这里,find-all 在列表 food 中搜索字符串 "onion"。它使用 > 函数作为比较函数,因此它会找到 "onion" 大于的任何东西。对于字符串,"大于" 表示在默认的 ASCII 排序顺序中排在后面,因此 "cheese" 大于 "bread" 但小于 "onion"。注意,与其他允许你提供比较函数的函数(即 findrefref-allreplace 当用于列表时、set-refset-ref-allsort)不同,比较函数 必须 提供。使用 < 函数,结果是一个包含 "onion" 小于的事物的列表。

(find-all "onion" food (print $0 { }) <)
;-> pickle

ref 和 ref-all

[edit | edit source]

ref 函数返回列表中元素第一个出现的索引。它特别适合用于嵌套列表,因为与 find 不同,它会查看所有子列表内部,并返回元素第一个出现的 地址。例如,假设你已经使用 newLISP 的内置 XML 解析器将 XML 文件(如你的 iTunes 库)转换为一个大型嵌套列表。

(xml-type-tags nil nil nil nil)         ; controls XML parsing
(set 'itunes-data 
 (xml-parse
   (read-file "/Users/me/Music/iTunes/iTunes Music Library.xml") 
   (+ 1 2 4 8 16)))

现在你可以查找数据中的任何表达式,该表达式以普通 newLISP 列表的形式出现。

(ref "Brian Eno" itunes-data)

返回的列表将是该字符串在列表中第一个出现的地址。

(0 2 14 528 6 1)

- 这是一组索引号,它们共同定义了一种 地址。这个例子意味着:在列表元素 0 中,查找子列表元素 2,然后查找该子列表的子列表元素 14,以此类推,深入到高度嵌套的基于 XML 的数据结构中。参见 使用 XML

ref-all 做类似的工作,并返回一个地址列表。

(ref-all "Brian Eno" itunes-data)
;-> ((0 2 14 528 6 1) (0 2 16 3186 6 1) (0 2 16 3226 6 1))

这些函数也可以接受比较函数。参见 搜索列表

当你在嵌套列表中搜索某些东西时,使用这些函数。如果你想在找到它后替换它,请使用 set-refset-ref-all 函数。参见 查找和替换匹配的元素

过滤列表:filter、clean 和 index

[edit | edit source]

在列表中查找事物的另一种方法是过滤列表。就像淘金一样,你可以创建一个过滤器,只保留你想要的东西,将不需要的东西冲走。

filterindex 函数具有相同的语法,但 filter 返回列表元素,而 index 返回想要元素的索引号(索引)而不是列表元素本身。(这些函数不适用于嵌套列表。)

过滤函数 filtercleanindex 使用另一个函数来测试元素:元素根据它是否通过测试而出现在结果列表中。你可以使用内置函数,也可以定义自己的函数。通常,newLISP 中测试并返回 true 或 false 的函数(有时称为谓词函数)的名称以问号结尾。

NaN? array? atom? context? directory? empty? file? float? global? integer? lambda? legal? list? macro? nil? null? number? primitive? protected? quote? string? symbol? true? zero?

因此,例如,在列表中查找整数(并删除浮点数)的一种简单方法是使用 integer? 函数和 filter。只有整数能通过这个过滤器。

(set 'data '(0 1 2 3 4.01 5 6 7 8 9.1 10))
(filter integer? data)
;-> (0 1 2 3 5 6 7 8 10)

filter 有一个名为 clean 的补充函数,它会删除满足测试条件的元素。

(set 'data '(0 1 2 3 4.01 5 6 7 8 9.1 10))
(clean integer? data)
;-> (4.01 9.1)

clean 想象成清除污垢 - 它会清除任何通过测试的东西。将 filter 想象成淘金,保留任何通过测试的东西。

下一个过滤器找到柯南·道尔的故事 空屋 中所有包含字母 pp 的词。过滤器是一个 lambda 表达式(一个没有名称的临时函数),如果元素不包含 pp,则返回 nil。列表是由 parse 生成的字符串元素列表,它根据模式将字符串分解为较小的字符串列表。

(set 'empty-house-text 
 (parse 
   (read-file "/Users/me/Sherlock-Holmes/the-empty-house.txt")
   {,\s*|\s+} 0))

(filter (fn (s) (find "pp" s)) empty-house-text)

;->
("suppressed" "supply" "disappearance" "appealed" "appealed"
"supplemented" "appeared" "opposite" "Apparently" "Suppose"
"disappear" "happy" "appears" "gripped" "reappearance."
"gripped" "opposite" "slipped" "disappeared" "slipped"
"slipped" "unhappy" "appealed" "opportunities." "stopped"
"stepped" "opposite" "dropped" "appeared" "tapped"
"approached" "suppressed" "appeared" "snapped" "dropped"
"stepped" "dropped" "supposition" "opportunity" "appear"
"happy" "deal-topped" "slipper" "supplied" "appealing"
"appear")

你还可以使用 filterclean 来清理列表,然后才能使用它们 - 例如,删除 parse 操作产生的空字符串。

什么时候使用 index 而不是 filterclean?嗯,当你想通过索引号而不是它们的价值来访问列表元素时,可以使用 index:我们将在下一节中遇到一些函数,这些函数用于通过索引选择列表项。例如,虽然 ref 只找到了第一个出现的索引,但你可以使用 index 返回元素所有出现的索引号。

如果你有一个谓词函数,它用于查找字母 c 后面跟着 ie 的字符串,你就可以使用该函数来搜索匹配字符串的列表。

(set 'word-list '("agencies" "being" "believe" "ceiling"
"conceit" "conceive" "deceive" "financier" "foreign"
"neither" "receive" "science" "sufficient" "their" "vein"
"weird"))

(define (i-before-e-after-c? wd)        ; a predicate function
 (find {(c)(ie)(?# i before e after c...)} wd 0))

(index i-before-e-after-c? word-list)
;-> (0 7 11 12)                         
; agencies, financier, science, sufficient

记住,列表可以包含嵌套列表,并且某些函数不会查看子列表内部。

(set 'maze 
 '((1 2.1)
   (1 2 3)
   (1 2 3 4)))

(filter integer? maze)
;-> ()                                  ; I was sure it had integers...

(filter list? maze)                     
;-> ((1 2.1) (1 2 3) (1 2 3 4))         ; ah yes, they're sublists!

(filter integer? (flat maze))           
;-> (1 1 2 3 1 2 3 4)                   ; one way to do it...

测试列表

[edit | edit source]

exists 函数和for-all 函数检查列表中的元素,查看它们是否通过了测试。

exists 返回列表中第一个通过测试的元素,或者如果它们都没有通过测试则返回 nil

(exists string? '(1 2 3 4 5 6 "hello" 7))
;-> "hello"

(exists string? '(1 2 3 4 5 6 7))
;-> nil

for-all 返回 true 或 nil。如果每个列表元素都通过测试,它将返回 true。

(for-all number? '(1 2 3 4 5 6 7))
;-> true

(for-all number? '("zero" 2 3 4 5 6 7))
;-> nil

搜索列表

[编辑 | 编辑源代码]

findrefref-allreplace 用于在列表中查找项目。通常,您使用这些函数查找与您要查找的内容相等的项目。但是,相等只是默认测试:所有这些函数都可以接受一个可选的比较函数,该函数用于代替相等测试。这意味着您可以查找满足任何测试的列表元素。

以下示例使用 < 比较函数。find 查找与 n 相比较有利的第一个元素,即 n 小于的第一个元素。对于值为 1002,满足测试的第一个元素是 1003,即列表中的第 3 个元素,因此返回的值为 3。

(set 's (sequence 1000 1020))
;-> (1000 1001 1002 1003 1004 1005 1006 1007 1008 100
; 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020)

(set 'n 1002)
; find the first something that n is less than:
(find n s <)

;-> 3, the index of 1003 in s

您可以编写自己的比较函数

(set 'a-list 
   '("elephant" "antelope" "giraffe" "dog" "cat" "lion" "shark" ))

(define (longer? x y)
  (> (length x) (length y)))

(find "tiger" a-list longer?)
;-> 3 ; "tiger" is longer than element 3, "dog"

longer? 函数在第一个参数比第二个参数长时返回 true。因此,find 在此函数作为比较时,找到列表中使比较为真的第一个元素。因为 tigerdog 长,所以该函数返回 3,即 dog 在列表中的索引。

您可以将匿名(或 lambda)函数作为 find 函数的一部分提供,而不是编写单独的函数

(find "tiger" a-list (fn (x y) (> (length x) (length y))))

如果您希望代码可读,您可能会将较长或更复杂的比较器移到自己的单独的 - 并且有文档的 - 函数中。

您还可以将比较函数与 refref-allreplace 一起使用。

比较函数可以是任何函数,它接受两个值并返回 true 或 false。例如,以下函数在 y 大于 6 且小于 x 时返回 true。因此,搜索是在 data 列表中查找一个元素,该元素既小于要搜索的数字(在本例中为 15),又大于 6。

(set 'data '(31 23 -63 53 8 -6 -16 71 -124 29))

(define (my-func x y)
  (and (> x y) (> y 6)))

(find 15 data my-func)
;-> 4 ; there's an 8 at index location 4

总结:比较和对比

[编辑 | 编辑源代码]

为了总结这些 contains 函数,以下是在它们起作用时的示例

(set 'data 
   '("this" "is" "a" "list" "of" "strings" "not" "of" "integers"))

(find "of" data)                        ; equality is default test
;-> 4                                   ; index of first occurrence

(ref "of" data)                         ; where is "of"?
;-> (4)                                 ; returns a list of indexes

(ref-all "of" data)                     
;-> ((4) (7))                           ; list of address lists

(filter (fn (x) (= "of" x)) data)       ; keep every of
;-> ("of" "of")                         

(index (fn (x) (= "of" x)) data)        ; indexes of the of's
;-> (4 7)                               

(match (* "of" * "of" *) data)          ; three lists between the of's
;-> (("this" "is" "a" "list") ("strings" "not") ("integers"))

(member "of" data)                      ; and the rest
;-> ("of" "strings" "not" "of" "integers")

(count (list "of") data)                ; remember to use two lists
;-> (2)                                 ; returns list of counts

从列表中选择项目

[编辑 | 编辑源代码]

有多种函数用于获取存储在列表中的信息

  • first 获取第一个元素
  • rest 获取除第一个元素之外的所有元素
  • last 返回最后一个元素
  • nth 获取第 n 个元素
  • select 根据索引选择某些元素
  • slice 提取子列表

firstrest 函数是传统 carcdr LISP 函数更合理的名称,它们基于旧计算机硬件寄存器的名称。

选择元素:nth、select 和 slice

[编辑 | 编辑源代码]

nth 获取列表中的第 n 个元素

(set 'phrase '("the" "quick" "brown" "fox" "jumped" "over" "the" "lazy" "dog"))

(nth 1 phrase)
;-> "quick"

nth 还可以查看嵌套列表的内部,因为它接受多个索引号

(set 'zoo
 '(("ape" 3)
   ("bat" 47)
   ("lion" 4)))
(nth '(2 1) zoo)                         ; item 2, then subitem 1
;-> 4

如果您想从列表中挑选一组元素,您会发现 select 很有用。您可以在两种不同的形式中使用它。第一种形式允许您提供一系列松散的索引号

(set 'phrase '("the" "quick" "brown" "fox" "jumped" "over" "the" "lazy" "dog"))

(select phrase 0 -2 3 4 -4 6 1 -1)
;-> ("the" "lazy" "fox" "jumped" "over" "the" "quick" "dog")

正数通过从开头向前计数来选择元素,负数通过从结尾向后计数来选择元素

   0      1       2      3       4      5      6      7     8
("the" "quick" "brown" "fox" "jumped" "over" "the" "lazy" "dog")
  -9     -8      -7     -6      -5     -4     -3     -2    -1


您还可以将索引号列表提供给 select。例如,您可以使用 rand 函数生成一个从 0 到 8 之间的 20 个随机数的列表,然后使用此列表从 phrase 中随机选择元素

(select phrase (rand 9 20))
;-> ("jumped" "lazy" "over" "brown" "jumped" "dog" "the" "dog" "dog"
; "quick" "the" "dog" "the" "dog" "the" "brown" "lazy" "lazy" "lazy" "quick")

注意重复项。如果您改为编写

(randomize phrase)

则不会有重复项:(randomize phrase) 打乱元素而不重复它们。

slice 允许您提取列表的部分。为它提供列表,后跟一个或两个数字。第一个数字是起始位置。如果您省略第二个数字,则会返回列表的其余部分。第二个数字(如果为正数)是要返回的元素数。

(slice (explode "schwarzwalderkirschtorte") 7)
;-> ("w" "a" "l" "d" "e" "r" "k" "i" "r" "s" "c" "h" "t" "o" "r" "t" "e")

(slice (explode "schwarzwalderkirschtorte") 7 6)
;-> ("w" "a" "l" "d" "e" "r")

如果为负数,则第二个数字指定切片另一端的一个元素,从列表末尾向后计数,-1 表示最后一个元素

(slice (explode "schwarzwalderkirschtorte") 19 -1)
;-> ("t" "o" "r" "t")

切刀可以延伸到 - 但不包括 - 您指定的元素。

隐式寻址

[编辑 | 编辑源代码]

newLISP 提供了一种更快、更高效的方式来选择和切片列表。您可以使用索引号和列表一起使用,而不是使用函数。此技术称为隐式寻址。

使用隐式寻址选择元素

[编辑 | 编辑源代码]

作为使用 nth 的替代方法,将列表的符号和索引号放在一个列表中,如下所示

(set 'r '("the" "cat" "sat" "on" "the" "mat"))
(r 1)                                   ; element index 1 of r
;-> "cat"

(nth 1 r)                               ; the equivalent using nth
;-> "cat"

(r 0)
;-> "the"

(r -1)
;-> "mat"

如果您有一个嵌套列表,您可以提供一系列索引号来标识层次结构中的列表

(set 'zoo 
 '(("ape" 3) 
   ("bat" 47) 
   ("lion" 4)))                            ; three sublists in a list

(zoo 2 1)
;-> 4
(nth '(2 1) zoo)                           ; the equivalent using nth
;-> 4

其中 '(2 1) 首先找到元素 2,("lion" 4),然后找到该子列表中的元素 1(第二个元素)。

使用隐式寻址选择切片

[编辑 | 编辑源代码]

您还可以使用隐式寻址来获取列表的切片。这次,在列表的符号之前,在一个列表中放置一个或两个数字来定义切片

(set 'alphabet '("a" "b" "c" "d" "e" "f" "g" "h" "i" "j" "k" 
"l" "m" "n" "o" "p" "q" "r" "s" "t" "u" "v" "w" "x" "y" "z"))

(13 alphabet)                 ; start at 13, get the rest
;-> ("n" "o" "p" "q" "r" "s" "t" "u" "v" "w" "x" "y" "z")

(slice alphabet 13)           ; equivalent using slice
;-> ("n" "o" "p" "q" "r" "s" "t" "u" "v" "w" "x" "y" "z")

(3 7 alphabet)                ; start at 3, get 7 elements
;-> ("d" "e" "f" "g" "h" "i" "j")

(slice alphabet 3 7)          ; equivalent using slice
;-> ("d" "e" "f" "g" "h" "i" "j")

之前,我们解析了 iTunes XML 库

(xml-type-tags nil nil nil nil)
(silent 
 (set 'itunes-data
   (xml-parse 
    (read-file 
      "/Users/me/Music/iTunes/iTunes Music Library.xml")
   (+ 1 2 4 8 16))))

让我们使用隐式寻址技术访问结果 XML 结构的内部

(set 'eno (ref "Brian Eno" itunes-data))
;-> (0 2 14 528 6 1)                    ; address of Brian Eno

(0 4 eno)                               ; implicit slice
;-> (0 2 14 528)

(itunes-data (0 4 eno))
;->
(dict 
 (key "Track ID") 
 (int "305") 
 (key "Name") 
 (string "An Ending (Ascent)") 
 (key "Artist") 
 (string "Brian Eno") ; this was (0 2 14 528 6 1)
 (key "Album") 
 (string "Ambient Journeys") 
 (key "Genre") 
 (string "ambient, new age, electronica") 
 (key "Kind") 
 (string "Apple Lossless audio file") 
 (key "Size") 
 (int "21858166") 
; ... 
 )

如何记住两种类型隐式寻址之间的区别?sLice 数字放在 Lead 中,sElect 数字放在 End 中。

列表手术

[编辑 | 编辑源代码]

缩短列表

[编辑 | 编辑源代码]

要缩短列表,从前面或后面删除元素,请使用 choppopchop 创建一个副本并从末尾开始,pop 更改原始副本并从开头开始。

chop 通过切断列表的末尾来返回一个新列表

(set 'vowels '("a" "e" "i" "o" "u"))
(chop vowels)
;-> ("a" "e" "i" "o")

(println vowels)
("a" "e" "i" "o" "u")                   ; original unchanged

chop 的可选第三个参数指定要删除的元素数量

(chop vowels 3)
;-> ("a" "e")

(println vowels)
("a" "e" "i" "o" "u") ; original unchanged

pop(与 push 相反)永久地从列表中删除指定的元素,并使用列表索引而不是长度

(set 'vowels '("a" "e" "i" "o" "u"))

(pop vowels)                            ; defaults to 0-th element

(println vowels)
("e" "i" "o" "u")

(pop vowels -1)

(println vowels)
("e" "i" "o")

您还可以使用 replace 从列表中删除项目。

更改列表中的项目

[编辑 | 编辑源代码]

您可以轻松地使用以下函数更改列表中的元素

  • replace 更改或删除元素
  • swap 交换两个元素
  • setf 设置元素的值
  • set-ref 搜索嵌套列表并更改元素
  • set-ref-all 搜索并更改嵌套列表中的所有元素

这些是破坏性函数,就像 pushpopreversesort 一样,它们会更改原始列表,因此请谨慎使用它们。

更改第 n 个元素

[编辑 | 编辑源代码]

要将列表(或数组)中的第 n 个元素设置为另一个值,请使用通用的 setf 命令

(set 'data (sequence 100 110))
;-> (100 101 102 103 104 105 106 107 108 109 110)

(setf (data 5) 0)
;-> 0

data
;-> (100 101 102 103 104 0 106 107 108 109 110)

注意 setf 函数如何返回刚刚设置的值 0,而不是更改后的列表。

此示例使用更快的隐式寻址。当然,您也可以使用 nth 首先创建对第 n 个元素的引用

(set 'data (sequence 100 110))
;-> (100 101 102 103 104 105 106 107 108 109 110)

(setf (nth 5 data) 1)
;-> 1

data
;-> (100 101 102 103 104 1 106 107 108 109 110)

setf 必须用于存储在符号中的列表或数组或元素。您不能将原始数据传递给它

(setf (nth 5 (sequence 100 110)) 1)
;-> ERR: no symbol reference found

(setf (nth 5 (set 's (sequence 100 110))) 1)
; 'temporary' storage in symbol s
;-> 1
s
;-> (100 101 102 103 104 1 106 107 108 109 110)

使用它

[编辑 | 编辑源代码]

有时,当您使用 setf 时,您希望在设置新值时引用旧值。为此,请使用系统变量 $it。在 setf 表达式期间,$it 包含旧值。因此,要将列表第一个元素的值增加 1

(set 'lst (sequence 0 9))
;-> (0 1 2 3 4 5 6 7 8 9)
(setf (lst 0) (+ $it 1))
;-> 1
lst
;-> (1 1 2 3 4 5 6 7 8 9)

您也可以对字符串执行此操作。以下是“增加”字符串第一个字母的方法

(set 'str "cream")
;-> "cream"
(setf (str 0) (char (inc (char $it))))
;-> "d"
str
;-> "dream"

替换信息:replace

[编辑 | 编辑源代码]

您可以使用 replace 更改或删除列表中的元素。指定要更改的元素和要搜索的列表,以及(如果有)替换项。

(set 'data (sequence 1 10)) 
(replace 5 data)                   ; no replacement specified
;-> (1 2 3 4 6 7 8 9 10)           ; the 5 has gone

(set 'data '(("a" 1) ("b" 2)))
(replace ("a" 1) data)             ; data is now (("b" 2))

所有匹配项都将被删除。

replace 返回已更改的列表

(set 'data (sequence 1 10)) 
(replace 5 data 0)                 ; replace 5 with 0
;-> (1 2 3 4 0 6 7 8 9 10)

替换可以是简单值,也可以是返回值的任何表达式。

(set 'data (sequence 1 10)) 
(replace 5 data (sequence 0 5))
;->(1 2 3 4 (0 1 2 3 4 5) 6 7 8 9 10)

replace 使用系统变量集 $0、$1、$2,一直到 $15,以及特殊变量 $it,更新匹配数据。对于列表替换,仅使用 $0 和 $it,它们保存找到的项目的 value,适合在替换表达式中使用。

(replace 5 data (list (dup $0 2)))    ; $0 holds 5
;-> (1 2 3 4 ((5 5)) 6 7 8 9 10)

有关系统变量及其在字符串替换中的使用,请参阅 系统变量

如果您没有提供测试函数,则使用 =

(set 'data (sequence 1 10)) 
(replace 5 data 0 =)
;-> (1 2 3 4 0 6 7 8 9 10)

(set 'data (sequence 1 10)) 
(replace 5 data 0)              ; = is assumed
;-> (1 2 3 4 0 6 7 8 9 10)

您可以使 replace 查找通过不同测试(而不是相等性)的元素。在替换值之后提供测试函数

(set 'data (randomize (sequence 1 10)))
;-> (5 10 6 1 7 4 8 3 9 2)
(replace 5 data 0 <)  ; replace everything that 5 is less than
;-> (5 0 0 1 0 4 0 3 0 2)

测试可以是任何比较两个值并返回真值或假值的函数。这可能非常强大。假设您有一个包含姓名及其分数的列表

(set 'scores '(
   ("adrian" 234 27 342 23 0) 
   ("hermann" 92 0 239 47 134) 
   ("neville" 71 2 118 0) 
   ("eric" 10 14 58 12 )))

将所有分数包含 0 的人的数字加起来有多容易?有了 match 函数的帮助,这很容易

(replace '(* 0 *) scores (list (first $0) (apply + (rest $0))) match)

(("adrian" 626) 
 ("hermann" 512) 
 ("neville" 191) 
 ("eric" 10 14 58 12))

在这里,对于每个匹配元素,替换表达式从名称和分数之和构建一个列表。match 用作比较函数 - 仅选择匹配的列表元素进行汇总,因此 Eric 的分数未被汇总,因为他没有设法获得 0 分。

有关在字符串上使用 replace 的更多信息,请参阅 更改子字符串

修改列表

[编辑 | 编辑源代码]

还有更强大的方法可以修改列表中的元素。认识一下 set-refset-ref-all

您可以使用这些函数定位和修改元素,这些函数旨在与嵌套列表配合使用。(另请参阅 使用 XML,了解一些应用程序。)

查找并替换匹配元素

[编辑 | 编辑源代码]

set-ref 函数允许您修改列表中的第一个匹配元素

(set 'l '((aaa 100) (bbb 200)))
;-> ((aaa 100) (bbb 200))

要将 200 更改为 300,请使用 set-ref,如下所示

(set-ref 200 l 300)               ; change the first 200 to 300
;-> ((aaa 100) (bbb 300))

查找并替换所有匹配元素:set-ref-all

[编辑 | 编辑源代码]

set-ref 在嵌套列表中找到第一个匹配元素并更改它;set-ref-all 可以替换每个匹配元素。考虑以下包含行星数据的嵌套列表

 (("Mercury"
      (p-name "Mercury")
      (diameter 0.382)
      (mass 0.06)
      (radius 0.387)
      (period 0.241)
      (incline 7)
      (eccentricity 0.206)
      (rotation 58.6)
      (moons 0))
  ("Venus"
      (p-name "Venus")
      (diameter 0.949)
      (mass 0.82)
      (radius 0.72)
      (period 0.615)
      (incline 3.39)
      (eccentricity 0.0068)
      (rotation -243)
      (moons 0))
  ("Earth"
      (p-name "Earth")
      (diameter 1)
;      ...

如何将所有“incline”符号更改为“inclination”?使用 set-ref-all 很容易

(set-ref-all 'incline planets 'inclination)   ; key - list - replacement

这将返回一个列表,其中每个“incline”都更改为“inclination”。

replace 一样,查找匹配元素的默认测试是相等性。但是您可以提供不同的比较函数。以下是您如何检查行星列表并将每个月球值大于 9 的条目更改为“lots”而不是实际数字的方法。

(set-ref-all '(moons ?) planets (if (> (last $0) 9) "lots" (last $0)) match)

替换表达式比较月球数量(结果的最后一个项目,存储在 $0 中),如果大于 9,则评估为“lots”。搜索词使用 match 友好的通配符语法来匹配比较函数的选择。

swap 函数可以交换列表中的两个元素,或两个符号的值。这将更改原始列表

(set 'fib '(1 2 1 3 5 8 13 21))
(swap (fib 1) (fib 2))                         ; list swap 
;-> (1 1 2 3 5 8 13 21)

fib
;-> (1 1 2 3 5 8 13 21)                 ; is 'destructive'

有用的是,swap 还可以交换两个符号的值,而无需使用中间临时变量。

(set 'x 1 'y 2)
(swap x y)
;-> 1
x
;-> 2
y
;-> 1

这种并行分配有时可以简化生活,例如在这个略有不寻常的查找斐波那契数的迭代函数版本中

(define (fibonacci n)
 (let (current 1 next 0)
  (dotimes (j n)
    (print current " ")
    (inc next current)
    (swap current next))))
  
(fibonacci 20)
1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765

使用两个或多个列表

[编辑 | 编辑源代码]

如果您有两个列表,您可能想问一些问题,例如 两个列表中共有多少个项目?哪些项目只在一个列表中?此列表中的项目在另一个列表中出现多少次?,等等。以下是一些用于回答这些问题的有用函数

  • difference 查找两个列表的集合差
  • intersect 查找两个列表的交集
  • count 计算一个列表中的每个元素在第二个列表中出现的次数

例如,要查看句子中元音的数量,请将已知元音放在一个列表中,将句子放在另一个列表中(首先使用 explode 将句子转换为字符列表)

(count '("a" "e" "i" "o" "u") (explode "the quick brown fox jumped over the lazy dog"))
;-> (1 4 1 4 2)

或者更短一点

(count (explode "aeiou") (explode "the quick brown fox jumped over the lazy dog"))
;-> (1 4 1 4 2)

结果 (1 4 1 4 2) 表示句子中有 1 个 a、4 个 e、1 个 i、4 个 o 和 2 个 u。

differenceintersect 是让您想起学校里学的那些维恩图的函数(如果您上了教这些的学校)。在 newLISP 中,列表可以表示集合。

difference 返回一个列表,其中包含第一个列表中不在第二个列表中的那些元素。例如,您可以比较系统上的两个目录,以查找一个目录中存在但另一个目录中不存在的文件。您可以为此使用 directory 函数。

(set 'd1 
 (directory "/Users/me/Library/Application Support/BBEdit"))
(set 'd2 
 (directory "/Users/me/Library/Application Support/TextWrangler"))

(difference d1 d2)
;-> ("AutoSaves" "Glossary" "HTML Templates" "Stationery" "Text Factories")

(difference d2 d1)
;-> ()

您将哪个列表放在前面很重要!目录 d1 中有五个不在目录 d2 中的文件或目录,但 d2 中没有不在 d1 中的文件或目录。

intersect 函数查找两个列表中都存在的元素。

(intersect d2 d1)
;-> ("." ".." ".DS_Store" "Language Modules" "Menu Scripts" "Plug-Ins" "Read Me.txt" "Scripts" "Unix Support")

这两个函数都可以接受一个额外的参数,它控制是否保留或丢弃任何重复项。

您可以使用 difference 函数比较文本文件的两个修订版。首先使用 parse (解析字符串) 将文件拆分为行

(set 'd1 
 (parse (read-file "/Users/me/f1-(2006-05-29)-1.html") "\r" 0))

(set 'd2 
 (parse (read-file "/Users/me/f1-(2006-05-29)-6.html") "\r" 0))

(println (difference d1 d2))
(" <p class=\"body\">You could use this function to find" ...)

关联列表

[编辑 | 编辑源代码]

newLISP 中提供了各种用于存储信息的技术。一种非常简单且有效的技术是使用子列表列表,其中每个子列表的第一个元素是 key。这种结构被称为关联列表,但您也可以将其视为字典,因为您通过首先查找 key 元素来查找列表中的信息。

您还可以使用 newLISP 的上下文来实现字典。请参阅 介绍上下文

您可以使用基本的列表函数创建关联列表。例如,您可以提供一个手工制作的引用列表

(set 'ascii-chart '(("a" 97) ("b" 98) ("c" 99) 
; ...
))

或者您可以使用 listpush 等函数来构建关联列表

(for (c (char "a") (char "z"))
 (push (list (char c) c) ascii-chart -1))

ascii-chart
;-> (("a" 97) ("b" 98) ("c" 99) ... ("z" 122))

它是一个子列表列表,每个子列表都具有相同的格式。子列表的第一个元素是 key。key 可以是字符串、数字或符号。key 后面可以有任意数量的数据元素。

这是一个包含太阳系行星的一些数据的关联列表

(set 'sol-sys
 '(("Mercury" 0.382 0.06 0.387 0.241 7.00 0.206 58.6 0) 
   ("Venus" 0.949 0.82 0.72 0.615 3.39 0.0068 -243 0) 
   ("Earth" 1.00 1.00 1.00 1.00 0.00 0.0167 1.00 1) 
   ("Mars" 0.53 0.11 1.52 1.88 1.85 0.0934 1.03 2) 
   ("Jupiter" 11.2 318 5.20 11.86 1.31 0.0484 0.414 63) 
   ("Saturn" 9.41 95 9.54 29.46 2.48 0.0542 0.426 49) 
   ("Uranus" 3.98 14.6 19.22 84.01 0.77 0.0472 -0.718 27) 
   ("Neptune" 3.81 17.2 30.06 164.8 1.77 0.0086 0.671 13) 
   ("Pluto" 0.18 0.002 39.5 248.5 17.1 0.249 -6.5 3)
   )
   ; 0: Planet name 1: Equator diameter (earth) 2: Mass (earth) 
   ; 3: Orbital radius (AU) 4: Orbital period (years) 
   ; 5: Orbital Incline Angle 6: Orbital Eccentricity 
   ; 7: Rotation (days) 8: Moons
)

每个子列表都以字符串(行星名称)开头,后面是数据元素(在本例中为数字)。行星名称是 key。我在最后添加了一些注释,因为我永远不会记得元素 2 是行星的质量,以地球质量为单位。

您可以轻松地使用标准列表处理技术访问此信息,但 newLISP 提供了一些专门针对这些字典或关联列表而设计的定制函数

  • assoc 查找关键字的首次出现并返回子列表。
  • lookup 在子列表中查找关键字的值。

assoclookup 都使用子列表的第一个元素(即键)从相应的子列表中检索数据。以下是 assoc 的实际操作,它返回子列表。

(assoc "Uranus" sol-sys)
;-> ("Uranus" 3.98 14.6 19.22 84.01 0.77 0.0472 -0.718 27)

以下是 lookup,它更进一步,从子列表中的某个元素中获取数据,如果没有指定元素,则获取最后一个元素。

(lookup "Uranus" sol-sys)
;-> 27, moons - value of the final element of the sublist

(lookup "Uranus" sol-sys 2)
;-> 14.6, element 2 of the sublist is the planet's mass

这使你无需再使用 assocnth 的组合。

使用带有长子列表的关联列表时,你可能会遇到一个问题,即无法记住索引号代表什么。以下是一个解决方案。

(constant 'orbital-radius 3)
(constant 'au 149598000)                 ; 1 au in km
(println "Neptune's orbital radius is " 
 (mul au (lookup "Neptune" sol-sys orbital-radius)) 
 " kilometres")
Neptune's orbital radius is 4496915880 kilometres

这里我们定义了 orbital-radiusau(天文单位)作为常量,你可以使用 orbital-radius 来引用子列表的右侧列。这也有助于使代码更易读。constant 函数类似于 set,但你提供的符号受保护,不会因 set 的其他使用而意外更改。你只能使用 constant 函数再次更改符号的值。

定义完这些常量后,以下是一个以公里为单位列出不同行星轨道的表达式。

(dolist (planet-data sol-sys)             ; go through list
 (set 'planet (first planet-data))        ; get name 
 (set 'orb-rad
  (lookup planet sol-sys orbital-radius)) ; get radius
 (println 
    (format "%-8s %12.2f %18.0f" 
     planet 
     orb-rad 
     (mul au orb-rad))))
Mercury          0.39           57894426
Venus            0.72          107710560
Earth            1.00          149598000
Mars             1.52          227388960
Jupiter          5.20          777909600
Saturn           9.54         1427164920
Uranus          19.22         2875273560
Neptune         30.06         4496915880
Pluto           39.50         5909121000


当你想要操作浮点数时,请使用浮点算术运算符 addsubmuldiv 而不是 +-*/,后者用于整数(并将值转换为整数)。

替换关联列表中的子列表

[edit | edit source]

要更改存储在关联列表中的值,请像以前一样使用 assoc 函数查找匹配的子列表,然后使用 setf 在该子列表上将值更改为新的子列表。

(setf (assoc "Jupiter" sol-sys) '("Jupiter" 11.2 318 5.20 11.86 1.31 0.0484 0.414 64))

向关联列表添加新项目

[edit | edit source]

关联列表也是普通列表,因此你可以使用所有熟悉的 newLISP 技术来处理它们。想要向我们的 sol-sys 列表中添加一个新的第十颗行星吗?只需使用 push

(push '("Sedna" 0.093 0.00014 .0001 502 11500 0 20 0) sol-sys -1)

并使用以下命令检查它是否已添加成功。

(assoc "Sedna" sol-sys)
;-> ("Sedna" 0.093 0.00014 0.0001 502 11500 0 20 0)

你可以使用 sort 对关联列表进行排序。(但请记住,sort 会永久更改列表。)以下是一个按质量排序的行星列表。由于你不想按名称对其进行排序,因此可以使用自定义排序(参见 sort and randomize)来比较每对的质量(索引 2)值。

(constant 'mass 2)
(sort sol-sys (fn (x y) (> (x mass) (y mass))))

(println sol-sys)
("Jupiter" 11.2 318 5.2 11.86 1.31 0.0484 0.414 63) 
("Saturn" 9.41 95 9.54 29.46 2.48 0.0542 0.426 49) 
("Neptune" 3.81 17.2 30.06 164.8 1.77 0.0086 0.671 13) 
("Uranus" 3.98 14.6 19.22 84.01 0.77 0.0472 -0.718 27) 
("Earth" 1 1 1 1 0 0.0167 1 1) 
("Venus" 0.949 0.82 0.72 0.615 3.39 0.0068 -243 0) 
("Mars" 0.53 0.11 1.52 1.88 1.85 0.0934 1.03 2) 
("Mercury" 0.382 0.06 0.387 0.241 7 0.206 58.6 0) 
("Pluto" 0.18 0.002 39.5 248.5 17.1 0.249 -6.5 3)

你也可以轻松地将关联列表中的数据与其他列表组合起来。

; restore to standard order - sort by orbit radius
(sort sol-sys (fn (x y) (< (x 3) (y 3))))   

; define Unicode symbols for planets
(set 'unicode-symbols 
  '(("Mercury" 0x263F )
    ("Venus" 0x2640 )
    ("Earth" 0x2641 )
    ("Mars" 0x2642 )
    ("Jupiter" 0x2643 )
    ("Saturn" 0x2644 ) 
    ("Uranus" 0x2645 )
    ("Neptune" 0x2646 )
    ("Pluto" 0x2647)))
(map 
 (fn (planet) 
 (println (char (lookup (first planet) unicode-symbols)) 
  "\t" 
 (first planet))) 
 sol-sys)
☿ (Unicode symbol for Mercury)
♀ (Unicode symbol for Venus)
♁ (Unicode symbol for Earth)
♂ (Unicode symbol for Mars)
♃ (Unicode symbol for Jupiter)
♄ (Unicode symbol for Saturn)
♅ (Unicode symbol for Uranus)
♆ (Unicode symbol for Neptune)
♇ (Unicode symbol for Pluto)

这里我们创建了一个临时的内联函数,map 将其应用于 sol-sys 中的每个行星 - lookup 查找行星名称,并从 unicode-symbols 关联列表中检索该行星的 Unicode 符号。

你可以使用 pop-assoc 快速从关联列表中删除元素。

(pop-assoc (sol-sys "Pluto"))

这将从列表中删除冥王星元素。

newLISP 以上下文的形式提供了强大的数据存储功能,你可以使用这些功能来构建字典、哈希表、对象等等。你可以使用关联列表来构建字典,并使用关联列表函数来处理字典的内容。参见 Introducing contexts

你也可以使用数据库引擎 - 参见 Using a SQLite database

find-all 和关联列表

[edit | edit source]

find-all 的另一种形式允许你在关联列表中搜索与模式匹配的子列表。你可以使用通配符来指定模式。例如,以下是一个关联列表。

(set 'symphonies 
  '((Beethoven 9)
    (Haydn 104)
    (Mozart 41)
    (Mahler 10)
    (Wagner 1)
    (Schumann 4)
    (Shostakovich 15)
    (Bruckner 9)))

要查找所有以 9 结尾的子列表,请使用匹配模式 (? 9),其中问号匹配任何单个项目。

(find-all '(? 9) symphonies)
;-> ((Beethoven 9) (Bruckner 9))

(有关匹配模式(列表的通配符搜索)的更多信息,请参见 matching patterns in lists。)

你也可以在关联列表后面添加一个额外的操作表达式来使用此形式。

(find-all '(? 9) symphonies 
   (println (first $0) { wrote 9 symphonies.}))
Beethoven wrote 9 symphonies.
Bruckner wrote 9 symphonies.

这里,操作表达式使用 $0 依次引用每个匹配的元素。

字符串

[edit | edit source]

字符串处理工具是编程语言的重要组成部分。newLISP 拥有许多易于使用且强大的字符串处理工具,如果你的特定需求没有得到满足,你可以轻松地向你的工具箱中添加更多工具。

以下是对 newLISP 的 string orchestra 的导览。

newLISP 代码中的字符串

[edit | edit source]

你可以通过三种方式编写字符串

  • 用双引号括起来
  • 用花括号括起来
  • 用标记代码标记起来

像这样

(set 's "this is a string")
(set 's {this is a string})
(set 's [text]this is a string[/text])

所有三种方法都可以处理最多 2048 个字符的字符串。对于超过 2048 个字符的字符串,请始终使用 [text][/text] 标签来包围字符串。

如果你希望对转义字符(例如 \n 和 \t)或代码数字(\046)进行处理,请始终使用第一种方法(即引号)。

(set 's "this is a string \n with two lines")
(println s)
this is a string 
with two lines
(println "\110\101\119\076\073\083\080")    ; decimal ASCII
newLISP
(println "\x6e\x65\x77\x4c\x49\x53\x50")    ; hex ASCII
newLISP

双引号字符必须使用反斜杠转义,反斜杠也是如此,如果你希望它们出现在字符串中。

当字符串长度小于 2048 个字符且你不想处理任何转义字符时,请使用第二种方法(即花括号)。

(set 's {strings can be enclosed in \n"quotation marks" \n })
(println s)
strings can be enclosed in \n"quotation marks" \n

这是一种非常有用的编写字符串的方法,因为你无需担心在每个引号字符之前添加反斜杠,或在其他反斜杠之前添加反斜杠。你可以在带花括号的字符串中嵌套花括号对,但不能有未匹配的花括号。我喜欢使用花括号来表示字符串,因为它们朝向正确的方向(而简单的 dumb 引号则没有),并且因为你的文本编辑器可能能够平衡和匹配它们。

第三种方法是使用 [text][/text] 标记,适用于跨越多行的较长文本字符串,当 newLISP 输出大量文本时会自动使用这种方法。同样,你无需担心哪些字符可以包含,哪些字符不能包含 - 你可以包含任何你喜欢的字符,但 [/text] 除外。转义字符(如 \n 或 \046)也不会被处理。

(set 'novel (read-file {my-latest-novel.txt}))

;->
[text]
It was a dark and "stormy" night...
...
The End.
[/text]


如果你想知道字符串的长度,请使用 length

(length novel)
;-> 575196


newLISP 可以轻松处理数百万个字符的字符串。

与其使用 length,不如使用 utf8len 来获取 Unicode 字符串的长度。

(utf8len (char 955))
;-> 1

(length (char 955))
;-> 2


制作字符串

[edit | edit source]

许多函数(例如文件读取函数)会为你返回字符串或字符串列表。但如果你想从头开始构建字符串,一种方法是使用 char 函数。这会将提供的数字转换为具有该代码数字的等效字符字符串。它也可以反转操作,将提供的字符字符串转换为等效的代码数字。)

(char 33)
;-> "!"
(char "!")
;-> 33
(char 955)       ; Unicode lambda character, decimal code
;-> "\206\187"
(char 0x2643)    ; Unicode symbol for Jupiter, hex code
;-> "\226\153\131"


当你运行支持 Unicode 的 newLISP 版本时,最后这两个示例可用。由于 Unicode 偏向于十六进制,因此你可以为 char 提供一个以 0x 开头的十六进制数。要查看实际的字符,请使用打印命令

(println (char 955))

λ

;-> "\206\187"
(println (char 0x2643))

;-> "\226\140\152"

(println (char (int (string "0x" "2643"))))    ; equivalent

;-> "\226\140\152"


带有反斜杠的数字是 println 函数的结果,可能是 Unicode 字符的字节值。

你可以使用 char 以其他方式构建字符串。

(join (map char (sequence (char "a") (char "z"))))
;-> "abcdefghijklmnopqrstuvwxyz"


这使用 char 找出 az 的 ASCII 代码数字,然后使用 sequence 生成这两个数字之间的代码数字列表。然后将 char 函数映射到列表的每个元素上,从而产生一个字符串列表。最后,此列表通过 join 转换为单个字符串。

join 在构建字符串时还可以使用分隔符。

(join (map char (sequence (char "a") (char "z"))) "-")
;-> "a-b-c-d-e-f-g-h-i-j-k-l-m-n-o-p-q-r-s-t-u-v-w-x-y-z"

join 相似的是 append,它直接在字符串上操作。

(append "con" "cat" "e" "nation")
;-> "concatenation"


但更实用的是 string,它会将任何数字、列表和字符串的集合转换为单个字符串。

(string '(sequence 1 10) { produces } (sequence 1 10) "\n")
;-> (sequence 1 10) produces (1 2 3 4 5 6 7 8 9 10)


请注意,第一个列表没有被求值(因为它被引用),但第二个列表被求值,生成了一个数字列表,然后生成的列表(包括括号)被转换为字符串。

string 函数与花括号和标记标签等各种字符串标记相结合,是将变量的值包含在字符串中的一种方法。

(set 'x 42)
(string {the value of } 'x { is } x) 
;-> "the value of x is 42"

您也可以使用 format 来组合字符串和符号值。参见 格式化字符串

dup 用于创建副本

(dup "spam" 10)
;-> "spamspamspamspamspamspamspamspamspamspam"

date 用于生成日期字符串

(date)
;-> "Wed Jan 25 15:04:49 2006"

或者您可以提供自 1970 年以来的秒数进行转换

(date 1230000000) 
;-> "Tue Dec 23 02:40:00 2008"


参见 处理日期和时间

字符串操作

[编辑 | 编辑源代码]

现在您已经有了字符串,有很多函数可以对它们进行操作。其中一些是 破坏性 函数 - 它们永久地改变字符串,可能会永远丢失信息。另一些是构造性的,生成新的字符串,而不会破坏旧字符串。参见 破坏性函数

reverse 是破坏性的

(set 't "a hypothetical one-dimensional subatomic particle")
(reverse t)
;-> "elcitrap cimotabus lanoisnemid-eno lacitehtopyh a"

现在 t 永远改变了。但是,大小写转换函数不是破坏性的,它们会生成新的字符串,而不会破坏旧字符串

(set 't "a hypothetical one-dimensional subatomic particle")

(upper-case t)
;-> "A HYPOTHETICAL ONE-DIMENSIONAL SUBATOMIC PARTICLE"

(lower-case t)
;-> "a hypothetical one-dimensional subatomic particle"

(title-case t)
;-> "A hypothetical one-dimensional subatomic particle"

子字符串

[编辑 | 编辑源代码]

如果您知道要提取字符串的哪个部分,请使用以下构造函数之一

(set 't "a hypothetical one-dimensional subatomic particle")
(first t)
;-> "a"

(rest t)
;-> " hypothetical one-dimensional subatomic particle"

(last t)
;-> "e"

(t 2)            ; counting from 0
;-> "h"

您也可以将此技术用于列表。参见 从列表中选择项目

字符串切片

[编辑 | 编辑源代码]

slice 为您提供现有字符串的新切片,从切割点开始向前计数(正整数)或从末尾开始向后计数(负整数),对于给定的字符数或到指定的职位

(set 't "a hypothetical one-dimensional subatomic particle")
(slice t 15 13)
;-> "one-dimension"

(slice t -8 8)
;-> "particle"

(slice t 2 -9)
;-> "hypothetical one-dimensional subatomic"

(slice "schwarzwalderkirschtorte" 19 -1)
;-> "tort"

还有一个捷径可以做到这一点。在列表中将所需的开始位置和长度放在字符串之前

(15 13 t)
;-> "one-dimension"

(0 14 t)
;-> "a hypothetical"

如果您不想要连续的字符,而是想要挑选一些字符来组成新的字符串,请使用 select 后跟一系列字符索引号

(set 't "a hypothetical one-dimensional subatomic particle")
(select t 3 5 24 48 21 10 44 8)
;-> "yosemite"

(select t (sequence 1 49 12)) ; every 12th char starting at 1
;-> " lime"

这对于在文本中查找隐藏的编码消息很有用。

更改字符串的末尾

[编辑 | 编辑源代码]

trimchop 都是构造性的字符串编辑函数,它们从原始字符串的末尾向内工作。

chop 从末尾开始工作

(chop t)       ; defaults to the last character
;-> "a hypothetical one-dimensional subatomic particl"

(chop t 9)     ; chop 9 characters off
;-> "a hypothetical one-dimensional subatomic"

trim 可以从两端删除字符

(set 's "        centred       ")
(trim s)            ; defaults to removing spaces
;-> "centred"

(set 's "------centred------")
(trim s "-")
;-> "centred"

(set 's "------centred******")
(trim s "-" "*")    ; front and back
;-> "centred"

push 和 pop 也可以用于字符串

[编辑 | 编辑源代码]

您已经看到 pushpop 在向列表添加和删除项目。它们也可以用于字符串。使用 push 向字符串添加字符,使用 pop 从字符串中删除一个字符。除非您指定索引,否则字符串将添加到或从字符串的开头添加或删除。

(set 't "some ")
(push "this is " t)
(push "text " t -1)
;-> t is now "this is some text"

pop 始终返回被弹出内容,但 push 返回修改后的目标。当您想分解字符串并按顺序处理这些片段时,它很有用。例如,要打印 newLISP 版本号(存储为 4 位或 5 位整数),请使用类似于以下代码

(set 'version-string (string (sys-info -2)))
; eg: version-string is now "10303"
(set 'dev-version (pop version-string -2 2))   ; always two digits
; dev-version is "03", version-string is "103"
(set 'point-version (pop version-string -1))   ; always one digit
; point-version is "3", version-string is now "10"
(set 'version version-string)                  ; one or two digits
(println version "." point-version "." dev-version)
10.3.03

从字符串的右侧开始工作并使用 pop 提取信息并将其删除在一个操作中更容易。

修改字符串

[编辑 | 编辑源代码]

更改字符串中的字符有两种方法。要么使用字符的索引号,要么指定要查找或更改的子字符串。

在字符串中使用索引号

[编辑 | 编辑源代码]

要按索引号更改字符,请使用 setf,这是更改字符串、列表和数组的通用函数

(set 't "a hypothetical one-dimensional subatomic particle")
(setf (t 0) "A")
;-> "A"
t
;-> "A hypothetical one-dimensional subatomic particle"

您也可以使用 nthsetf 来指定位置

(set 't "a hypothetical one-dimensional subatomic particle")
;-> "a hypothetical one-dimensional subatomic particle"
(setf (nth 0 t) "A")
;-> "A"
t
;-> "A hypothetical one-dimensional subatomic particle"

以下是“递增”字符串第一个(第零个)字母的方法

(set 'wd "cream")
;-> "cream"
(setf (wd 0) (char (+ (char $it) 1)))
;-> "d"
wd
;-> "dream"

$it包含 setf 表达式第一部分找到的值,其数值将递增以形成第二部分。

更改子字符串

[编辑 | 编辑源代码]

如果您不想使用或无法使用索引号或字符位置,请使用 replace,这是一个强大的破坏性函数,它对字符串执行各种有用的操作。在以下形式中使用它

(replace old-string source-string replacement)

所以

(set 't "a hypothetical one-dimensional subatomic particle")
(replace "hypoth" t "theor")
;-> "a theoretical one-dimensional subatomic particle"

replace 是破坏性的,但是如果您想构造性地使用 replace 或其他破坏性函数以获得其副作用,而无需修改原始字符串,请使用 copy 函数

(set 't "a hypothetical one-dimensional subatomic particle")
(replace "hypoth" (copy t) "theor")
;-> "a theoretical one-dimensional subatomic particle"
t
;-> "a hypothetical one-dimensional subatomic particle"

副本被 replace 修改。原始字符串 t 不会受到影响。

正则表达式

[编辑 | 编辑源代码]

replace 是 newLISP 函数组中的一员,这些函数接受正则表达式来定义文本中的模式。对于大多数函数,您会在表达式末尾添加一个额外的数字来指定正则表达式操作的选项:0 表示基本匹配,1 表示不区分大小写匹配,等等。

(set 't "a hypothetical one-dimensional subatomic particle")
(replace {h.*?l(?# h followed by l but not too greedy)} t {} 0) 

;-> "a  one-dimensional subatomic particle"

有时我会在正则表达式中添加注释,以便几天后阅读代码时知道自己想做什么。(?# 和后面的闭合括号之间的文本将被忽略。

如果您习惯使用与 Perl 兼容的正则表达式 (PCRE),您会对 replace 及其使用正则表达式的同类函数 (findregexfind-allparsestarts-withends-withdirectorysearch) 感到满意。完整详细信息请参见 newLISP 参考手册。

您必须将模式引导到 newLISP 阅读器和正则表达式处理器中。请记住引号括起来的字符串和花括号括起来的字符串之间的区别?引号允许处理转义字符,而花括号则不。花括号有一些优势:它们在视觉上相互对齐,没有聪明和愚蠢的版本来混淆您,您的文本编辑器可能会为您平衡它们,并且它们允许您在字符串中使用更常见的引号字符,而无需一直转义它们。但如果您使用引号,则必须将反斜杠加倍,以便单个反斜杠在正则表达式处理器中完好无损

(set 'str "\s")
(replace str "this is a phrase" "|" 0)  ; oops, not searching for \s (white space) ...
;-> thi| i| a phra|e                    ; but for the letter s 

(set 'str "\\s")
(replace str "this is a phrase" "|" 0)
;-> this|is|a|phrase                    ; ah, better!

系统变量:$0、$1 ...

[编辑 | 编辑源代码]

replace 使用系统变量 $0、$1、$2 等(最多 $15)更新匹配项。这些指的是模式中的带括号的表达式,等效于您可能熟悉的 \1、\2(如果您使用过 grep)。例如

(set 'quotation {"I cannot explain." She spoke in a low, eager voice,
with a curious lisp in her utterance. "But for God's sake do what I 
ask you. Go back and never set foot upon the moor again."})

(replace {(.*?),.*?curious\s*(l.*p\W)(.*?)(moor)(.*)} 
    quotation 
    (println {$1 } $1 { $2 } $2 { $3 } $3 { $4 } $4 { $5 } $5)
    4)
$1 "I cannot explain." She spoke in a low $2 lisp  $3 in her utterance.
"But for God's sake do what I ask you. Go back and never set foot upon 
the $4 moor $5 again."

在这里,我们查找了五个模式,它们以逗号开头,以 curious 结尾的任何字符串分隔。$0 存储匹配的表达式,$1 存储第一个带括号的子表达式,依此类推。

如果您希望使用引号而不是我在这里使用的大括号,请记住某些字符必须使用反斜杠进行转义。

替换表达式

[编辑 | 编辑源代码]

前面的示例表明,replace 的一个重要特征是替换不必只是一个简单的字符串或列表,它可以是任何 newLISP 表达式。每次找到模式时,都会评估替换表达式。您可以使用它来提供动态计算的替换值,或者您可以对找到的文本执行其他任何操作。甚至可以评估与找到的文本完全无关的表达式。

以下是一个示例:搜索字母 t 后面是字母 h 或者任何元音,并打印出 replace 找到的组合

(set 't "a hypothetical one-dimensional subatomic particle")
(replace {t[h]|t[aeiou]} t (println $0) 0)
th
ti
to
ti
;-> "a hypothetical one-dimensional subatomic particle"

对于找到的每个匹配文本片段,第三个表达式

(println $0)

被评估。这是查看正则表达式引擎在函数运行时在做什么的好方法。在这个例子中,原始字符串似乎没有改变,但实际上它确实改变了,因为(println $0)做了两件事:它打印了字符串,并将其值返回给 replace,从而用自身替换了找到的文本。无形缝补!如果替换表达式没有返回字符串,则不会进行替换。

您也可以做其他有用的事情,例如为以后处理构建匹配列表,并且您可以使用 newLISP 系统变量和任何其他函数来使用找到的任何文本。

在下一个示例中,我们查找字母 a、e 或 c,并将每个出现项强制转换为大写

(replace "a|e|c" "This is a sentence" (upper-case $0) 0)
;-> "This is A sEntEnCE"

另一个示例,这是一个简单的搜索和替换操作,它统计在字符串中找到字母 'o' 的次数,并将原始字符串中的每个出现项替换为到目前为止的计数。替换是一个表达式块,这些表达式块被组合到单个 begin 表达式中。每次找到匹配项时都会评估此块

(set 't "a hypothetical one-dimensional subatomic particle")
(set 'counter 0)
(replace "o" t 
 (begin 
  (inc counter)
  (println {replacing "} $0 {" number } counter) 
  (string counter))         ; the replacement text should be a string 
 0)
replacing "o" number 1
replacing "o" number 2
replacing "o" number 3
replacing "o" number 4
"a hyp1thetical 2ne-dimensi3nal subat4mic particle"


println 的输出不会出现在字符串中;整个 begin 表达式的最终值是一个字符串版本的计数器,因此它会被插入到字符串中。

这是一个 replace 实践的另一个例子。假设我有一个文本文件,内容如下

1 a = 15
2 another_variable = "strings"
4 x2 = "another string"
5 c = 25 
3x=9


我想编写一个 newLISP 脚本,以 10 为倍数重新编号行,从 10 开始,并将文本对齐,以便等号对齐,如下所示

10 a                   = 15
20 another_variable    = "strings"
30 x2                  = "another string"
40 c                   = 25 
50 x                   = 9

(我不知道这是什么语言!)

以下脚本将执行此操作

(set 'file (open ((main-args) 2) "read"))
(set 'counter 0)
(while (read-line file)
 (set 'temp 
   (replace {^(\d*)(\s*)(.*)}        ; the numbering
     (current-line)
     (string (inc counter 10) " " $3) 
     0))
 (println 
   (replace {(\S*)(\s*)(=)(\s*)(.*)}  ; the spaces around =
    temp 
    (string $1 (dup " " (- 20 (length $1))) $3 " " $5) 
    0)))
(exit)

我在 while 循环中使用了两个 replace 操作,以使事情更清晰。第一个操作将一个临时变量设置为替换操作的结果。搜索字符串({^(\d*)(\s*)(.*)}) 是一个正则表达式,它查找一行开头的任何数字,后面跟着一些空格,再后面跟着任何内容。替换字符串((string (inc counter 10) " " $3) 0)) 由一个增量的计数器值、一个空格和第三个匹配项(即我刚刚查找的任何内容)组成。

第二个替换操作的结果被打印出来。我在临时变量temp中搜索更多带有等号的字符串和空格

({(\S*)(\s*)(=)(\s*)(.*)})

替换表达式是由重要的已找到元素($1、$3、$5)构建的,但它还包括对将等号移到字符 20 所需空格量的快速计算,这应该是第一项宽度与位置 20(我任意选择为等号位置)之间的差值。

对于新手来说,正则表达式并不容易,但它们非常强大,特别是与 newLISP 的 replace 函数结合使用,因此值得学习。

测试和比较字符串

[edit | edit source]

您可以对字符串运行各种测试。newLISP 的比较运算符通过查找和比较字符的代码编号来进行比较,直到可以做出决定。

(> {Higgs Boson} {Higgs boson})         ; nil
(> {Higgs Boson} {Higgs})               ; true
(< {dollar} {euro})                     ; true
(> {newLISP} {LISP})                    ; true
(= {fred} {Fred})                       ; nil
(= {fred} {fred})                       ; true

当然,newLISP 的灵活参数处理允许您同时测试大量字符串。

(< "a" "c" "d" "f" "h") 
;-> true

这些比较函数也允许您使用单个参数。如果您只提供一个参数,newLISP 会非常人性化地假设您指的是 0 或 "",具体取决于第一个参数的类型。

(> 1)                               ; true - assumes > 0
(> "fred")                          ; true - assumes > ""

要检查两个字符串是否具有共同特征,您可以使用 starts-withends-with,也可以使用更通用的模式匹配命令 memberregexfindfind-allstarts-withends-with 足够简单。

(starts-with "newLISP" "new")       ; does newLISP start with new?
;-> true
(ends-with "newLISP" "LISP")
;-> true

它们也可以接受正则表达式,使用其中一个正则表达式选项(0 是最常用的)。

(starts-with {newLISP} {[a-z][aeiou](?\#lc followed by lc vowel)} 0)
;-> true
(ends-with {newLISP} {[aeiou][A-Z](?\# lc vowel followed by UCase)} 0)
;-> false

findfind-allmemberregex 在字符串中查找所有内容。find 返回匹配子字符串的索引。

(set 't "a hypothetical one-dimensional subatomic particle")
(find "atom" t)
;-> 34

(find "l" t)
;-> 13

(find "L" t)
;-> nil                             ; search is case-sensitive

member 检查一个字符串是否在另一个字符串中。它返回包括搜索字符串在内的字符串的剩余部分,而不是第一个出现的索引。

(member "rest" "a good restaurant")
;-> "restaurant"

findmember 都允许您使用正则表达式。

(set 'quotation {"I cannot explain." She spoke in a low,
eager voice, with a curious lisp in her utterance. "But for
Gods sake do what I ask you. Go back and never set foot upon
the moor again."})

(find "lisp" quotation)            ; without regex
;-> 69                             ; character 69

(find {i} quotation 0)             ; with regex
;-> 15                             ; character 15

(find {s} quotation 1)             ; case insensitive regex
;-> 20                             ; character 20

(println "character " 
 (find {(l.*?p)} quotation 0) ": " $0)  ; l followed by a p
;-> character 13: lain." She sp

find-all 的工作原理类似于 find,但返回所有匹配字符串的列表,而不是第一个匹配项的索引。它始终接受正则表达式,因此您不需要在末尾添加正则表达式选项数字。

(set 'quotation {"I cannot explain." She spoke in a low,
eager voice, with a curious lisp in her utterance. "But for
Gods sake do what I ask you. Go back and never set foot upon
the moor again."})

(find-all "[aeiou]{2,}" quotation $0)       ; two or more vowels
;-> ("ai" "ea" "oi" "iou" "ou" "oo" "oo" "ai")

或者您可以使用 regex。如果字符串不包含模式,它将返回 nil,但如果包含模式,它将返回一个列表,其中包含匹配的字符串和子字符串,以及每个字符串的开始位置和长度。结果可能非常复杂。

(set 'quotation 
 {She spoke in a low, eager voice, with a curious lisp in her utterance.})

(println (regex {(.*)(l.*)(l.*p)(.*)} quotation 0))
("She spoke in a low, eager voice, with a curious lisp in
her utterance." 0 70 "She spoke in a " 0 15 "low, eager
voice, with a curious " 15 33 "lisp" 48 4 " in her
utterance." 52 18)


此结果列表可以解释为“第一个匹配项从字符 0 开始,持续 70 个字符,第二个匹配项从字符 0 开始,持续 15 个字符,另一个匹配项从字符 15 开始,持续 33 个字符”,等等。

匹配项也被存储在系统变量($0、$1、...)中,您可以使用简单的循环轻松检查它们。

(for (x 1 4)
 (println {$} x ": " ($ x)))
$1: She spoke in a 
$2: low, eager voice, with a curious 
$3: lisp 
$4: in her utterance.

字符串到列表

[edit | edit source]

有两个函数可以将字符串转换为列表,以便使用 newLISP 强大的列表处理功能进行操作。命名为 explode 的函数可以打开一个字符串,并返回一个包含单个字符的列表。

(set 't "a hypothetical one-dimensional subatomic particle")
(explode t)

:-> ("a" " " "h" "y" "p" "o" "t" "h" "e" "t" "i" "c" "a" "l"
" " "o" "n" "e" "-" "d" "i" "m" "e" "n" "s" "i" "o" "n" "a"
"l" " " "s" "u" "b" "a" "t" "o" "m" "i" "c" " " "p" "a" "r"
"t" "i" "c" "l" "e")


可以使用 join 轻松反转爆炸操作。explode 还可以接受一个整数。这定义了片段的大小。例如,要将字符串分成密码学家风格的 5 个字母组,请删除空格并使用 explode,如下所示

(explode (replace " " t "") 5)
;-> ("ahypo" "theti" "calon" "e-dim" "ensio" "nalsu" "batom" "icpar" "ticle")

您可以使用 find-all 执行类似的操作。注意末尾部分。

(find-all ".{3}" t)                 ; this regex drops chars!
;-> ("a h" "ypo" "the" "tic" "al " "one" "-di" "men" 
; "sio" "nal" " su" "bat" "omi" "c p" "art" "icl")

解析字符串

[edit | edit source]

parse 是将字符串分解并返回各个部分的强大方法。单独使用时,它会将字符串分解,通常在词语边界处,吃掉边界,并返回一个包含剩余部分的列表。

(parse t)                               ; defaults to spaces...
;-> ("a" "hypothetical" "one-dimensional" "subatomic" "particle")

或者您可以提供一个分隔符字符,parse 就会在遇到该字符时分解字符串。

(set 'pathname {/System/Library/Fonts/Courier.dfont})
(parse pathname {/})
;-> ("" "System" "Library" "Fonts" "Courier.dfont")

顺便说一下,我可以通过过滤掉第一个空字符串来消除列表中的第一个空字符串。

(clean empty? (parse pathname {/}))
;-> ("System" "Library" "Fonts" "Courier.dfont")

您还可以指定分隔符字符串而不是分隔符字符。

(set 't (dup "spam" 8))
;-> "spamspamspamspamspamspamspamspam"

(parse t {am})                          ; break on "am"
;-> ("sp" "sp" "sp" "sp" "sp" "sp" "sp" "sp" "")

但最棒的是,您可以指定一个正则表达式分隔符。请确保您提供了选项标志(0 或任何其他值),就像 newLISP 中的大多数正则表达式函数一样。

(set 't {/System/Library/Fonts/Courier.dfont})
(parse t {[/aeiou]} 0)                  ; split at slashes and vowels
;-> ("" "Syst" "m" "L" "br" "ry" "F" "nts" "C" "" "r" "" "r.df" "nt")

这是一个众所周知的快速且不太可靠的 HTML 标签剥离器。

(set 'html (read-file "/Users/Sites/index.html"))
(println (parse html {<.*?>} 4))        ; option 4: dot matches newline

为了解析 XML 字符串,newLISP 提供了 xml-parse 函数。请参阅 使用 XML

使用 parse 处理文本时要小心。除非您准确指定需要的内容,否则它会认为您传递给它的是 newLISP 源代码。这可能会产生意想不到的结果。

(set 't {Eats, shoots, and leaves ; a book by Lynn Truss})
(parse t)
;-> ("Eats" "," "shoots" "," "and" "leaves")    ; she's gone!

分号在 newLISP 中被视为注释字符,因此 parse 忽略了它以及该行上的所有后续内容。使用分隔符或正则表达式告诉它您真正想要的内容。

(set 't {Eats, shoots, and leaves ; a book by Lynn Truss})
(parse t " ")
;-> ("Eats," "shoots," "and" "leaves" ";" "a" "book" "by" "Lynn" "Truss")

(parse t "\\s" 0)                   ; white space
;-> ("Eats," "shoots," "and" "leaves" ";" "a" "book" "by" "Lynn" "Truss")

如果您想以其他方式分割字符串,请考虑使用 find-all,它返回一个包含匹配模式的字符串列表。如果您能够将分割操作指定为正则表达式,那么您就很幸运。例如,如果您想将数字分成三个数字一组,请使用此技巧。

(set 'a "1212374192387562311")
(println (find-all {\d{3}|\d{2}$|\d$} a))
;-> ("121" "237" "419" "238" "756" "231" "1")

; alternatively
(explode a 3)
;-> ("121" "237" "419" "238" "756" "231" "1")

该模式必须考虑末尾剩下 2 位或 1 位数字的情况。

parse 在分隔符完成工作后会将其吃掉 - find-all 会查找并返回它找到的内容。

(find-all {\w+} t )                     ; word characters
;-> ("Eats" "shoots" "and" "leaves" "a" "book" "by" "Lynn" "Truss")

(parse t {\w+} 0 )                      ; eats and leaves delimiters
;-> ("" ", " ", " " " "; " " " " " " " " " "")

其他字符串函数

[edit | edit source]

还有其他函数可以处理字符串。search 在磁盘上的文件中查找字符串。

(set 'f (open {/private/var/log/system.log} {read}))
(search f {kernel})
(seek f (- (seek f) 64))                ; rewind file pointer
(dotimes (n 3)
 (println (read-line f)))
(close f)

此示例在 system.log 中查找字符串kernel。如果找到,newLISP 会将文件指针倒带 64 个字符,然后打印三行,显示上下文中的行。

还有用于处理 base64 编码文件以及加密字符串的函数。

格式化字符串

[edit | edit source]

值得一提的是 format 函数,它允许您将 newLISP 表达式的值插入到预定义的模板字符串中。使用 %s 表示模板中字符串表达式的占位符,使用其他 % 代码包含数字。例如,假设您想显示一个类似这样的文件列表

folder: Library
 file:  mach

适合文件夹(目录)的模板如下所示

"folder: %s" ; or
"  file: %s"

将模板字符串和产生文件或文件夹名称的表达式 (f) 传递给 format 函数

(format "folder: %s" f) ; or
(format "  file: %s" f)

当它被计算时,f 的内容将被插入到字符串中 %s 所在的位置。使用 directory 函数以这种格式生成目录列表的代码如下所示

(dolist (f (directory)) 
 (if (directory? f)
  (println (format "folder: %s" f))
  (println (format "  file: %s" f))))

我使用 directory? 函数来选择正确的模板字符串。典型的列表如下所示

folder: .
folder: ..
  file: .DS_Store
  file: .hotfiles.btree
folder: .Spotlight-V100
folder: .Trashes
folder: .vol
  file: .VolumeIcon.icns
folder: Applications
folder: Applications (Mac OS 9)
folder: automount
folder: bin
folder: Cleanup At Startup
folder: cores
...

您可以使用许多格式化代码来生成您想要的输出。您可以使用数字控制字符串和数字的对齐方式和精度。只要确保格式字符串中的 % 结构与后面出现的表达式或符号匹配,并且每个结构的数量相同。

这里还有另一个示例。我们将以十进制、十六进制和二进制形式显示前 400 个左右的 Unicode 字符。我们将使用 bits 函数来生成二进制字符串。我们将三个值的列表传递给 format,并在格式字符串后面添加三个条目

(for (x 32 0x01a0)
 (println (char x)                 ; the character, then
   (format "%4d\t%4x\t%10s"        ; decimal \t hex \t binary-string
    (list x x (bits x)))))
   32       20     100000
!  33       21     100001
"  34       22     100010
#  35       23     100011
$  36       24     100100
%  37       25     100101
&  38       26     100110
'  39       27     100111
(  40       28     101000
)  41       29     101001
...

让 newLISP 思考的字符串

[edit | edit source]

最后,我必须提到 evaleval-string。这两个函数都允许您将 newLISP 代码传递给 newLISP 进行计算。如果它是有效的 newLISP 代码,您将看到计算结果。eval 接受一个表达式

(set 'expr '(+ 1 2))
(eval expr)
;-> 3

eval-string 接受一个字符串

(set 'expr "(+ 1 2)")
(eval-string expr)
;-> 3

这意味着您可以使用我们已经遇到的任何函数构建 newLISP 代码,然后让 newLISP 对其进行评估。eval 在定义宏(在您选择执行之前延迟评估的函数)时特别有用。请参阅

您可以使用 evaleval-string 编写编写程序的程序。

以下一段有趣的 newLISP 代码会不断地无意识地重新排列一些字符串并尝试对其进行评估。未成功尝试将被安全地捕获。当它最终成为有效的 newLISP 代码时,它将被成功地评估,并且结果将满足结束条件并结束循环。

(set 'code '(")" "set" "'valid" "true" "("))
(set 'valid nil)
(until valid
 (set 'code (randomize code))
 (println (join code " "))
 (catch (eval-string (join code " ")) 'result))
true 'valid set ) (
) ( set true 'valid
'valid ( set true )
set 'valid true ( )
'valid ) ( true set
set true ) ( 'valid
true ) ( set 'valid
'valid ( true ) set
true 'valid ( ) set
'valid ) ( true set
true ( 'valid ) set
set ( 'valid ) true
set true 'valid ( )
( set 'valid true )

我使用过显然是使用这种编程技术编写的程序……

应用和映射:将函数应用于列表

[edit | edit source]

使函数和数据协同工作

[edit | edit source]

通常,您会发现自己有一些数据存储在一个列表中,并且想要将函数应用于它。例如,假设您从太空探测器中获取了一些温度读数,它们存储在一个名为 data 的列表中

(println data)
(0.1 3.2 -1.2 1.2 -2.3 0.1 1.4 2.5 0.3)


您将如何将这些数字加起来,然后除以总数以找到平均值?也许您认为可以使用 add,它对浮点数列表求和,但是您不是交互式工作的,因此您无法编辑代码使其像这样读取

(add 0.1 3.2 -1.2 1.2 -2.3 0.1 1.4 2.5 0.3)

由于我们将数据保存在名为 data 的符号中,因此我们可以尝试这样做

(add data)
value expected in function add : data


但是不行,这行不通,因为 add 需要数字来加,而不是列表。当然,您可以使用困难的方法,编写一个循环,遍历列表中的每个项目,并每次增加一个运行总数

(set 'total 0)
(dolist (i data)
 (inc total i))

(println total)
5.3


这可以正常工作。但是 newLISP 针对此问题和其他许多问题提供了一个更强大的解决方案:您可以将函数视为数据,将数据视为函数,因此您可以像操作数据一样轻松地操作函数。您可以简单地将 add 和数据列表“引入”到彼此,然后退后一步,让它们自行处理。

有两个重要的函数可以做到这一点:applymap

apply

[edit | edit source]

apply 接受一个函数和一个列表,并使它们协同工作

(apply add data)
;-> 5.3
(div (apply add data) (length data))
;-> 0.5888888889

这将产生所需的结果。在这里,我们将 add 函数视为任何其他 newLISP 列表、字符串或数字,将其用作另一个函数的参数。您无需引用它(尽管您可以引用),因为 apply 已经期望函数的名称。

另一个可以使函数和列表协同工作的函数是 map,它将函数逐一应用于列表的每个项目。例如,如果您想将 floor 函数应用于数据列表的每个元素(将其向下舍入到最接近的整数),您可以将 mapfloor 和数据组合如下

(map floor data)
;-> (0 3 -2 1 -3 0 1 2 0)

并且 floor 函数将应用于数据的每个元素。结果将被组合并返回到一个新列表中。

更详细地介绍 apply 和 map

[edit | edit source]

applymap 都允许您将函数视为数据。它们具有相同的基本形式

(apply f l)
(map f l)

其中 f 是函数的名称,l 是列表。基本思想是您告诉 newLISP 使用您指定的函数处理列表。

apply and map use functions on list elements

apply 函数将列表中的所有元素用作函数的参数,并评估结果。

(apply reverse '("this is a string"))
;-> "gnirts a si siht"

在这里,apply 查看列表(在本例中,它包含一个字符串),并将元素作为参数提供给函数。该字符串将被反转。请注意,您无需引用函数,但您确实需要引用列表,因为您不希望 newLISP 在指定函数有机会使用它之前对其进行评估。

另一方面,map 函数会像军官检查士兵队列一样逐个遍历列表,并将函数依次应用于每个元素,使用该元素作为参数。但是,map 会记住每次评估的结果,将其收集起来,并将其返回到一个新列表中。

因此,map 看起来像是一个控制流词,有点像 dolist,而 apply 是在程序中控制 newLISP 列表评估过程的一种方法,它可以在您想要调用函数时和您想要调用函数的位置调用函数,而不仅仅是作为正常评估过程的一部分。

如果我们为 map 调整前面的示例,它会给出类似的结果,尽管结果是一个列表,而不是一个字符串

(map reverse '("this is a string"))
;-> ("gnirts a si siht")

因为我们使用了一个只有一个元素的列表,所以结果与 apply 示例几乎相同,尽管请注意 map 返回一个列表,而在本例中,apply 返回一个字符串

(apply reverse '("this is a string"))
;-> "gnirts a si siht"

该字符串已从列表中提取,反转,然后存储在 map 创建的另一个列表中。

在下面的示例中

(map reverse '("this" "is" "a" "list" "of" "strings"))
;-> ("siht" "si" "a" "tsil" "fo" "sgnirts")

您可以清楚地看到 map 已将 reverse 依次应用于列表的每个元素,并返回了一个包含结果反转字符串的列表。

用另一个函数来写它?

[edit | edit source]

为了说明这两个函数之间的关系,这里尝试用 apply 来定义 map

(define (my-map f l , r) 
 ; declare a local variable r to hold the results
 (dolist (e l)
   (push (apply f (list e)) r -1)))

我们正在将将函数 f 应用于每个列表项的结果推送到临时列表的末尾,然后依赖 push 在最后返回列表,就像 map 一样。这可以工作,至少对于简单的表达式

(my-map explode '("this is a string"))
;-> ("t" "h" "i" "s" " " "i" "s" " " "a" " " "s" "t" "r" "i" "n" "g")

(map explode '("this is a string"))
;-> (("t" "h" "i" "s" " " "i" "s" " " "a" " " "s" "t" "r" "i" "n" "g"))

这个示例说明了为什么 map 如此有用。它是一种简单的方法,可以转换列表的所有元素,而无需使用 dolist 表达式逐个遍历它们。

更多技巧

[edit | edit source]

mapapply 都有一些额外的技巧。map 可以同时遍历多个列表。如果您提供两个或多个列表,newLISP 会将每个列表的元素交织在一起,从每个列表的第一个元素开始,并将它们作为参数传递给函数

(map append '("cats " "dogs " "birds ") '("miaow" "bark" "tweet"))
;-> ("cats miaow" "dogs bark" "birds tweet")

在这里,每个列表的第一个元素将作为一对传递给 append,然后是每个列表的第二个元素,依此类推。

这种将线编织在一起的做法有点像用列表编织。或者像拉上拉链。

apply 也有一招。第三个参数指示函数应该使用多少个前面的列表参数。因此,如果一个函数需要两个参数,而您提供三个或更多参数,apply 会回来并再次尝试,使用第一次应用的结果和另一个参数。它会继续遍历列表,直到所有参数都用完为止。

为了实际演示这一点,让我们先定义一个函数,它接受两个参数并比较它们的长度

(define (longest s1 s2)
 (println s1 " is longest so far, is " s2 " longer?") ; feedback
 (if (>= (length s1) (length s2))                     ; compare 
     s1
     s2))

现在,您可以将此函数应用于一个字符串列表,使用第三个参数告诉 apply 一次使用两个字符串的论据

(apply longest '("green" "purple" "violet" "yellow" "orange"
"black" "white" "pink" "red" "turquoise" "cerise" "scarlet"
"lilac" "grey" "blue") 2)
green is longest so far, is purple longer?
purple is longest so far, is violet longer?
purple is longest so far, is yellow longer?
purple is longest so far, is orange longer?
purple is longest so far, is black longer?
purple is longest so far, is white longer?
purple is longest so far, is pink longer?
purple is longest so far, is red longer?
purple is longest so far, is turquoise longer?
turquoise is longest so far, is cerise longer?
turquoise is longest so far, is scarlet longer?
turquoise is longest so far, is lilac longer?
turquoise is longest so far, is grey longer?
turquoise is longest so far, is blue longer?
turquoise


这就像在海滩上散步,发现一块鹅卵石,并一直拿着它,直到出现一块更好的鹅卵石。

apply 还为您提供了一种方法,可以遍历列表并将函数应用于每一对项目

(apply (fn (x y)
    (println {x is } x {, y is } y)) (sequence 0 10) 2)
x is 0, y is 1
x is 1, y is 2
x is 2, y is 3
x is 3, y is 4
x is 4, y is 5
x is 5, y is 6
x is 6, y is 7
x is 7, y is 8
x is 8, y is 9
x is 9, y is 10


这里发生的事情是,println 函数返回的值是该对的第二个成员,它将成为下一对的第一个元素的值。

Lisp 特性

[edit | edit source]

将函数的名称像数据一样传递来回,这是 newLISP 的一个非常典型的特性,而且非常有用。您会发现它有许多用途,有时会使用您认为无法与 map 一起使用的函数。例如,这里有 setmap 的控制下努力工作

(map set '(a b) '(1 2))
;-> a is 1, b is 2

(map set '(a b) (list b a))
;-> a is 2, b is 1

这种结构为您提供了一种以并行方式而不是顺序方式将值分配给符号的另一种方法。(您也可以使用 swap。)

map 的一些用途很简单

(map char (explode "hi there"))
;-> (104 105 32 116 104 101 114 101)

(map (fn (h) (format "%02x" h)) (sequence 0 15))
;-> ("00" "01" "02" "03" "04" "05" "06" "07" "08" "09" "0a" "0b" "0c" "0d" "0e" "0f")

其他的可能变得相当复杂。例如,给定以这种形式存储在符号 image-data 中的一串数据

("/Users/me/graphics/file1.jpg" "  pixelHeight: 978" "  pixelWidth: 1181")

可以使用以下方法提取这两个数字

(map set '(height width) (map int (map last (map parse (rest image-data)))))

柯里化

[edit | edit source]

一些内置的 newLISP 函数对其他函数执行操作。一个示例是 curry,它创建一个双参数函数的副本,并创建一个具有预先确定第一个参数的单参数版本。因此,如果一个函数 f1 通常像这样调用

(f1 arg1 arg2)

你可以使用curry创建一个新的函数f2,该函数带有一个可直接使用的内置arg1

(set 'f2 (curry f1 arg1))

现在你可以忘记第一个参数,只需要给f2提供第二个参数。

(f2 arg2)

为什么这有用?考虑dup函数,它经常被用来插入多个空格。

(dup { } 10)

使用curry,你可以创建一个新函数,例如blank,它是dup的一个特殊版本,始终以空格作为字符串。

(set 'blank (curry dup { }))

现在你可以使用(blank n)

(blank 10)
;->           ; 10 spaces

curry可以用来创建带有map的临时或匿名函数。

(map (curry pow 2) (sequence 1 10))
;-> (2 4 8 16 32 64 128 256 512 1024)

(map (fn (x) (pow 2 x)) (sequence 1 10)) ; equivalent
;-> (2 4 8 16 32 64 128 256 512 1024)

但是避免在诸如inc之类的破坏性函数上使用curry

(setq a-list-of-pairs (sequence 2 10 2))
;-> (2 4 6 8 10)
(map (curry inc 3) a-list-of-pairs) ;-> you would expect (5 7 9 11 13), instead you get
;-> (5 9 15 23 33)
; one proper way to get every number incremented by 3 would be
(map (curry + 3) a-list-of-pairs)
;-> (5 7 9 11 13)
; or if you insist in using inc, then provide a copy of the increment so the reference inc gets doesn't mess up things
(map (curry inc (copy 3)) a-list-of-pairs)
;-> (5 7 9 11 13)

介绍上下文

[edit | edit source]

我们都喜欢将自己的东西整理到不同的区域或隔间。厨师将鱼、肉和甜点区域分开,电子工程师将电源供应器与射频和音频阶段分开,newLISP程序员使用上下文来组织代码。

什么是上下文?

[edit | edit source]

newLISP上下文为符号提供了一个命名的容器。不同上下文中的符号可以具有相同的名称而不会发生冲突。因此,例如,在一个上下文中,我可以将名为meaning-of-life的符号定义为值为42,但在另一个上下文中,同名符号的值可以是dna-propagation,而在另一个上下文中,该符号的值可以是worship-of-deity

除非你明确地选择创建和/或切换上下文,否则你所有的newLISP工作都在默认上下文MAIN中进行。在本手册中,当创建新的符号时,它们被添加到MAIN上下文中。

上下文非常灵活,你可以根据手头的任务将它们用于字典、软件对象或超级函数。

上下文:基本知识

[edit | edit source]

context函数可用于执行许多不同的任务。

  • 创建新的上下文。
  • 从一个上下文切换到另一个上下文。
  • 检索上下文中的现有符号的值。
  • 查看当前所在的上下文。
  • 在上下文中创建新的符号并为其分配值。

newLISP通常能够读懂你的想法,并根据你使用context函数的方式知道你想要做什么。例如

(context 'Test)

创建一个名为Test的新上下文,正如你所预期的那样。如果在交互式模式下输入此代码,你会看到newLISP更改提示以告知你当前正在另一个上下文中工作。

> (context 'Test)
Test
Test> 

你可以自由地在上下文之间切换。

> (context MAIN)
MAIN
> (context Test)
Test
Test> 

单独使用时,它只会告诉你当前所在的上下文。

> (context)
MAIN
>

一旦上下文存在,你不必引用名称(但你也可以引用,如果你愿意的话)。请注意,我为上下文名称使用了大写字母。这不是强制性的,只是一种约定。

上下文包含符号及其值。有多种方法可以创建符号并为其赋予值。

> (context 'Doyle "villain" "moriarty")
"moriarty"
>

这会创建一个新的上下文 - 注意引号,因为newLISP以前从未见过它 - 并创建一个名为“villain”的新符号,其值为“Moriarty”,但停留在MAIN上下文中。如果上下文已经存在,你可以省略引号。

> (context Doyle "hero" "holmes")
"holmes"
> 

要获取符号的值,你可以这样做

> (context Doyle "hero")
"holmes"
>

或者,如果使用控制台,你可以逐步执行此操作

> (context Doyle)
Doyle
Doyle> hero
"holmes"
Doyle> 

或者,从MAIN上下文

> Doyle:hero
"holmes"
>

符号的完整地址是上下文名称,后跟冒号(:),后跟符号名称。如果你在另一个上下文中,始终使用完整地址。

要查看上下文中的所有符号,请使用symbols生成一个列表。

(symbols Doyle)
;-> (Doyle:hero Doyle:period Doyle:villain)

或者,如果你已在Doyle上下文中

> (symbols)
;-> (hero period villain)

你可以以通常的方式使用此符号列表,例如使用dolist对其进行遍历。

(dolist (s (symbols Doyle))
 (println s))
Doyle:hero
Doyle:period
Doyle:villain

要查看每个符号的值,请使用eval查找其值,并使用term仅返回符号的名称。

(dolist (s (symbols Doyle))
 (println (term s) " is " (eval s)))
hero is Holmes
period is Victorian
villain is Moriarty

在上下文中循环遍历符号有一种更有效(稍微快一点)的技术。使用dotree函数。

(dotree (s Doyle)
 (println (term s) " is " (eval s)))
hero is Holmes
period is Victorian
villain is Moriarty

隐式创建上下文

[edit | edit source]

除了使用context显式创建上下文之外,你还可以让newLISP自动为你创建上下文。例如

(define (C:greeting) 
  (println "greetings from context " (context)))
 
(C:greeting)
greetings from context C

在这里,newLISP创建了一个新的上下文C和一个名为greeting的函数,该函数位于该上下文中。你也可以通过这种方式创建符号

(define D:greeting "this is the greeting string of context D")
(println D:greeting)
this is the greeting string of context D

在这两个示例中,请注意你始终停留在MAIN上下文中。

以下代码创建了一个名为L的新上下文,其中包含一个名为ls的新列表,该列表包含字符串。

(set 'L:ls '("this" "is" "a" "list" "of" "strings"))
;-> ("this" "is" "a" "list" "of" "strings")

上下文中的函数

[edit | edit source]

上下文可以包含函数和符号。要在除MAIN之外的上下文中创建函数,请执行以下操作

(context Doyle)                         ; switch to existing context
(define (hello-world)                   ; define a local function
 (println "Hello World"))

或者执行以下操作

(context MAIN)                          ; stay in MAIN
(define (Doyle:hello-world)             ; define function in context
 (println "Hello World"))

第二种语法允许你在上下文中创建上下文和函数,同时始终安全地停留在MAIN上下文中。

(define (Moriarty:helloworld)
 (println "(evil laugh) Hello World"))

你无需在此处引用新的上下文名称,因为我们正在使用define,而define(根据定义)并不期望现有符号的名称。

要在另一个上下文中使用函数,请记住使用context:function语法调用它们。

默认函数

[edit | edit source]

如果上下文中符号的名称与上下文相同,则称为默认函数(尽管实际上它可以是函数,也可以是包含列表或字符串的符号)。例如,这是一个名为Evens的上下文,它包含一个名为Evens的符号。

(define Evens:Evens (sequence 0 30 2))
;-> (0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30)
Evens:Evens
;-> (0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30)

这是一个名为Double的函数,位于名为Double的上下文中。

(define (Double:Double x)
   (mul x 2))

因此,Evens和Double是其上下文的默认函数。

默认函数有很多优点。如果默认函数的名称与上下文相同,则在表达式中使用上下文名称时,它将被执行,除非newLISP期望上下文名称。例如,虽然你可以始终使用context函数以通常的方式切换到Evens上下文

> (context Evens)
Evens
Evens> (context MAIN)
MAIN
>

你可以将Evens用作列表(因为Evens:Evens是一个列表)

(reverse Evens)
;-> (30 28 26 24 22 20 18 16 14 12 10 8 6 4 2 0)

你可以在不提供完整地址的情况下使用默认函数。类似地,你可以将Double函数用作普通函数,而不提供完整的冒号分隔地址

> (Double 3)
6

你仍然可以以通常的方式切换到Double上下文

> (context Double)
Double
Double>

newLISP足够聪明,可以从你的代码中判断是使用上下文的默认函数还是上下文本身。

按引用传递参数

[edit | edit source]

当默认函数用作符号时,它们与其更普通的同类之间存在重要区别。当使用默认函数将数据传递给函数时,newLISP使用对数据的引用而不是副本。对于较大的列表和字符串,引用对于newLISP在函数之间传递要快得多,因此如果你可以将数据存储为默认函数并将上下文名称用作参数,那么你的代码将更快。

此外,作为结果,函数会更改作为引用参数传递的任何默认函数的内容。普通符号在作为参数传递时会被复制。观察以下代码。我将创建两个符号,其中一个是“默认函数”,另一个是普通符号。

(define Evens:Evens (sequence 0 30 2))   ; symbol is the default function for the Evens context
;-> (0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30)

(define odds (sequence 1 31 2))          ; ordinary symbol
;-> (1 3 5 7 9 11 13 15 17 19 21 23 25 27 29 31)

; this function reverses a list

(define (my-reverse lst)
  (reverse lst))

(my-reverse Evens)                       ; default function as parameter
;-> (30 28 26 24 22 20 18 16 14 12 10 8 6 4 2 0)

(my-reverse odds)                        ; ordinary symbol as parameter
;-> (31 29 27 25 23 21 19 17 15 13 11 9 7 5 3 1)

到目前为止,它们看起来行为相同。但现在检查原始符号。

> Evens:Evens
(30 28 26 24 22 20 18 16 14 12 10 8 6 4 2 0)
> odds
(1 3 5 7 9 11 13 15 17 19 21 23 25 27 29 31)

作为引用传递的列表 - 被修改了,而普通的列表参数被复制了,如往常一样,没有被修改。

具有记忆功能的函数

[edit | edit source]

在以下示例中,我们创建一个名为Output的上下文,并在其中创建一个默认函数,也称为Output。此函数打印其参数,并将计数器增加输出字符数。由于默认函数的名称与上下文相同,因此在表达式中使用上下文名称时,它将被执行。

在此函数内部,如果存在counter变量(位于Output上下文中)的值,则将其增加;如果不存在,则创建并初始化。然后执行函数的主要任务 - 打印参数。counter符号记录输出字符数。

(define (Output:Output)                 ; define the default function
 (unless Output:counter
  (set 'Output:counter 0))
 (inc Output:counter (length (string (args))))
 (map print (args))
 (println))

(dotimes (x 90)
 (Output                                ; use context name as a function
 "the square root of " x " is " (sqrt x)))

(Output "you used " Output:counter " characters")
the square root of 0 is 0
the square root of 1 is 1
the square root of 2 is 1.414213562
the square root of 3 is 1.732050808
the square root of 4 is 2
the square root of 5 is 2.236067977
the square root of 6 is 2.449489743
the square root of 7 is 2.645751311
the square root of 8 is 2.828427125
the square root of 9 is 3
...
the square root of 88 is 9.38083152
the square root of 89 is 9.433981132
you used 3895 characters


Output函数实际上会记住自创建以来完成的工作量。它甚至可以将该信息追加到日志文件中。

想想可能性。你可以记录所有函数的使用情况,并根据用户的使用频率向他们收费。

你可以覆盖内置的println函数,以便在调用它时使用此代码。请参阅按你自己的方式

字典和表格

[edit | edit source]

上下文的一个常见用途是字典:一个有序的唯一键值对集合,排列方式使得您可以获取键的当前值,或添加新的键值对。newLISP 使创建字典变得容易。为了说明,我将借助伟大的侦探夏洛克·福尔摩斯。首先,我从 Project Gutenberg 下载了亚瑟·柯南·道尔的《四签名》,然后我将该文件加载为一个单词列表。

(set 'file "/Users/me/Sherlock Holmes/sign-of-four.txt")
(set 'data (clean empty? (parse (read-file file) "\\W" 0)))  ;read file and remove all white-spaces, returns a list.

接下来,我定义一个空字典

(define Doyle:Doyle)

这定义了 Doyle 上下文和默认函数,但将该默认函数保留为未初始化状态。如果默认函数为空,则可以使用以下表达式来构建和检查字典

  • (Doyle key value) - 将键设置为值
  • (Doyle key) - 获取键的值
  • (Doyle key nil) - 删除键

要从单词列表构建字典,您需要扫描所有单词,如果单词不在字典中,则将其添加为键,并将值设置为 1。但如果单词已在字典中,则获取该值,将其加 1,并保存新值

(dolist (word data)
  (set 'lc-word (lower-case word))
  (if (set 'tally (Doyle lc-word))
      (Doyle lc-word (inc tally))
      (Doyle lc-word 1)))

这个更短的替代方法消除了条件语句

(dolist (word data)
   (set 'lc-word (lower-case word))
   (Doyle lc-word (inc (int (Doyle lc-word)))))

或者,更短一点

(dolist (word data)
   (Doyle (lower-case word) (inc (Doyle (lower-case word)))))

每个单词都被添加到字典中,并且值(出现次数)增加 1。在上下文内部,键的名称已添加下划线前缀("_")。这样做是为了避免任何人在键的名称和 newLISP 保留字之间产生混淆,而许多保留字都出现在柯南·道尔的文本中。

您可以通过多种方式浏览字典。要查看单个符号

(Doyle "baker")
;-> 10
(Doyle "street")
;-> 26

要查看符号在上下文中存储的方式,请使用 **dotree** 遍历上下文,并评估每个符号

(dotree (wd Doyle)
   (println wd { } (eval wd)))
Doyle:Doyle nil
Doyle:_1 1
Doyle:_1857 1
Doyle:_1871 1
Doyle:_1878 2
Doyle:_1882 3
Doyle:_221b 1
...
Doyle:_your 107
Doyle:_yours 7
Doyle:_yourself 9
Doyle:_yourselves 2
Doyle:_youth 3
Doyle:_zigzag 1
Doyle:_zum 2

要将字典显示为关联列表,请单独使用字典名称。这将创建一个新的关联列表:

(Doyle)
;-> (("1" 1) 
 ("1857" 1) 
 ("1871" 1) 
 ("1878" 2) 
 ("1882" 3) 
 ("221b" 1) 
 ...
 ("you" 543) 
 ("young" 19) 
 ("your" 107) 
 ("yours" 7) 
 ("yourself" 9) 
 ("yourselves" 2) 
 ("youth" 3) 
 ("zigzag" 1) 
 ("zum" 2))

这是一个标准的关联列表,您可以使用列表章节中描述的函数访问它(请参见关联列表)。例如,要查找出现 20 次的所有单词,请使用 **find-all**

(find-all '(? 20) (Doyle) (println $0))
;-> ("friends" 20)
("gone" 20)
("seemed" 20)
("those" 20)
("turned" 20)
("went" 20)

关联列表由以下代码返回(Doyle)是字典中数据的临时副本,而不是原始字典上下文。要更改数据,请不要对这个临时列表进行操作,而是使用键值访问技术对上下文的数据进行操作。

您还可以使用关联列表形式的数据向字典添加新条目,或修改现有条目

(Doyle '(("laser" 0) ("radar" 0)))

保存和加载上下文

[edit | edit source]

如果您想再次使用字典,您可以将上下文保存在文件中

(save "/Users/me/Sherlock Holmes/doyle-context.lsp" 'Doyle)

这个包含在名为 Doyle 的上下文中的数据集合可以被另一个脚本或 newLISP 会话快速加载,方法是

(load "/Users/me/Sherlock Holmes/doyle-context.lsp")

newLISP 将自动重新创建 Doyle 上下文中的所有符号,并在完成时切换回 MAIN(默认)上下文。

使用 newLISP 模块

[edit | edit source]

上下文用作软件模块的容器,因为它们提供词法隔离的命名空间。newLISP 安装中提供的模块通常定义一个上下文,该上下文包含一组用于处理特定领域的任务的函数。

以下是一个示例。POP3 模块允许您检查 POP3 电子邮件帐户。您首先加载模块

(load "/usr/share/newlisp/modules/pop3.lsp")

模块现在已添加到 newLISP 系统中。您可以切换到上下文

(context POP3)

并调用上下文中的函数。例如,要检查您的电子邮件,请使用 **get-mail-status** 函数,并提供用户名、密码和 POP3 服务器名称

(get-mail-status "[email protected]" "secret" "mail.example.com")
;-> (3 197465 37)
; (totalMessages, totalBytes, lastRead)

如果您没有切换到上下文,您仍然可以通过提供完整地址来调用相同的函数

(POP3:get-mail-status "[email protected]" "secret" "mail.example.com")

作用域

[edit | edit source]

您已经看到了 newLISP 如何动态查找符号的 **当前** 版本(请参见作用域)。但是,当您使用上下文时,您可以采用不同的方法,程序员称之为词法作用域。使用词法作用域,您可以显式控制使用哪个符号,而不是依靠 newLISP 为您自动跟踪名称相似的符号。

在以下代码中,width 符号在 Right-just 上下文中定义。

(context 'Right-just) 
(set 'width 30)
(define (Right-just:Right-just str)
  (slice (string (dup " " width) str) (* width -1)))

(context MAIN)
(set 'width 0)                         ; this is a red herring
(dolist (w (symbols))
  (println (Right-just w)))
                             !
                            !=
                             $
                            $0
                            $1
                           ...
                    write-line
                     xml-error
                     xml-parse
                 xml-type-tags
                         zero?
                             |
                             ~


第二行 (set 'width ...) 是一个无关紧要的信息:在这里更改它没有任何影响,因为右对齐函数实际使用的符号位于不同的上下文中。

您仍然可以进入 Right-just 上下文来设置宽度

(set 'Right-just:width 15)

关于这两种方法的优缺点有很多讨论。无论您选择哪种方法,请确保您知道代码运行时符号将从哪里获取值。例如

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

这里,y 是函数的第一个参数,并且与任何其他 y 无关。但 x 呢?它是一个全局符号,还是在刚刚调用了 f 的其他函数中定义了值?或者它根本没有值!

最好避免使用这些 **free** 符号,并在可能的情况下使用局部变量(使用 **let** 或 **local** 定义)。也许您可以采用某种约定,例如在全局符号周围加上星号。

对象

[edit | edit source]

关于面向对象编程 (OOP) 的著作比您一生中所能读到的还要多,因此本节只是对该主题的快速概览。newLISP 足够灵活,可以支持多种 OOP 风格,您可以在网上轻松找到这些风格的参考资料,以及关于每种风格的优缺点的讨论。

在本介绍中,我将简要概述其中的一种风格:**FOOP**,即函数式面向对象编程。

FOOP 简介

[edit | edit source]

FOOP 在 newLISP 10.2 版(2010 年初)中发生了变化,因此如果您使用的是旧版本的 newLISP,请更新它。

在 FOOP 中,每个对象都存储为一个列表。类方法和类属性(即适用于该类所有对象的函数和符号)存储在一个上下文中。

对象存储在列表中,因为列表是 newLISP 的基本元素。对象列表中的第一个项目是一个符号,用于标识对象的类;其余项目是描述对象属性的值。

一个类中的所有对象都共享相同的属性,但这些属性可以具有不同的值。类还可以具有在类中所有对象之间共享的属性;这些是类属性。存储在类上下文中的函数提供了用于管理对象和处理它们所持数据的各种方法。

为了说明这些概念,请考虑以下使用时间和日期的代码。它建立在 newLISP 提供的基本日期和时间函数之上(请参见使用日期和时间)。一个时间点表示为一个 **时间对象**。一个对象包含两个值:自 1970 年初开始经过的秒数,以及相对于格林威治的时区偏移量,以分钟计(西经)。因此,表示典型时间对象的列表如下所示

(Time 1219568914 0)

其中 Time 是表示类名的符号,两个数字是此特定时间的数值(这些数字表示 2008 年 8 月 24 日星期日上午 10 点左右的时间,在英格兰的某个地方)。

使用 newLISP 通用 FOOP 构造函数构建此对象所需的代码很简单

(new Class 'Time) ; defines Time context

(setq some-england-date (Time 1219568914 0))

但是,您可能想要定义一个不同的构造函数,例如,您可能想要为该对象赋予一些默认值。为此,您必须重新定义充当 **构造函数** 的默认函数

(define (Time:Time (t (date-value)) (zone 0))
    (list Time t zone))

它是 Time 上下文的默认函数,它构建一个列表,其中第一个位置是类名,以及两个整数,用于表示时间。当没有提供值时,它们默认为当前时间和零偏移量。您现在可以使用构造函数,而无需提供任何参数

(set 'time-now (Time))
;-> your output *will* differ for this one but will be something like (Time 1324034009 0)
(set 'my-birthday (Time (date-value 2008 5 26)))
;-> (Time 1211760000 0)
(set 'christmas-day (Time (date-value 2008 12 25)))
;-> (Time 1230163200 0)

接下来,您可以定义其他函数来检查和管理时间对象。所有这些函数都存储在同一个上下文中。它们可以通过使用 **self** 函数从对象中获取秒数和时区信息来提取它们。所以(self 1)获取秒数,以及(self 2)从作为参数传递的对象中获取时区偏移量。请注意,这些定义不需要您指定对象参数。以下是一些明显的类函数

(define (Time:show)
   (date (self 1) (self 2)))

(define (Time:days-between other)
   "Return difference in days between two times."
   (div (abs (- (self 1) (other 1))) (* 24 60 60)))

(define (Time:get-hours)
   "Return hours."
   (int (date (self 1) (self 2) {%H})))

(define (Time:get-day)
   "Return day of week."
   (date (self 1) (self 2) {%A}))

(define (Time:leap-year?)
   (let ((year (int (date (self 1) (self 2) {%Y}))))
      (and (= 0 (% year 4)) 
         (or (!= 0 (% year 100)) (= 0 (% year 400))))))

这些函数是通过使用 **冒号运算符** 并提供我们希望函数作用于的对象来调用的

; notice 'show' uses 'date' which works with local time, so your output probably will differ
(:show christmas-day)
;-> Thu Dec 25 00:00:00 2008
(:show my-birthday)
;-> Mon May 26 01:00:00 2008

注意我们如何使用冒号运算符作为函数的前缀,没有用空格隔开,这是一种风格问题,你可以使用空格或不使用空格。

(:show christmas-day) ; same as before
;-> Thu Dec 25 00:00:00 2008
(: show christmas-day) ; notice the space between colon and function
;-> Thu Dec 25 00:00:00 2008

这种技术允许 newLISP 提供面向对象程序员喜欢的特性:**多态性**。

多态性

[编辑 | 编辑源代码]

让我们添加另一个类,它处理持续时间 - 两个时间对象之间的间隔 - 以天为单位测量。

(define (Duration:Duration (d 0))
   (list Duration d))

(define (Duration:show)
   (string (self 1) " days "))

有一个新的类构造函数 Duration:Duration 用于创建新的持续时间对象,以及一个简单的 show 函数。它们可以像这样与时间对象一起使用

; define two times
(set 'time-now (Time) 'christmas-day (Time (date-value 2008 12 25)))

; show days between them using the Time:days-between function

(:show (Duration (:days-between time-now christmas-day)))
;-> "122.1331713 days "

将该 :show 函数调用与上一节中的 :show 进行比较

(:show christmas-day)
;-> Thu Dec 25 00:00:00 2008

您可以看到,newLISP 根据 :show 参数的类来选择要评估的 show 函数的哪个版本。因为 christmas-day 是一个 Time 对象,所以 newLISP 评估 Time:show。但是当参数是 Duration 对象时,它评估 Duration:show。这个想法是,你可以在各种类型的对象上使用一个函数:你可能不需要知道你正在处理的对象的类。使用这种多态性,您可以将 show 函数应用于不同类型的对象列表,并且 newLISP 会每次选择合适的那个。

(map (curry :show) 
    (list my-birthday (Duration (:days-between time-now christmas-day))))

;-> ("Mon May 26 01:00:00 2008" "123.1266898 days ")

**注意:**我们必须在这里使用 **curry**,因为 **map** 需要同时使用冒号运算符和 **show** 函数。

修改对象

[编辑 | 编辑源代码]

我们称这种特殊的 OOP 风格为 **FOOP**,因为它被认为是函数式的。在这里,术语“函数式”指的是 newLISP 推崇的编程风格,它强调函数的评估,避免状态和可变数据。正如您所见,许多 newLISP 函数返回列表的副本,而不是修改原始列表。但是,有一些被称为**破坏性**的函数,这些函数被认为不那么纯粹地函数化。但 FOOP 不提供破坏性对象方法,因此可以认为更函数化。

关于 FOOP 需要注意的一点是,对象是不可变的;它们不能被类函数修改。例如,这里有一个用于 Time 类的函数,它将给定数量的天数添加到时间对象中

(define (Time:adjust-days number-of-days)
  (list Time (+ (* 24 60 60 number-of-days) (self 1)) (self 2)))

当它被调用时,它返回对象的修改副本;原始对象保持不变。

(set 'christmas-day (Time (date-value 2008 12 25)))
;-> (Time 1230163200 0)

(:show christmas-day)
;-> "Thu Dec 25 00:00:00 2008"

(:show (:adjust-days christmas-day 3))
;-> "Sun Dec 28 00:00:00 2008"

(:show christmas-day)
;-> "Thu Dec 25 00:00:00 2008"
; notice it's unchanged

christmas-day 对象的原始日期没有改变,尽管 :adjust-days 函数返回了一个修改后的副本,该副本调整了 3 天。

换句话说,要更改对象,请使用熟悉的使用函数返回的值的 newLISP 方法

(set 'christmas-day (:adjust-days christmas-day 3))

(:show christmas-day)
;-> "Sun Dec 28 00:00:00 2008"

christmas-day 现在包含修改后的日期。

您可以在 newLISP 论坛上搜索以下内容,找到对此想法的更完整的阐述timeutilities。另外,请务必阅读参考手册中关于 FOOP 的部分,它有一个关于嵌套对象的很好的例子,即包含其他对象的。对象。

介绍宏

[编辑 | 编辑源代码]

我们已经介绍了 newLISP 的基础知识,但还有很多强大的功能有待发现。一旦您掌握了语言的主要规则,就可以决定要添加哪些更高级的工具。您可能想要探索的一项功能是 newLISP 提供的宏。

宏是一种特殊的函数类型,您可以使用它来更改 newLISP 评估代码的方式。例如,您可以创建新的控制函数类型,例如您自己的 ifcase 版本。

使用宏,您可以开始让 newLISP 按照您想要的方式工作。

严格来说,newLISP 的宏是 **fexprs**,而不是宏。在 newLISP 中,fexprs 被称为宏,部分原因是说“宏”比说“fexprs”容易得多,但主要是因为它们在其他 LISP 方言中与宏的作用类似:它们允许您定义 **特殊形式**,例如您自己的控制函数。

何时进行评估

[编辑 | 编辑源代码]

要理解宏,让我们回到本介绍中的第一个示例之一。考虑这个表达式是如何评估的

(* (+ 1 2) (+ 3 4))
;-> (* 3 7)
;-> 21

* 函数根本没有看到 + 表达式,只看到了它们的结果。newLISP 热心地评估了加法表达式,然后只将结果传递给乘法函数。这通常是您想要的,但有时您不希望立即评估所有表达式。

考虑内置函数 if 的操作

(if (<= x 0) (exit))


如果 x 大于 0,则测试返回 nil,因此(exit)函数不会被评估。现在假设您想定义您自己的 if 函数版本。这应该很容易

(define (my-if test true-action false-action)
   (if test true-action false-action))
> (my-if (> 3 2) (println "yes it is" ) (exit))
yes it is

$ 

但这不起作用。如果比较返回 true,newLISP 打印一条消息,然后退出。即使比较返回 false,newLISP 也会在打印消息之前退出。问题是(exit)在调用 my-if 函数之前被评估,即使您不希望它被评估。对于普通函数,参数中的表达式首先被评估。

宏类似于函数,但它们让您控制何时以及是否评估参数。您使用 define-macro 函数定义宏,就像您使用 define 定义函数一样。这两个定义函数都允许您创建接受参数的自定义函数。重要的区别是,使用普通的 define,参数在函数运行之前被评估。但是,当您调用使用 define-macro 定义的宏函数时,参数以原始未评估的形式传递给定义。您决定何时评估参数。

my-if 函数的宏版本如下所示

(define-macro (my-if test true-action false-action)
   (if (eval test) (eval true-action) (eval false-action)))

(my-if (> 3 2) (println "yes it is" ) (exit))
"yes it is"

test 和 action 参数不会立即被评估,只有当您想要评估它们时才使用 eval 评估。这意味着(exit)在进行测试之前不会被评估。

这种推迟评估的能力使您能够编写自己的控制结构并向语言添加强大的新形式。

构建宏的工具

[编辑 | 编辑源代码]

newLISP 提供了许多用于构建宏的有用工具。除了 define-macroeval 之外,还有 letex,它为您提供了一种在评估表达式之前将局部符号扩展到表达式中的方法,以及 args,它返回传递给您的宏的所有参数。

符号混淆

[编辑 | 编辑源代码]

在编写宏时要注意的一个问题是宏中的符号名称可能会与调用宏的代码中的符号名称混淆。这是一个简单的宏,它向语言添加了一个新的循环构造,它结合了 dolistdo-while。循环变量在条件为 true 时遍历列表

(define-macro (dolist-while)
  (letex (var (args 0 0)   ; loop variable
          lst (args 0 1)   ; list
          cnd (args 0 2)   ; condition
          body (cons 'begin (1 (args)))) ; body
  (let (y)
      (catch (dolist (var lst)
         (if (set 'y cnd) body (throw y)))))))

它被这样调用

(dolist-while (x (sequence 20 0) (> x 10))
  (println {x is } (dec x 1)))
x is 19
x is 18
x is 17
x is 16
x is 15
x is 14
x is 13
x is 12
x is 11
x is 10

它似乎工作得很好。但有一个微妙的问题:您不能使用名为 y 的符号作为循环变量,即使您可以使用 x 或其他任何东西。在循环中放置一个 (println y) 语句以了解原因

(dolist-while (x (sequence 20 0) (> x 10))
    (println {x is } (dec x 1))
    (println {y is }  y))
x is 19
y is true
x is 18
y is true
x is 17
y is true

如果您尝试使用 y,它将不起作用

(dolist-while (y (sequence 20 0) (> y 10))
  (println {y is } (dec y 1)))
y is 
value expected in function dec : y

问题是,y 被宏用于保存条件值,即使它在自己的 let 表达式中。它显示为 true/nil 值,因此它不能被递减。要解决此问题,将宏封装在上下文中,并将宏设置为该上下文中的默认函数

(context 'dolist-while)  
(define-macro (dolist-while:dolist-while)
      (letex (var (args 0 0)
              lst (args 0 1)
              cnd (args 0 2)
              body (cons 'begin (1 (args))))
      (let (y)
         (catch (dolist (var lst)
           (if (set 'y cnd) body (throw y)))))))
(context MAIN)

它可以以相同的方式使用,但没有任何问题

(dolist-while (y (sequence 20 0) (> y 10))
      (println {y is } (dec y 1)))
y is 19
y is 18
y is 17

宏的其他想法

[编辑 | 编辑源代码]

newLISP 用户找到了许多不同的理由使用宏。以下是我在 newLISP 用户论坛上找到的一些宏定义。

这是一个 case 版本,称为 ecase(evaluated-case),它确实评估了测试

(define-macro (ecase _v) 
 (eval (append 
   (list 'case _v) 
   (map (fn (_i) (cons (eval (_i 0)) (rest _i))) 
    (args))))) 

(define (test n) 
   (ecase n 
    ((/ 4 4)     (println "n was 1")) 
    ((- 12 10)   (println "n was 2")))) 

(set 'n 2) 
(test n)
n was 2 

您可以看到,分隔符(/ 4 4)(- 12 10)都被评估了。使用标准版本的 case 它们不会被评估。

这是一个创建函数的宏

(define-macro (create-functions group-name) 
 (letex 
  ((f1 (sym (append (term group-name) "1"))) 
   (f2 (sym (append (term group-name) "2")))) 
 (define (f1 arg) (+ arg 1)) 
 (define (f2 arg) (+ arg 2)))) 

(create-functions foo)    
; this creates two functions starting with 'foo'

(foo1 10)
;-> 11
(foo2 10)
;-> 12

(create-functions bar)    
; and this creates two functions starting with 'bar'

(bar1 12)
;-> 13
(bar2 12)
;-> 14

一个跟踪宏

[编辑 | 编辑源代码]

以下代码更改了 newLISP 的操作,以便使用 define 定义的每个函数在评估时,将它的名称和它的参数的详细信息添加到一个日志文件中。当您运行脚本时,日志文件将包含已评估的函数和参数的记录。

(context 'tracer)
(define-macro (tracer:tracer farg) 
  (set (farg 0) 
    (letex (func   (farg 0) 
            arg    (rest farg) 
            arg-p  (cons 'list (map (fn (x) (if (list? x) (first x) x)) 
                     (rest farg)))
            body   (cons 'begin (args))) 
           (lambda 
               arg 
               (append-file 
                     (string (env "HOME") "/trace.log") 
                     (string 'func { } arg-p "\n")) 
               body))))

(context MAIN)
(constant (global 'newLISP-define) define)
; redefine the built-in define:
(constant (global 'define) tracer)

要使用这个简单的跟踪器运行脚本,在运行之前加载上下文

(load {tracer.lsp})


生成的日志文件包含已调用的每个函数的列表及其接收的参数

 Time:Time (1211760000 0)
 Time:Time (1230163200 0)
 Time:Time (1219686599 0)
 show ((Time 1211760000 0))
 show ((Time 1230163200 0))
 get-hours ((Time 1219686599 0))
 get-day ((Time 1219686599 0))
 days-between ((Time 1219686599 0) (Time 1230163200 0))
 leap-year? ((Time 1211760000 0))
 adjust-days ((Time 1230163200 0) 3)
 show ((Time 1230422400 0))
 Time:Time (1219686599 0)
 days-between ((Time 1219686599 0) (Time 1230422400 0))
 Duration:Duration (124.256956)
 period-to-string ((Duration 124.256956))
 days-between ((Time 1219686599 0) (Time 1230422400 0))
 Duration:Duration (124.256956)
 Time:print ((Time 1211760000 0))
 Time:string ((Time 1211760000 0))
 Duration:print ((Duration 124.256956))
 Duration:string ((Duration 124.256956))

它会使执行速度降低很多。

使用数字

[edit | edit source]

如果您使用数字,您会很高兴知道 newLISP 包含了您期望找到的大多数基本函数,以及更多其他函数。本节旨在帮助您最大程度地利用这些函数,并避免您可能会遇到的某些小陷阱。和往常一样,请参阅官方文档以获取完整详细信息。

整数和浮点数

[edit | edit source]

newLISP 处理两种不同的数字类型:整数和浮点数。整数是精确的,而浮点数 (floats) 精度较低。两者各有优缺点。如果您需要使用非常大的整数,大于 9 223 372 036 854 775 807,请参阅涵盖大型(64 位)整数和大型整数(无限大小)之间差异的部分 - 更大的数字.

算术运算符 +-*/% 始终返回整数值。一个常见的错误是忘记这一点,并在没有意识到它们正在执行整数运算的情况下使用 /*

(/ 10 3)
;-> 3

这可能不是您所期望的!

浮点数仅保留 15 或 16 个最重要的数字(即数字在数字的左侧,具有最高位值)。

浮点数的理念是 足够接近,而不是 这就是确切值

假设您尝试定义一个符号 PI 来存储 pi 的值,精确到 50 位小数

(constant 'PI 3.14159265358979323846264338327950288419716939937510)
;-> 3.141592654

(println PI)
3.141592654

看起来 newLISP 从右侧截取了大约 40 位数字!实际上,大约存储了 15 或 16 位数字,并且丢弃了 35 个不太重要的数字。

newLISP 如何存储此数字?让我们使用 format 函数来查看

(format {%1.50f} PI)
;-> "3.14159265358979311599796346854418516159057617187500"

现在让我们创建一个小的脚本,将这两个数字作为字符串进行比较,这样我们就不必目视 grep 差异

(setq original-pi-str "3.14159265358979323846264338327950288419716939937510")
(setq pi (float original-pi-str))
(setq saved-pi-str (format {%1.50f} pi))

(println pi " -> saved pi (float)")
(println saved-pi-str " -> saved pi formatted")
(println original-pi-str " -> original pi")

(dotimes (i (length original-pi-str) (!= (original-pi-str i) (saved-pi-str i)))
	(print (original-pi-str i)))

(println " -> original and saved versions are equal up to this")
3.141592654 -> saved pi (float)
3.14159265358979311599796346854418516159057617187500 -> saved pi formatted
3.14159265358979323846264338327950288419716939937510 -> original pi
3.141592653589793 -> original and saved versions are equal up to this

请注意,该值精确到 9793,但随后偏离了您最初提供的更精确的字符串。9793 之后的数字是所有计算机存储浮点值的典型方式 - 这不是 newLISP 对您的数据进行创意处理!

您可以在我的机器上使用的最大浮点数似乎约为 10308。但是,仅存储了前 15 位左右的数字,因此大多数都是零,而且您实际上无法对其加 1。

这是浮点数格言的另一个例子:足够接近

顺便说一下,以上评论适用于大多数计算机语言,而不仅仅是 newLISP。浮点数是便利性、速度和准确性之间的折衷方案。

整数和浮点数数学

[edit | edit source]

当您使用浮点数时,请使用浮点算术运算符 addsubmuldivmod,而不是 +-*/%,它们的仅限整数的等效项

(mul PI 2)
;-> 6.283185307

并且,要查看 newLISP 正在存储的值(因为解释器的默认输出分辨率为 9 或 10 位数字)

(format {%1.16f} (mul PI 2))
;-> "6.2831853071795862"

如果您忘记在这里使用 mul,而是使用 *,则小数点后的数字会被丢弃

(format {%1.16f} (* PI 2))
;-> "6.0000000000000000"

这里,pi 被转换为 3,然后乘以 2。

您可以重新定义熟悉的算术运算符,使它们默认使用浮点例程而不是仅限整数的算术

; before
(+ 1.1 1.1)
;-> 2

(constant (global '+) add)

; after
(+ 1.1 1.1)
;-> 2.2

您可以在您的 init.lsp 文件中添加这些定义,以便在您机器上的所有 newLISP 工作中使用它们。您会遇到的主要问题是与他人共享代码或使用导入的库。它们的代码可能会产生意想不到的结果,或者您的代码可能会产生意想不到的结果!

转换:显式和隐式

[edit | edit source]

要将字符串转换为数字,或将一种类型的数字转换为另一种类型的数字,请使用 intfloat 函数。

这些函数的主要用途是将字符串转换为数字 - 整数或浮点数。例如,您可能正在使用正则表达式从较长的字符串中提取数字字符串

(map int (find-all {\d+} {the answer is 42, not 41}))
;-> (42 41)                             ; a list of integers

(map float (find-all {\d+(\.\d+)?} {the value of pi is 3.14, not 1.618}))
;-> (3.14 1.618)                        ; a list of floats

传递给 int 的第二个参数指定了一个默认值,如果转换失败,应使用该默认值

(int "x")
;-> nil
(int "x" 0)
;-> 0

int 是一个巧妙的函数,它还可以将表示以 10 以外的其他进制表示的数字的字符串转换为数字。例如,要将十六进制数的字符串形式转换为十进制数,请确保它以0x为前缀,并且不要使用超出f的字母

(int (string "0x" "1F"))
;-> 31
(int (string "0x" "decaff"))
;-> 14600959

您可以通过仅以0为前缀来转换包含八进制数的字符串

(int "035")
;-> 29

可以通过以0b为前缀来转换二进制数

(int "0b100100100101001001000000000000000000000010100100")
;-> 160881958715556

即使您从未使用过八进制或十六进制,了解这些转换也是有价值的,因为有一天您可能会故意或意外地编写以下代码

(int "08")

这将计算为 0 而不是 8 - 失败的八进制到十进制转换,而不是您可能期望的十进制 8!因此,在对字符串输入使用 int 时,始终指定一个默认值和进制,这是一个好主意

(int "08" 0 10)                         ; default to 0 and assume base 10
;-> 8

如果您使用的是大整数(大于 64 位整数的整数),请使用 bigint 而不是 int。请参阅 更大的数字.

无形转换和舍入

[edit | edit source]

某些函数会自动将浮点数转换为整数。从 newLISP 10.2.0 版本开始,所有由字母组成的运算符都会生成浮点数,而用特殊字符编写的运算符会生成整数。

因此,使用 ++ 将会将您的数字转换为整数并进行舍入,而使用 inc 将会将您的数字转换为浮点数

(setq an-integer 2)
;-> 2
(float? an-integer)
;-> nil
(inc an-integer)
;-> 3
(float? an-integer)
;-> true
(setq a-float (sqrt 2))
;-> 1.414213562
(integer? a-float)
;-> nil
(++ a-float)
;-> 2
(integer? a-float)
;-> true

要使 incdec 对列表起作用,您需要访问特定元素或使用 map 来处理所有元素

(setq numbers '(2 6 9 12))
;-> (2 6 9 12)
(inc (numbers 0))
;-> 3
numbers
;-> (3 6 9 12)
(map inc numbers)
;-> (4 7 10 13)
; but WATCH OUT!
(map (curry inc 3) numbers) ; this one doesn't produce what you expected
;-> (6 12 21 33)
; use this instead:
(map (curry + 3) numbers)
;-> (6 9 12 15)

许多 newLISP 函数会自动将整数参数转换为浮点值。这通常不是问题。但是,如果您将非常大的整数传递给转换为浮点的函数,可能会丢失一些精度

(format {%15.15f} (add 1 922337203685477580))
;-> "922337203685477632.000000000000000"

由于 add 函数将非常大的整数转换为浮点数,因此丢失了一小部分精度(在这种情况下大约为 52)。足够接近吗?如果不是,请仔细考虑您存储和操作数字的方式。

数字测试

[edit | edit source]

有时您需要测试一个数字是整数还是浮点数

(set 'PI 3.141592653589793)
;-> 3.141592654

(integer? PI)
;-> nil

(float? PI)
;-> true

(number? PI)
;-> true

(zero? PI)
;-> nil

使用 integer?float?,您是在测试该数字是以整数还是浮点数形式存储的,而不是测试该数字在数学上是整数还是浮点值。例如,此测试返回 nil,这可能会让您感到惊讶

(integer? (div 30 3))
;-> nil

并不是答案不是 10(它是),而是答案是浮点 10,而不是整数 10,因为 div 函数始终返回浮点值。

绝对符号,从下舍入到上舍入

[edit | edit source]

值得知道的是,floorceil 函数返回包含整数值的浮点数。例如,如果您使用 floor 将 pi 向下舍入到最接近的整数,则结果为 3,但它存储为浮点数,而不是整数

(integer? (floor PI))
;-> nil

(floor PI)
;-> 3

(float? (ceil PI))
;-> true

abssgn 函数也可用于测试和转换数字。abs 始终返回其参数的正版本,而 sgn 返回 1、0 或 -1,具体取决于参数是正、零还是负。

round 函数将数字舍入到最接近的整数,浮点数仍然是浮点数。您还可以提供一个可选的附加值,将数字舍入到特定的小数位数。负数在小数点后舍入,正数在小数点前舍入。

(set 'n 1234.6789)
(for (i -6 6)
 (println (format {%4d %12.5f} i (round n i))))
  -6   1234.67890
  -5   1234.67890
  -4   1234.67890
  -3   1234.67900
  -2   1234.68000
  -1   1234.70000
   0   1235.00000
   1   1230.00000
   2   1200.00000
   3   1000.00000
   4      0.00000
   5      0.00000
   6      0.00000


sgn 具有替代语法,允许您根据第一个参数是负数、零还是正数来评估最多三个不同的表达式。

(for (i -5 5) 
	(println i " is " (sgn i "below 0" "0" "above 0")))
-5 is below 0
-4 is below 0
-3 is below 0
-2 is below 0
-1 is below 0
0 is 0
1 is above 0
2 is above 0
3 is above 0
4 is above 0
5 is above 0

数字格式化

[edit | edit source]

要将数字转换为字符串,请使用 stringformat 函数

(reverse (string PI))
;-> "456395141.3"

stringprintln 都只使用前 10 位左右的数字,即使内部存储了更多数字(最多 15 或 16 位)。

使用 format 以更多控制方式输出数字

(format {%1.15f} PI)
;-> "3.141592653589793"

format 规范字符串使用广泛采用的printf风格格式化。请记住,您还可以使用 format 函数的结果

(string "the value of pi is " (format {%1.15f} PI))
;-> "the value of pi is 3.141592653589793"

format 函数允许您将数字输出为十六进制字符串

(format "%x" 65535)
;-> "ffff"

数字实用程序

[edit | edit source]

创建数字

[edit | edit source]

有一些有用的函数可以轻松地创建数字。

序列和级数

[edit | edit source]

sequence 生成一个算术序列的数字列表。提供起始和结束数字(包含),以及步长值

(sequence 1 10 1.5)
;-> (1 2.5 4 5.5 7 8.5 10)

如果指定了步长值,则所有数字都将存储为浮点数,即使结果为整数,否则它们将为整数

; with step value sequence gives floats
(sequence 1 10 2)
;-> (1 3 5 7 9)
(map float? (sequence 1 10 2))
;-> (true true true true true)
; without step value sequence gives integers
(sequence 1 5)
;-> (1 2 3 4 5)
> (map float? (sequence 1 5))
;-> (nil nil nil nil nil)

series 将其第一个参数乘以其第二个参数若干次。重复次数由第三个参数指定。这将生成几何序列

(series 1 2 20)
;-> (1 2 4 8 16 32 64 128 256 512 1024 2048 4096 8192 16384 32768 65536 131072 262144 524288)

每个数字都存储为浮点数。

series 的第二个参数也可以是函数。该函数应用于第一个数字,然后应用于结果,然后应用于该结果,依此类推。

(series 10 sqrt 20)
;-> (10 3.16227766 1.77827941 1.333521432 1.154781985 1.074607828 1.036632928
1.018151722 1.009035045 1.004507364 1.002251148 1.001124941 1.000562313
1.000281117 1.000140549 1.000070272 1.000035135 1.000017567 1.00000878
1.000004392)

normal 函数返回一个具有指定平均值和标准差的浮点数列表。例如,可以按如下方式生成一个平均值为 10 且标准差为 5 的 6 个数字的列表

(normal 10 5 6)
;-> (6.5234375 14.91210938 6.748046875 3.540039062 4.94140625 7.1484375)

随机数

[edit | edit source]

rand 创建一个随机选择的小于您提供的数字的整数列表

(rand 7 20)                             
; 20 numbers between 0 and 6 (inclusive) or 7 (exclusive)
;-> (0 0 2 6 6 6 2 1 1 1 6 2 0 6 0 5 2 4 4 3)

显然 (rand 1) 生成一个零列表,没有用。(rand 0) 也没有用,但它被分配了初始化随机数生成器的任务。

如果省略第二个数字,它只生成一个范围内的随机数。

random 生成一个浮点数列表,这些浮点数乘以一个比例因子,从第一个参数开始

(random 0 2 10)                         
; 10 numbers starting at 0 and scaled by 2 
;-> (1.565273852e-05 0.2630755763 1.511210644 0.9173002638
; 1.065534475 0.4379183727 0.09408923243 1.357729434
; 1.358592812 1.869385792))

随机性

[edit | edit source]

使用 seed 来控制 rand(整数)、random(浮点数)、randomize(随机列表)和 amb(随机选择的列表元素)的随机性。

如果不用 seed,每次运行时都会出现相同的随机数集。这为您提供了可预测的随机性——这对调试很有用。当您想要模拟现实世界的随机性时,每次运行脚本时使用不同的值对随机数生成器进行播种

不使用 seed

; today
(for (i 10 20)
 (print (rand i) { }))
7 1 5 10 6 2 8 0 17 18 0
; tomorrow
(for (i 10 20)
 (print (rand i) { }))
7 1 5 10 6 2 8 0 17 18 0              ; same as yesterday

使用 seed

; today
(seed (date-value)) 
(for (i 10 20)
 (print (rand i) { }))
2 10 3 10 1 11 8 13 6 4 0
; tomorrow
(seed (date-value)) 
(for (i 10 20)
 (print (rand i) { }))
0 7 10 5 5 8 10 16 3 1 9

通用数字工具

[edit | edit source]

minmax 按预期工作,尽管它们始终返回浮点数。与许多算术运算符一样,您可以提供多个值

(max 1 2 13.2 4 2 1 4 3 2 1 0.2)
;-> 13.2
(min -1 2 17 4 2 1 43 -20 1.1 0.2)
;-> -20
(float? (max 1 2 3))
;-> true

比较函数允许您只提供一个参数。如果将它们与数字一起使用,newLISP 会很有用地假设您正在与 0 进行比较。请记住,您正在使用后缀表示法

(set 'n 3)
(> n)
;-> true, assumes test for greater than 0
(< n)
;-> nil, assumes test for less than 0

(set 'n 0)
(>= n)
;-> true

factor 函数找到整数的因子并将它们返回到列表中。这是测试数字是否为素数的一种有用方法

(factor 5)
;-> (5)

(factor 42)
;-> (2 3 7)

(define (prime? n)
 (and 
   (set 'lst (factor n))
   (= (length lst) 1)))
   
(for (i 0 30) 
 (if (prime? i) (println i)))
2
3
5
7
11
13
17
19
23
29


或者您可以用它来测试数字是否为偶数

(true? (find 2 (factor n)))
;-> true if n is even

gcd 找到两个或多个数字的最大公约数

(gcd 8 12 16)
;-> 4

浮点数实用程序

[edit | edit source]

如果省略,pow 函数的第二个参数默认为 2。

(pow 2)                                 ; default is squared
;-> 4

(pow 2 2 2 2)                           ; (((2 squared) squared) squared)
;-> 256

(pow 2 8)                               ; 2 to the 8
;-> 256

(pow 2 3)
;-> 8

(pow 2 0.5)                             ; square root
;-> 1.414213562

您也可以使用 sqrt 来求平方根。要查找立方根和其他根,请使用 pow

(pow 8 (div 1 3))                       ; 8 to the 1/3
;-> 2

exp 函数计算 ex,其中 e 是数学常数 2.718281828,x 是参数

(exp 1)
;-> 2.71828128

log 函数有两种形式。如果省略底数,则使用自然对数

(log 3)                                 ; natural (base e) logarithms
;-> 1.098612289

或者您可以指定另一个底数,例如 2 或 10

(log 3 2)
;-> 1.584962501

(log 3 10)                              ; logarithm base 10
;-> 0.4771212547

newLISP 中默认可用的其他数学函数是 fft(快速傅立叶变换)和 ifft(逆快速傅立叶变换)。

三角学

[edit | edit source]

newLISP 的所有三角函数,sincostanasinacosatanatan2 以及双曲函数 sinhcoshtanh,都在弧度制下工作。如果您喜欢以度数工作,可以将替代版本定义为函数

(constant 'PI 3.141592653589793)

(define (rad->deg r)
 (mul r (div 180 PI)))

(define (deg->rad d)
 (mul d (div PI 180)))

(define (sind _e)
 (sin (deg->rad (eval _e))))

(define (cosd _e)
 (cos (deg->rad (eval _e))))  

(define (tand _e)
 (tan (deg->rad (eval _e))))  

(define (asind _e)
 (rad->deg (asin (eval _e))))
 
(define (atan2d _e _f)
 (rad->deg (atan2 (deg->rad (eval _e)) (deg->rad (eval _f)))))

等等。

在编写方程式时,一种方法是从末尾开始构建它们。例如,要转换以下方程式

将它分阶段构建,如下所示

1                                  (tand beta)
2                                  (tand beta) (sind epsilon)
3                             (mul (tand beta) (sind epsilon))
4 (sind lamda)                (mul (tand beta) (sind epsilon))
5 (sind lamda) (cosd epsilon) (mul (tand beta) (sind epsilon))
6 (sub (mul (sind lamda) (cosd epsilon))  
                              (mul (tand beta) (sind epsilon)))
7 (atan2d (sub (mul (sind lamda) (cosd epsilon)) (mul (tand beta)(sind epsilon)))
          (cosd lamda))
8 (set 'alpha

等等...

在文本编辑器中将各种表达式对齐通常很有用

(set 'right-ascension
 (atan2d
   (sub
      (mul
        (sind lamda)
        (cosd epsilon))
      (mul
        (tand beta)
        (sind epsilon)))
   (cosd lamda)))

如果您必须将许多数学表达式从中缀转换为后缀表示法,您可能需要研究 infix.lsp 模块(可从 newLISP 网站获得)

(load "/usr/share/newlisp/modules/infix.lsp")
(INFIX:xlate 
 "(sin(lamda) * cos(epsilon)) - (cos(beta) * sin(epsilon))")
;->
(sub (mul (sin lamda) (cos epsilon)) (mul (tan beta) (sin epsilon)))

数组

[edit | edit source]

newLISP 提供多维数组。数组与列表非常相似,您也可以对数组使用大多数对列表进行操作的函数。

大型数组可能比大小相似的列表更快。以下代码使用 time 函数来比较数组和列表的工作速度。

(for (size 200 1000)
 ; create an array
 (set 'arry (array size (randomize (sequence 0 size))))
 ; create a list
 (set 'lst (randomize (sequence 0 size)))
 
 (set 'array-time 
 (time (dotimes (x (/ size 2)) 
  (nth x arry)) 100))
  ; repeat at least 100 times to get non-zero time!
 (set 'list-time
 (time (dotimes (x (/ size 2))
  (nth x lst)) 50))

 (println "with " size " elements: array access: " 
   array-time 
   "; list access: " 
   list-time 
   " " 
   (div list-time array-time )))
with 200 elements: array access: 1; list access: 1 1
with 201 elements: array access: 1; list access: 1 1
with 202 elements: array access: 1; list access: 1 1
with 203 elements: array access: 1; list access: 1 1
...
with 997 elements: array access: 7; list access: 16 2.285714286
with 998 elements: array access: 7; list access: 17 2.428571429
with 999 elements: array access: 7; list access: 17 2.428571429
with 1000 elements: array access: 7; list access: 17 2.428571429


确切时间因机器而异,但通常,对于 200 个元素,数组和列表的速度相当。随着列表和数组大小的增加,nth 访问器函数的执行时间也会增加。当列表和数组分别包含 1000 个元素时,数组的访问速度比列表快 2 到 3 倍。

要创建数组,请使用 array 函数。您可以创建一个新的空数组,创建一个新数组并用默认值填充它,或者创建一个新的数组,该数组是现有列表的精确副本。

(set 'table (array 10))                 ; new empty array
(set 'lst (randomize (sequence 0 20)))  ; new full list
(set 'arry (array (length lst) lst))    ; new array copy of a list

要创建一个作为现有数组副本的新列表,请使用 array-list 函数

(set 'lst2 (array-list arry))           ; makes new list

要区分列表和数组,可以使用 list?array? 测试

(array? arry)     
;-> true
(list? lst)
;-> true

可用于数组的函数

[edit | edit source]

以下通用函数对数组和列表同样有效:firstlastrestmatnthsetfsortappendslice

还有一些针对数组和列表的特殊函数,提供矩阵运算:invertdetmultiplytranspose。见 矩阵.

数组可以是 多维的。例如,要创建一个 2x2 表格,用 0 填充,请使用以下方法

(set 'arry (array 2 2 '(0)))
;-> ((0 0) (0 0))

array 的第三个参数提供了一些初始值,newLISP 将使用这些值来填充数组。newLISP 尽可能有效地使用该值。因此,例如,您可以提供一个足够的初始化表达式

(set 'arry (array 2 2 (sequence 0 10)))
arry
;-> ((0 1) (2 3))                       ; don't need all of them

或者只提供一些提示

(set 'arry (array 2 2 (list 1 2)))
arry
;-> ((1 2) (1 2))

(set 'arry (array 2 2 '(42)))
arry
;-> ((42 42) (42 42))

这个数组初始化功能很酷,所以我有时甚至在创建列表时也会使用它

(set 'maze (array-list (array 10 10 (randomize (sequence 0 10)))))
;-> ((9 4 0 2 10 6 7 1 8 5)
 (3 9 4 0 2 10 6 7 1 8)
 (5 3 9 4 0 2 10 6 7 1)
 (8 5 3 9 4 0 2 10 6 7)
 (1 8 5 3 9 4 0 2 10 6)
 (7 1 8 5 3 9 4 0 2 10)
 (6 7 1 8 5 3 9 4 0 2)
 (10 6 7 1 8 5 3 9 4 0)
 (2 10 6 7 1 8 5 3 9 4)
 (0 2 10 6 7 1 8 5 3 9))

获取和设置值

[edit | edit source]

要从数组中获取值,请使用 nth 函数,该函数期望一个用于数组维度的索引列表,后跟数组的名称

(set 'size 10)
(set 'table (array size size (sequence 0 (pow size))))

(dotimes (row size)
   (dotimes (column size)
     (print (format {%3d} (nth (list row column) table))))
   ; end of row 
   (println))
 0 1 2 3 4 5 6 7 8 9
 10 11 12 13 14 15 16 17 18 19
 20 21 22 23 24 25 26 27 28 29
 30 31 32 33 34 35 36 37 38 39
 40 41 42 43 44 45 46 47 48 49
 50 51 52 53 54 55 56 57 58 59
 60 61 62 63 64 65 66 67 68 69
 70 71 72 73 74 75 76 77 78 79
 80 81 82 83 84 85 86 87 88 89
 90 91 92 93 94 95 96 97 98 99


(nth 也适用于列表和字符串。)

与列表一样,您可以使用隐式寻址来获取值

(set 'size 10)
(set 'table (array size size (sequence 0 (pow size))))

(table 3)
;-> (30 31 32 33 34 35 36 37 38 39)     ; row 3 (0-based!)

(table 3 3)                             ; row 3 column 3 implicitly
;-> 33

要设置值,请使用 setf。以下代码将所有非素数替换为 0。

(set 'size 10)
(set 'table (array size size (sequence 0 (pow size))))
(dotimes (row size)
  (dotimes (column size)
    (if (not (= 1 (length (factor (nth (list row column) table)))))
        (setf (table row column) 0))))

table
;-> ((0 0 2 3 0 5 0 7 0 0) 
 (0 11 0 13 0 0 0 17 0 19) 
 (0 0 0 23 0 0 00 0 29)
 (0 31 0 0 0 0 0 37 0 0)
 (0 41 0 43 0 0 0 47 0 0)
 (0 0 0 53 0 0 0 0 0 59)
 (0 61 0 0 0 0 0 67 0 0)
 (0 71 0 73 0 0 0 0 0 79)
 (0 0 0 83 0 0 0 0 0 89)
 (0 0 0 0 0 0 0 97 0 0))

除了隐式寻址 (table row column) 之外,我还可以写 (setf (nth (list row column) table) 0)。隐式寻址速度稍快,但使用 nth 有时可以使代码更易读。

矩阵

[edit | edit source]

有一些函数将数组或列表(具有正确的结构)视为矩阵。

  • invert 返回矩阵的逆矩阵
  • det 计算行列式
  • multiply 乘以两个矩阵
  • mat 将函数应用于两个矩阵或矩阵和数字
  • transpose 返回矩阵的转置

transpose 在嵌套列表上使用时也很有用(参见 关联列表)。

统计、金融和建模函数

[edit | edit source]

newLISP 拥有大量用于金融和统计分析以及仿真建模的函数。

给定一个数字列表,stats 函数返回值的数量、平均值、与平均值的平均偏差、标准差(总体估计)、方差(总体估计)、分布偏度和分布峰度

(set 'data (sequence 1 10))
;->(1 2 3 4 5 6 7 8 9 10)
(stats data)
(10 5.5 2.5 3.02765035409749 9.16666666666667 0 -1.56163636363636)

以下是内置的其他函数列表

  • beta 计算贝塔函数
  • betai 计算不完全贝塔函数
  • binomial 计算二项式函数
  • corr 计算皮尔逊积矩相关系数
  • crit-chi2 计算给定概率的卡方
  • crit-f 计算给定置信概率的最小临界 F
  • crit-t 计算给定置信概率的最小临界学生 t
  • crit-z 计算给定累积概率的临界正态分布 Z 值
  • erf 计算数字的误差函数
  • gammai 计算不完全伽马函数
  • gammaln 计算对数伽马函数
  • kmeans-query 计算数据向量到质心的欧几里德距离
  • kmeans-train 对矩阵数据执行 K 均值聚类分析
  • normal 生成一个正态分布浮点数列表
  • prob-chi2 计算卡方的累积概率
  • prob-f 查找观察到的统计量的概率
  • prob-t 查找正态分布值的概率
  • prob-z 计算 Z 值的累积概率
  • stats 查找值的中心趋势和分布矩的统计值
  • t-test 使用学生 t 检验比较平均值

贝叶斯分析

[edit | edit source]

托马斯·贝叶斯牧师在 18 世纪初发展起来的统计方法已被证明用途广泛且流行,并已进入当今的编程语言。在 newLISP 中,两个函数 bayes-trainbayes-query 协同工作,提供了一种简便的方法来计算数据集的贝叶斯概率。

以下是如何使用这两个函数来预测一小段文本是由两位作者中的一位撰写的可能性。

首先,选择两位作者的文本,并为每位作者生成数据集。我选择了奥斯卡·王尔德和柯南·道尔。

(set 'doyle-data 
 (parse (lower-case 
   (read-file "/Users/me/Documents/sign-of-four.txt")) {\W} 0))
(set 'wilde-data 
 (parse (lower-case 
   (read-file "/Users/me/Documents/dorian-grey.txt")) {\W} 0))

现在,bayes-train 函数可以扫描这两个数据集并将单词频率存储在一个新的上下文中,我将其称为 Lexicon

(bayes-train doyle-data wilde-data 'Lexicon)

此上下文现在包含出现在列表中的单词列表以及每个单词的频率。例如

Lexicon:_always
;-> (21 110)

即单词 always 在柯南·道尔的文本中出现 21 次,在王尔德的文本中出现 110 次。接下来,可以将 Lexicon 上下文保存到文件中

(save "/Users/me/Documents/lex.lsp" 'Lexicon)

并在需要时使用以下命令重新加载

(load "/Users/me/Documents/lex.lsp")

完成训练后,可以使用 bayes-query 函数在上下文中查找单词列表,并返回两个数字,即这些单词属于第一组或第二组单词的概率。以下列出三个查询。请记住,第一组是道尔,第二组是王尔德

(set 'quote1 
 (bayes-query 
   (parse (lower-case 
    "the latest vegetable alkaloid" ) {\W} 0) 
   'Lexicon))
;-> (0.973352412 0.02664758802)

(set 'quote2 
 (bayes-query 
   (parse 
    (lower-case 
    "observations of threadbare morality to listen to" ) {\W} 0)
    'Lexicon))
;-> (0.5 0.5)

(set 'quote3 
 (bayes-query 
   (parse 
    (lower-case
    "after breakfast he flung himself down on a divan 
     and lit a cigarette" ){\W} 0) 
   'Lexicon))
;-> (0.01961482169 0.9803851783)

这些数字表明 quote1 可能(97% 的确定性)来自柯南·道尔,quote2 不属于道尔或王尔德,quote3 很可能来自奥斯卡·王尔德。

也许这很幸运,但这是一个不错的结果。第一段引文来自道尔的 血字的研究,第三段引文来自王尔德的 亚瑟·萨维尔勋爵的罪行,这两个文本都没有包含在训练过程中,但显然属于作者的词汇。第二段引文来自简·奥斯汀,贝叶斯牧师发展的方法无法将其归类到两位作者中的任何一位。

金融函数

[edit | edit source]

newLISP 提供以下金融函数

  • fv 返回投资的未来值
  • irr 返回投资的内部收益率
  • nper 返回投资的期限数量
  • npv 返回投资的净现值
  • pmt 返回贷款的付款额
  • pv 返回投资的现值

逻辑编程

[edit | edit source]

Prolog 编程语言普及了一种称为统一的逻辑编程类型。newLISP 提供了一个 unify 函数,它可以通过匹配表达式来执行统一。

(unify '(X Y) '((+ 1 2) (- (* 4 5))))
((X (+ 1 2)) (Y (- (* 4 5))))

使用 unify 时,未绑定的变量以大写字母开头,以将其与符号区分开来。

位运算符

[edit | edit source]

位运算符将数字视为由 1 和 0 组成。我们将使用一个实用程序函数,该函数使用 bits 函数以二进制格式打印数字

(define (binary n)
   (if (< n 0)
       ; use string format for negative numbers
      (println (format "%6d %064s" n (bits n)))
      ; else, use decimal format to be able to prefix with zeros
      (println (format "%6d %064d" n (int (bits n))))))

此函数打印出原始数字及其二进制表示形式

(binary 6)
;->       6 0000000000000000000000000000000000000000000000000000000000000110
;-> "     6 0000000000000000000000000000000000000000000000000000000000000110"

移位函数(<<>>)将位向左或向右移动

(binary (<< 6)) ; shift left
;->     12 0000000000000000000000000000000000000000000000000000000000001100
;->"    12 0000000000000000000000000000000000000000000000000000000000001100"
(binary (>> 6)) ; shift right
;->      3 0000000000000000000000000000000000000000000000000000000000000011
;->"     3 0000000000000000000000000000000000000000000000000000000000000011"

以下运算符比较两个或多个数字的位。以 4 和 5 为例

(map binary '(5 4))
;->     5 0000000000000000000000000000000000000000000000000000000000000101
;->     4 0000000000000000000000000000000000000000000000000000000000000100
;-> ("     5 0000000000000000000000000000000000000000000000000000000000000101"
;-> "     4 0000000000000000000000000000000000000000000000000000000000000100")
(binary (^ 4 5)) ; exclusive or: 1 if only 1 of the two bits is 1
;->      1 0000000000000000000000000000000000000000000000000000000000000001
;->"     1 0000000000000000000000000000000000000000000000000000000000000001"
(binary (| 4 5)) ; or: 1 if either or both bits are 1
;->      5 0000000000000000000000000000000000000000000000000000000000000101
;->"     5 0000000000000000000000000000000000000000000000000000000000000101"
(binary (& 4 5)) ; and: 1 only if both are 1
;->      4 0000000000000000000000000000000000000000000000000000000000000100
;->"     4 0000000000000000000000000000000000000000000000000000000000000100"

取反或非函数(~)反转数字中的所有位,交换 1 和 0

(binary (~ 5)) ; not: 1 <-> 0
;->     -6 1111111111111111111111111111111111111111111111111111111111111010
;->"    -6 1111111111111111111111111111111111111111111111111111111111111010"

打印出这些字符串的二进制函数使用 & 函数测试数字的最后一位以查看它是否是 1,并使用 >> 函数将数字向右移动 1 位,准备进行下一次迭代。

OR 运算符(|)的一种用途是在您希望将正则表达式选项与 regex 函数结合使用时。

crc32 为字符串计算 32 位 CRC(循环冗余校验)。

更大的数字

[edit | edit source]

对于大多数应用程序,newLISP 中的整数计算涉及从 9223372036854775807 到 -9223372036854775808 的整数。这些是使用 64 位存储的最大整数。如果您将 1 加到最大的 64 位整数,您将“回绕”(或循环回到)该范围的负数端

(set 'large-int 9223372036854775807)
(+ large-int 1)
;-> -9223372036854775808

但 newLISP 可以处理比这更大的整数,即所谓的“bignums”或“大整数”。

(set 'number-of-atoms-in-the-universe 100000000000000000000000000000000000000000000000000000000000000000000000000000000)
;-> 100000000000000000000000000000000000000000000000000000000000000000000000000000000L
(++ number-of-atoms-in-the-universe)
;-> 100000000000000000000000000000000000000000000000000000000000000000000000000000001L
(length number-of-atoms-in-the-universe)
;-> 81
(float  number-of-atoms-in-the-universe)
;->1e+80

请注意,newLISP 使用尾随的“L”来指示大整数。通常,您可以在不加思考的情况下对大整数进行计算

(* 100000000000000000000000000000000 100000000000000000000000000000)
;-> 10000000000000000000000000000000000000000000000000000000000000L

这里两个操作数都是大整数,因此答案也会自动变为大整数。

但是,在计算中将大整数与其他类型的数字结合使用时,您需要更加注意。规则是计算的第一个参数决定是否使用大整数。比较此循环

(for (i 1 10) (println (+ 9223372036854775800 i)))
9223372036854775801
9223372036854775802
9223372036854775803
9223372036854775804
9223372036854775805
9223372036854775806
9223372036854775807
-9223372036854775808
-9223372036854775807
-9223372036854775806
-9223372036854775806

和这个

(for (i 1 10) (println (+ 9223372036854775800L i))) ; notice the "L"
9223372036854775801L
9223372036854775802L
9223372036854775803L
9223372036854775804L
9223372036854775805L
9223372036854775806L
9223372036854775807L
9223372036854775808L
9223372036854775809L
9223372036854775810L
;-> 9223372036854775810L

在第一个示例中,函数的第一个参数是大型(64 位整数)。因此,将 1 加到最大的 64 位整数会导致回绕 - 计算仍然保留在大整数范围内。

在第二个示例中,追加到第一个参数的 L 强制 newLISP 切换到大整数运算,即使 两个操作数都是 64 位整数。第一个参数的大小决定结果的大小。

如果您提供一个字面量大整数,则无需追加“L”,因为很明显该数字是大整数

(for (i 1 10) (println (+ 92233720368547758123421231455634 i)))
92233720368547758123421231455635L
92233720368547758123421231455636L
92233720368547758123421231455637L
92233720368547758123421231455638L
92233720368547758123421231455639L
92233720368547758123421231455640L
92233720368547758123421231455641L
92233720368547758123421231455642L
92233720368547758123421231455643L
92233720368547758123421231455644L
92233720368547758123421231455644L

您可以控制 newLISP 在大型整数和大整数之间转换的方式还有其他方法。例如,您可以使用 bigint 函数将某物转换为大整数

(set 'bignum (bigint 9223372036854775807))
(* bignum bignum)
;-> 85070591730234615847396907784232501249L

(set 'atoms (bigint 1E+80))
;-> 100000000000000000000000000000000000000000000000000000000000000000000000000000000L
(++ atoms) 
;-> 100000000000000000000000000000000000000000000000000000000000000000000000000000001L

使用日期和时间

[edit | edit source]

日期和时间函数

[edit | edit source]

要使用日期和时间,请使用以下函数

  • date 将秒数转换为日期/时间,或返回当前的日期/时间
  • date-value 返回自 1970 年 1 月 1 日以来的日期和时间的秒数,或返回当前时间的秒数
  • now 以列表形式返回当前日期/时间信息
  • time-of-day 返回自今天开始到现在的毫秒数

date-valuenow 在 UT 中工作,而不是在您的本地时间中。date 可以考虑您的本地时间和 UT 之间的时差。

当前时间和日期

[edit | edit source]

所有四个函数都可用于返回有关当前时间的 信息。date-value 返回 1970 年到当前时间(在 UT 中)之间的秒数

1142798985

now 返回一个包含有关当前日期和时间(在 UT 中)信息的整数列表

(now)
;-> (2006 3 19 20 5 2 125475 78 1 0 0)

这提供以下信息

  • 年、月、日 (2006, 3, 19)
  • 时、分、秒、微秒 (20, 5, 2, 125475)
  • 当年的第几天 (78)
  • 当前一周的日期(1)
  • 本地时区偏移量(以分钟计,相对于格林威治标准时间)(0)
  • 夏令时标志(0)

要提取所需信息,请使用切片或提取元素

(slice (now) 0 3)             ; year month day using explicit slice
(0 3 (now))                   ; year month day using implicit slice
(select (now) '(0 1 2))        ; year month day using selection
(3 3 (now))                   ; hour minute second
(nth 8 (now))                 ; day of the week, starting from Sunday

date 单独使用时会返回本地时区的当前日期和时间(nowdate-value 返回的都是 UCT/UTC 时间,而不是相对于本地时区的日期和时间)

(date)
;-> "Mon Mar 19 20:05:02 2006"

它还可以告诉您自 1970 年(Unix 纪元开始)以来的某个整数秒数的日期,并根据您的本地时区进行调整

(date 0)                                ; a US newLISP user sees this
;-> "Wed Dec 31 16:00:00 1969"
(date 0)                                ; a European newLISP user sees this
;-> "Thu Jan 1 01:00:00 1970"

date-value 可以计算特定日期或日期/时间(以 UT 计)的秒数

(date-value 2006 5 11)                  ; just the date
;-> 1147305600

(date-value 2006 5 11 23 59 59)         ; date and time (in UT)
;-> 1147391999

因为 date-value 可以接受年份、月份、日期、小时、分钟和秒作为输入,所以它可以应用于 now 的输出

(apply date-value (now))         
;-> 1164723787

通过将不同的时间转换为这些日期值,可以进行计算。例如,要从 2005 年 1 月 3 日减去 2003 年 11 月 13 日

(- (date-value 2005 1 3) (date-value 2003 11 13))
;-> 36028800 
; seconds, which is

(/ 36028800 (* 24 60 60))
;-> 417 
; this is a duration in days - don't convert this to a date!

您可以通过将 12 天的秒数添加到该日期来找出 2005 年圣诞节之后的第 12 天

(+ (date-value 2005 12 25) (* 12 24 60 60)) 
; seconds in 12 days 
;-> 1136505600 
; this is an instant in time, so it can be converted!

此秒数值可以通过 date 的较长形式转换为人类可读的日期,它接受自 1970 年以来的秒数值,并将其转换为此基于 UT 值的本地时区表示形式

(date 1136505600)           
;-> "Fri Jan 6 00:00:00 2006"           ; for this European user...

当然,(date (date-value))(date) 相同,但如果要更改日期格式,则必须使用较长形式。date 接受一个额外的格式字符串(以分钟为单位的时区偏移量开头)。如果您熟悉 C 语言风格的 strftime 格式,那么您就知道该怎么做

(date (date-value) 0 "%Y-%m-%d %H:%M:%S")     ; ISO 8601
;-> 2006-06-08 11:55:08

(date 1136505600 0 "%Y-%m-%d %H:%M:%S")
;-> "2006-01-06 00:00:00"

(date (date-value) 0 "%Y%m%d-%H%M%S")         ; in London
;-> "20061207-144445"

(date (date-value) (* -8 60) "%Y%m%d-%H%M%S") ; in Los Angeles
;-> "20061207-064445"                         ; 8 hours offset

读取日期和时间:parse-date

[edit | edit source]

parse-date 函数(不幸的是,它在 Windows 上不可用)可以将日期和时间字符串转换为自 1970 年以来的秒数值。您在字符串之后提供日期时间格式字符串

(parse-date "2006-12-13" "%Y-%m-%d")
;-> 1165968000

(date (parse-date "2007-02-08 20:12" "%Y-%m-%d %H:%M"))
;-> "Thu Feb  8 20:12:00 2007"

计时和计时器

[edit | edit source]

为了计时,您可以使用以下函数

  • time 返回评估表达式的花费时间,以毫秒为单位
  • timer 设置计时器,等待特定秒数,然后评估表达式
  • sleep 停止工作特定毫秒数

time 用于找出表达式评估所需的时间

(time (read-file "/Users/me/Music/iTunes/iTunes Music Library.xml"))
;-> 27                                  ; milliseconds

您也可以提供重复次数,这可能更准确地反映结果

(time (for (x 1 1000) (factor x)) 100)  ; 100 repetitions
;-> 426

如果您不能或不想将表达式括起来,可以使用 time-of-day 来进行更简单的计时

(set 'start-time (time-of-day))
(for (i 1 1000000)
  (set 'temp (sqrt i)))

(string {that took } (div (- (time-of-day) start-time) 1000) { seconds})
;-> "that took 0.238 seconds"

timer 本质上就是一个闹钟。设置它,然后忘记它,直到时间到来。您提供一个指定警报操作的符号,以及要等待的秒数

(define (teas-brewed) 
 (println (date) " Your tea has brewed, sir!"))

(timer teas-brewed (* 3 60))

三分钟后,您将看到以下内容

Sun Mar 19 23:36:33 2006 Your tea has brewed, sir!

如果没有参数,此函数将返回当前已分配为警报操作的符号的名称

(timer)
;-> teas-brewed

如果您正在等待警报发出,并且您迫不及待地想知道迄今为止已经过去了多少时间,请使用分配的符号名称作为参数,但不提供秒数值

(timer teas-brewed)
;-> 89.135747
; waited only a minute and a bit so far

有关这些函数用法的另一个示例,请参见 简单倒计时器

处理文件

[edit | edit source]

处理文件的函数可以分为两大类:与操作系统交互以及读写文件数据。

与文件系统交互

[edit | edit source]

newLISP 中维护着当前工作目录的概念。当您在终端中键入 newLISP 启动 newLISP 时,您的当前工作目录将成为 newLISP 的当前目录。

$ pwd
/Users/me/projects/programming/lisp
$ newlisp
newLISP v.9.3 on OSX UTF-8, execute 'newlisp -h' for more info.

> (env "PWD")
"/Users/me/projects/programming/lisp"
> (exit)

$ pwd
/Users/me/projects/programming/lisp
$


您还可以使用不带参数的 real-path 函数来检查当前工作目录

(real-path)
;-> "/Users/me/projects/programming/lisp"

但是,当您从其他地方运行 newLISP 脚本时,例如从文本编辑器内部运行,当前工作目录和其他设置可能不同。因此,如果不能确定,最好使用 change-dir 来建立当前工作目录

(change-dir "/Users/me/Documents")
;-> true

可以使用 env 访问其他环境变量

(env "HOME")
;-> "/Users/me"
(env "USER")
;-> "cormullion"

同样,从文本编辑器而不是交互式终端会话运行 newLISP 会影响可用的环境变量及其值。

一旦您拥有了正确的工作目录,就可以使用 directory 来列出其内容

(directory)
;-> ("." ".." ".bash_history" ".bash_profile" ".inputrc" ".lpoptions"
; ".sqlite_history" ".ssh" ".subversion" "bin" "Desktop" "Desktop Folder"
; "Documents" "Library" ...

等等。请注意,它为您提供的是相对文件名,而不是绝对路径名。directory 可以列出除当前工作目录之外的其他目录的内容,前提是您提供了路径。在这种扩展形式中,您可以使用正则表达式来过滤内容

(directory "./")                        ; just a pathname
;-> ("." ".." ".bash_history" ".bash_profile" ".inputrc" ".lpoptions"
; ".sqlite_history" ".ssh" ".subversion" "bin" "Desktop" "Desktop Folder"
; "Documents" "Library" ...

(directory "./" {^[^.]})                ; exclude files starting "."
;-> ("bin" "Desktop" "Desktop Folder" "Documents" "Library" ... )

同样,请注意,结果相对于当前工作目录。通常,将要列出的目录的路径存储起来很有用,以防以后需要使用它来构建完整路径名。real-path 返回文件或目录的完整路径名,它位于当前工作目录中

(real-path ".subversion")
;-> "/Users/me/.subversion"

或由另一个相对路径名指定

(real-path "projects/programming/lisp/lex.lsp")
;-> "/Users/me/projects/programming/lisp/lex.lsp"

要查找磁盘上某个项目的包含目录,您只需从完整路径名中删除文件名即可

(set 'f "lex.lsp")
(replace f (real-path f) "")
;-> "/Users/me/projects/programming/lisp/"

顺便说一下,如果文件名也作为目录名出现在路径的前面,则这并不总是有效。一个简单的解决方案是使用 $ 选项执行对 f 在路径名末尾出现的正则表达式搜索

(replace (string f "\$") (real-path f) "" 0)

要递归扫描文件系统的某一部分,请使用一个递归调用自身的函数。此处,只打印完整路径名

(define (search-tree dir)
 (dolist (item (directory dir {^[^.]}))
   (if (directory? (append dir item))
    ; search the directory
    (search-tree (append dir item "/"))
    ; or process the file
    (println (append dir item)))))
    
(search-tree {/usr/share/newlisp/})
/usr/share/newlisp/guiserver/allfonts-demo.lsp
/usr/share/newlisp/guiserver/animation-demo.lsp
...
/usr/share/newlisp/util/newlisp.vim
/usr/share/newlisp/util/syntax.cgi

另请参见 编辑文件夹和层次结构中的文本文件

您会发现一些测试函数很有用

  • file? 此文件或目录是否存在?
  • directory? 此路径名是目录还是文件?

请记住相对路径名和绝对路径名之间的区别

(file? "System")
;-> nil
(file? "/System")
;-> true

文件信息

[edit | edit source]

您可以使用 file-info 获取有关文件的信息。此函数询问操作系统有关文件的信息,并将信息以一系列数字形式返回

  • 0 是大小
  • 1 是模式
  • 2 是设备模式
  • 3 是用户 ID
  • 4 是组 ID
  • 5 是访问时间
  • 6 是修改时间
  • 7 是状态更改时间

例如,要查找文件的大小,请查看 file-info 返回的第一个数字。以下代码列出了目录中的文件,并包含它们的大小。

(set 'dir {/usr/share/newlisp/modules})
(dolist (i (directory dir {^[^.]}))
  (set 'item (string dir "/" i))
  (if (not (directory? item))
      (println (format {%7d %-30s} (nth 0 (file-info item)) item))))
  35935 /usr/share/newlisp/modules/canvas.lsp
   6548 /usr/share/newlisp/modules/cgi.lsp
   5460 /usr/share/newlisp/modules/crypto.lsp
   4577 /usr/share/newlisp/modules/ftp.lsp
  16310 /usr/share/newlisp/modules/gmp.lsp
   4273 /usr/share/newlisp/modules/infix.lsp
  12973 /usr/share/newlisp/modules/mysql.lsp
  16606 /usr/share/newlisp/modules/odbc.lsp
   9865 /usr/share/newlisp/modules/pop3.lsp
  12835 /usr/share/newlisp/modules/postgres.lsp
  31416 /usr/share/newlisp/modules/postscript.lsp
   4337 /usr/share/newlisp/modules/smtp.lsp
  10433 /usr/share/newlisp/modules/smtpx.lsp
  16955 /usr/share/newlisp/modules/sqlite3.lsp
  21807 /usr/share/newlisp/modules/stat.lsp
   7898 /usr/share/newlisp/modules/unix.lsp
   6979 /usr/share/newlisp/modules/xmlrpc-client.lsp
   3366 /usr/share/newlisp/modules/zlib.lsp


请注意,我们将目录的名称存储在 dir 中。directory 函数返回相对文件名,但必须将绝对路径名字符串传递给 file-info,除非字符串引用的是当前工作目录中的文件。

您可以使用隐式寻址来选择所需的项目。因此,您可以这样写(nth 0 (file-info item)),而不是(file-info item 0).

MacOS X:资源分支

[edit | edit source]

如果您在 MacOS X 上尝试了前面的脚本,您可能会注意到一些文件的大小为 0 字节。这可能表明存在从经典(即旧)Macintosh 时代继承而来的双分支系统。使用以下版本访问文件的资源分支。越来越罕见的资源分支的一个好搜索区域是字体文件夹

(set 'dir "/Users/me/Library/Fonts") ; fonts folder
(dolist (i (directory dir "^[^.]"))
 (set 'item (string dir "/" i))
  (and
    (not (directory? item))          ; don't do folders
    (println 
      (format "%9d DF %-30s" (nth 0 (file-info item)) item))
    (file? (format "%s/..namedfork/rsrc" item)) ; there's a resource fork too
    (println (format "%9d RF" 
      (first (file-info (format "%s/..namedfork/rsrc" item)))))))
...
        0 DF /Users/me/Library/Fonts/AvantGarBoo
    26917 RF  
        0 DF /Users/me/Library/Fonts/AvantGarBooObl
    34982 RF  
        0 DF /Users/me/Library/Fonts/AvantGarDem
    27735 RF  
        0 DF /Users/me/Library/Fonts/AvantGarDemObl
    35859 RF  
        0 DF /Users/me/Library/Fonts/ITC Avant Garde Gothic 1
   116262 RF  
...


文件管理

[edit | edit source]

要管理文件,您可以使用以下函数

  • rename-file 重命名文件或目录
  • copy-file 复制文件
  • delete-file 删除文件
  • make-dir 创建新目录
  • remove-dir 删除空目录

例如,要将当前工作目录中的所有文件重新编号,以便文件按修改日期排序,您可以编写如下代码

(set 'dir {/Users/me/temp/})
(dolist (i (directory dir {^[^.]}))
 (set 'item (string dir "/" i))
 (set 'mod-date (date (file-info item 6) 0 "%Y%m%d-%H%M%S"))
 (rename-file item (string dir "/" mod-date i)))
;-> before
image-001.png
image-002.png
image-003.png
image-004.png

;-> after
20061116-120534image-001.png
20061116-155127image-002.png
20061117-210447image-003.png
20061118-143510image-004.png


The(file-info item 6)提取 file-info 返回的结果的修改时间(项目 6)。

在实际使用之前,请务必测试此类脚本!错误的标点符号可能会造成严重破坏。

读取和写入数据

[edit | edit source]

newLISP 拥有丰富的输入和输出函数。

将文本写入文件的一种简单方法是使用 append-file,它将字符串添加到文件的末尾。如果文件不存在,则会创建它。它非常适合创建日志文件和定期写入的文件

(dotimes (x 10) 
 (append-file "/Users/me/Desktop/log.log" 
 (string (date) " logging " x "\n")))

现在在我的桌面上有一个文件,内容如下

Sat Sep 26 09:06:08 2009 logging 0
Sat Sep 26 09:06:08 2009 logging 1
Sat Sep 26 09:06:08 2009 logging 2
Sat Sep 26 09:06:08 2009 logging 3
Sat Sep 26 09:06:08 2009 logging 4
Sat Sep 26 09:06:08 2009 logging 5
Sat Sep 26 09:06:08 2009 logging 6
Sat Sep 26 09:06:08 2009 logging 7
Sat Sep 26 09:06:08 2009 logging 8
Sat Sep 26 09:06:08 2009 logging 9


您无需担心打开和关闭文件。

要将文件的内容一次性加载到一个符号中,请使用 read-file

(set 'contents (read-file "/usr/share/newlisp/init.lsp.example"))
;-> 
";; init.lsp - newLISP initialization file\n;; gets loaded automatically on 
; ...
(load (append $HOME \"/.init.lsp\")) 'error))\n\n;;;; end of file ;;;;\n\n\n          "

符号 contents 将文件的內容作为单个字符串存储。

open 返回一个值,该值充当对文件的引用或“句柄”。您可能以后会使用该文件,因此请将引用存储在一个符号中

(set 'data-file (open "newfile.data" "read"))  ; in current directory

; and later

(close data-file)

使用 read-line 从文件句柄中逐行读取文件。每次使用 read-line 时,下一行都会存储在一个缓冲区中,您可以使用 current-line 函数访问该缓冲区。读取文件的基本方法如下

(set 'file (open ((main-args) 2) "read")) ; the argument to the script
(while (read-line file) 
   (println (current-line)))               ; just output the line

read-line 会丢弃每行末尾的换行符。println 会在您提供的文本末尾添加一个换行符。有关参数处理和 main-args 的更多信息,请参见 STDIO

对于大小中等的文件,逐行读取源文件比将整个文件一次性加载到内存中要慢得多。例如,本书的源文档大约有 6000 行文本,约 350KBytes。使用 read-fileparse 处理文件要快大约 10 倍,方法如下

(set 'source-text (read-file "/Users/me/introduction.txt"))
(dolist (ln (parse source-text "\n" 0))
     (process-line ln))

而不是使用 read-line,方法如下

(set 'source-file (open "/Users/me/introduction.txt" "read"))
(while (read-line source-file)     
     (process-line (current-line)))

device 函数是一种在控制台和文件之间切换输出的便捷方法

(set 'output-file (open "/tmp/file.txt" "write"))
(println "1: this goes to the console")
(device output-file)
(println "2: this goes to the temp file")
(device 0)
(println "3: this goes to the console")
(close output-file)

directing output to more than one device

假设你的脚本接受单个参数,并且你想将输出写入与该参数同名但后缀为 .out 的文件。试试这个

(device (open (string ((main-args) 2) ".out") "write"))

(set 'file-contents (read-file ((main-args) 2)))

现在你可以处理文件的内容,并使用 println 语句输出任何信息。

loadsave 函数用于从文件加载 newLISP 源代码,以及将源代码保存到文件。

read-linewrite-line 函数可用于读取和写入线程以及文件中的行。参见 线程的读写.

标准输入和输出

[编辑 | 编辑源代码]

要从 STDIO(标准输入)读取并写入 STDOUT(标准输出),请使用 read-lineprintln。例如,以下是一个将标准输入转换为小写并将其输出到标准输出的简单过滤器

#!/usr/bin/newlisp

(while (read-line) 
   (println (lower-case (current-line))))

(exit)

它可以在 shell 中像这样运行

$ ./lower-case.lsp 
HI
hi
HI THERE
hi there
...


以下简短脚本是一个有用的 newLISP 格式化程序,由用户 Echeam 提交到用户论坛

#!/usr/bin/env newlisp
(set 'indent "    ")
(set 'level 0)

(while (read-line)
    (if (< level 0) (println "ERROR! Too many close-parenthesis. " level))
    (letn ((ln (trim (current-line))) (fc (first ln)))
        (if (!= fc ")") (println (dup indent level) ln))  ; (indent & print
        (if (and (!= fc ";") (!= fc "#"))     ; don't count if line starts with ; or #
            (set 'level 
              (+ level (apply - (count (explode "()") (explode (current-line)))))))
        (if (= fc ")") (println (dup indent level) ln)))  ; (dedent if close-parenthesis
    )

(if (!= level 0) (println "ERROR! Parenthesis not balanced. " level))
(exit)

你可以像这样从命令行运行它

$ format.lsp < inputfile.lsp


命令行参数

[编辑 | 编辑源代码]

要在命令行中使用 newLISP 程序,可以使用 main-args 函数访问参数。例如,如果你创建了这个文件

#!/usr/bin/newlisp 
(println (main-args))
(exit)

使其可执行,然后在 shell 中运行它,你将看到在运行脚本时传递给脚本的参数列表

$ test.lsp 1 2 3
("/usr/bin/newlisp" "/Users/me/bin/test.lsp" "1" "2" "3")
$


main-args 返回传递给程序的参数列表。前两个参数(你可能不想处理它们)是 newLISP 程序的路径和正在执行的脚本的路径名

(main-args)
;-> ("/usr/bin/newlisp" "/path/script.lsp" "1" "2" "3")

因此你可能想从索引 2 开始处理参数

((main-args) 2)
;-> 1

(main-args 2)       ; slightly simpler
;-> 1

它以字符串形式返回。或者,要从索引 2 开始处理所有参数,请使用切片

(2 (main-args))
;-> ("1" "2" "3")

参数以字符串列表的形式返回。

通常,你希望在脚本中处理所有主要的参数:一个方便的短语是

(dolist (a (2 (main-args)))
 (println a))

一个稍微更易读的等价物是这个,它处理其余的参数

(dolist (a (rest (rest (main-args))))
 (println a))

以下简短脚本会从文本文件中过滤掉不需要的 Unicode 字符,但允许通过少数特殊的字符

(set 'file (open ((main-args) 2) "read")) ; one file

(define (in-range? n low high)
 ; is n between low and high inclusive?
 (and (<= n high) (>= n low)))
 
(while (read-line file) 
 (dostring (c (current-line))
   (if 
      (or
       (in-range? c 32 127)      ; ascii
       (in-range? c 9 10)        ; tab newline
       (in-range? c 12 13)       ; \f \r
       (= c (int "\0xbb"))       ; right double angle
       (= c (int "\0x25ca"))     ; diamond
       (= c (int "\0x2022"))     ; bullet
       (= c (int "\0x201c"))     ; open double quote
       (= c (int "\0x201d"))     ; close double quote
       )
    (print (char c))))           ; nothing to do
   (println) ; because read-line swallows line endings
)

有关参数处理的更多示例,请参见 简单倒计时器.

使用管道、线程和进程

[编辑 | 编辑源代码]

进程、管道、线程和系统函数

[编辑 | 编辑源代码]

以下函数允许你与操作系统交互

  • ! 在操作系统中运行命令
  • abort 停止所有已生成的进程
  • destroy 杀死一个进程
  • exec 运行一个进程并从中读取或写入
  • fork 启动一个 newLISP 子进程线程(Unix)
  • pipe 创建一个用于进程间通信的管道
  • process 启动一个子进程,重新映射标准 I/O 和标准错误
  • semaphore 创建和控制信号量
  • share 与其他进程和线程共享内存
  • spawn 创建一个新的 newLISP 子进程
  • sync 监视和同步已生成的进程
  • wait-pid 等待子进程结束

由于这些命令与你的操作系统交互,因此你应参阅平台特定问题的文档和限制。

! 运行一个系统命令并在控制台中显示结果。exec 函数的功能类似,但它会等待操作系统完成,然后将标准输出作为字符串列表返回,每行一个字符串

(exec "ls -Rat /usr | grep newlisp")
;->
("newlisp" "newlisp-edit" "newlispdoc" "newlisp" "newlisp.1"
"newlispdoc.1" "/usr/share/newlisp:"
"/usr/share/newlisp/guiserver:"
"/usr/share/newlisp/modules:" "/usr/share/newlisp/util:"
"newlisp.vim" "newlisp" "/usr/share/doc/newlisp:"
"newlisp_index.html" "newlisp_manual.html"
"/usr/share/doc/newlisp/guiserver:")

与往常一样,你需要进行引用和双重引用才能将命令传递给 shell。

使用 exec,你的脚本将等待命令完成,然后才会继续执行。

(exec (string "du -s " (env "HOME") "/Desktop"))

你只有在命令完成时才会看到结果。

要与另一个与 newLISP 并行运行的进程进行交互,而不是等待进程完成,请使用 process。参见 进程.

多任务处理

[编辑 | 编辑源代码]

到目前为止,我们评估的所有 newLISP 表达式都是串行运行的,一个接一个,因此一个表达式必须完成评估才能让 newLISP 开始评估下一个表达式。这通常是可以的。但有时你希望启动一个表达式,然后继续执行另一个表达式,而第一个表达式仍在评估中。或者,你可能希望将一个大型任务划分为多个较小的任务,也许可以利用计算机的任何额外处理器。newLISP 使用三个函数来执行这种多任务处理:spawn 用于创建新的进程以并行运行,sync 用于监视和完成它们,abort 用于在它们完成之前停止它们。

在以下示例中,我将使用这个简短的“脉冲”函数

(define (pulsar ch interval)
 (for (i 1 20)
   (print ch)
   (sleep interval))
 (println "I've finished"))

当你正常运行它时,你将看到打印了 20 个字符,每 interval 毫秒打印一个字符。此函数的执行会阻塞所有其他操作,你必须等待所有 20 个字符打印完,或者使用 Control-C 停止执行。

要在当前进程中并行运行此函数,请使用 spawn 函数。提供一个符号来保存表达式的结果,然后是待评估的表达式

> (spawn 'r1 (pulsar "." 3000))
2882
> .


函数返回的数字是进程 ID。现在你可以在终端中继续使用 newLISP,而 pulsar 会继续中断你。这非常令人恼火 - 点会不断出现在你的输入中!

再启动几个

> (spawn 'r2 (pulsar "-" 5000))
2883
> (spawn 'r3 (pulsar "!" 7000))
2884
> (spawn 'r4 (pulsar "@" 9000))
2885


要查看有多少进程处于活动状态(尚未完成),请在没有参数的情况下使用 sync 函数

> (sync)
(2885 2884 2883 2882)

如果要停止所有 pulsar 进程,请使用 abort

 (abort)
true

在 MacOS X 上,尝试使用驻留语音的更有趣版本

(define (pulsar w interval)
   (for (i 1 20)
      (! (string " say " w)) 
      (sleep interval))
   (println "I've finished"))
 
(spawn 'r1 (pulsar "spaghetti" 2000))
(spawn 'r2 (pulsar "pizza" 3000))
(spawn 'r3 (pulsar "parmesan" 5000))

sync 还可以让你关注当前运行的进程。提供一个以毫秒为单位的值;newLISP 将等待该时间,然后检查已生成的进程是否已完成

> (spawn 'r1 (pulsar "." 3000))
2888
> .
> (sync 1000)
nil


如果结果为 nil,则进程尚未完成。如果结果为 true,则所有进程均已完成。现在 - 只有在 sync 函数运行并返回 true 后 - 返回符号 r1 的值将设置为进程返回的值。对于 pulsar,这将是字符串“I've finished”。

I've finished
> r1
nil
> (sync 1000)
true
> r1
"I've finished"
> 


请注意,进程已完成 - 或者更确切地说,它打印了其结束消息 - 但直到 sync 函数执行并返回 true 后,符号 r1 才被设置。这是因为 sync 返回 true,并且返回符号具有值,只有当 所有 已生成的进程都已完成时。

如果你想等待所有进程完成,可以执行循环

(until (sync 1000))

它每秒检查一次,查看进程是否已完成。

作为奖励,许多现代计算机拥有多个处理器,如果每个处理器都能专注于一项任务,你的脚本可能会运行得更快。newLISP 允许操作系统根据其所处硬件环境来调度任务和处理器。

分叉进程

[编辑 | 编辑源代码]

有一些用于操作进程的较低级函数。这些函数不像上一节中描述的已生成进程技术那样方便或易于使用,但它们提供了一些额外的功能,你可能会在某一天发现这些功能很有用。

可以使用 fork 在另一个进程中评估表达式。一旦进程启动,它就不会向父进程返回值,因此你需要考虑如何获取它的结果。以下是在另一个进程中计算素数并将输出保存到文件的方法

(define (isprime? n)
 (if (= 1 (length (factor n)))
   true))
 
(define (find-primes l h)
 (for (x l h)
   (if (isprime? x)
       (push x results -1)))
 results)

(fork (append-file "/Users/me/primes.txt" 
  (string "the result is: " (find-primes 500000 600000))))

这里,由 fork 启动的新的子进程知道如何查找素数,但与已生成的进程不同,它无法向其父进程返回信息以报告它找到了多少个素数。

进程可以共享信息。share 函数设置一个共同的内存区域,就像一个公告板,所有进程都可以读写。在后面的章节中有一个简单的share 示例:请参见一个简单的 IRC 客户端)。

为了控制进程如何访问共享内存,newLISP 提供了semaphore 函数。

读写线程

[edit | edit source]

如果希望分叉的线程互相通信,则需要先进行一些管道工作。使用pipe 设置通信通道,然后安排一个线程监听另一个线程。pipe 返回一个包含进程间通信管道的读写句柄的列表,然后可以使用这些句柄作为read-linewrite-line 函数的通道来读写。

(define (isprime? n)
 (if (= 1 (length (factor n)))
 true))
 
(define (find-primes l h)
 (for (x l h)
   (if (isprime? x)
       (push x results -1)))
 results)

(define (generate-primes channel)
 (dolist (x (find-primes 100 300))
  (write-line channel (string x))))         ; write a prime

(define (report-results channel)
 (do-until (> (int i) 290)
  (println (setq i (read-line channel)))))  ; get next prime

(define (start)
 (map set '(in out) (pipe))                 ; do some plumbing
 (set 'generator (fork (report-results in)))
 (set 'reporter (fork (generate-primes out)))
 (println "they've started"))
 
(start)
they've started
101
103
107
109
...
(wait-pid generator)
(wait-pid reporter)

请注意,"they've started" 字符串出现在任何素数打印之前,即使该println 表达式出现在线程启动之后。

wait-pid 函数等待由fork 启动的线程完成——当然,您不必立即执行此操作。

与其他进程通信

[edit | edit source]

要启动一个与 newLISP 同时运行的新操作系统进程,请使用process。与fork 一样,首先需要设置一些合适的管道工作,以便 newLISP 可以与进程进行通信,在下面的示例中,该进程是 Unix 计算器bcwrite-buffer 函数写入myout 管道,该管道由 bc 通过bcin 读取。bc 的输出通过bcout 指向,并由 newLISP 使用read-line 读取。

connecting newLISP pipes to another process

(map set '(bcin myout) (pipe))                 ; set up the plumbing
(map set '(myin bcout) (pipe))
(process "/usr/bin/bc" bcin bcout)             ; start the bc process 
(set 'sum "123456789012345 * 123456789012345")
(write-buffer myout (string sum "\n"))
(set 'answer (read-line myin))
(println (string sum " = " answer))
123456789012345 * 123456789012345 = 15241578753238669120562399025
(write-buffer myout "quit\n")                  ; don't forget to quit!

使用 XML

[edit | edit source]

将 XML 转换为列表

[edit | edit source]

如今,XML 文件被广泛使用,您可能已经注意到,XML 文件的高度组织的树状结构类似于我们在 newLISP 中遇到的嵌套列表结构。因此,如果您能像处理列表一样轻松地处理 XML 文件,那不是很好吗?

您已经了解了两个主要的 XML 处理函数。(请参见ref 和 ref-all)。xml-parsexml-type-tags 函数是将 XML 转换为 newLISP 列表所需的一切。(xml-error 用于诊断错误)。xml-type-tags 确定xml-parse 如何处理 XML 标记,xml-parse 执行 XML 文件的实际处理,将其转换为列表。

为了说明这些函数的使用,我们将使用 newLISP 论坛的 RSS 新闻提要

(set 'xml (get-url "http://newlispfanclub.alh.net/forum/feed.php"))


并将检索到的 XML 存储在一个文件中,以节省重复访问服务器。

(save {/Users/me/Desktop/newlisp.xml} 'xml)  ; save symbol in file
(load {/Users/me/Desktop/newlisp.xml})       ; load symbol from file


XML 从以下开始

<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-gb">
<link rel="self" type="application/atom+xml" href="http://newlispfanclub.alh.net/forum/feed.php" />

<title>newlispfanclub.alh.net</title>
<subtitle>Friends and Fans of NewLISP</subtitle>
<link href="http://newlispfanclub.alh.net/forum/index.php" />
<updated>2010-01-11T09:51:39+00:00</updated>

<author><name><![CDATA[newlispfanclub.alh.net]]></name></author>
<id>http://newlispfanclub.alh.net/forum/feed.php</id>
<entry>
<author><name><![CDATA[kosh]]></name></author>
<updated>2010-01-10T12:17:53+00:00</updated>
...


如果您使用xml-parse 解析 XML,但没有先使用xml-type-tags,则输出如下

(xml-parse xml)
(("ELEMENT" "feed" (("xmlns" "http://www.w3.org/2005/Atom") 
  ("xml:lang" "en-gb")) 
  (("TEXT" "\n") ("ELEMENT" "link" (("rel" "self") 
   ("type" "application/atom+xml") 
     ("href" "http://newlispfanclub.alh.net/forum/feed.php")) 
    ()) 
   ("TEXT" "\n\n") 
   ("ELEMENT" "title" () (("TEXT" "newlispfanclub.alh.net"))) 
   ("TEXT" "\n") 
   ("ELEMENT" "subtitle" () (("TEXT" "Friends and Fans of NewLISP"))) 
   ("TEXT" "\n") 
   ("ELEMENT" "link" (("href" "http://newlispfanclub.alh.net/forum/index.php")) ()) 
; ...

虽然它已经看起来有点像 LISP,但您可以看到元素已被标记为 "ELEMENT"、"TEXT"。目前,我们可以不用所有这些标签,这就是xml-type-tags 基本上要做的工作。它允许您确定四种类型的 XML 标记的标签:TEXT、CDATA、COMMENTS 和 ELEMENTS。我们将使用四个nil 隐藏它们。我们还将使用一些xml-parse 的选项来进一步整理输出。

(xml-type-tags nil nil nil nil)
(set 'sxml (xml-parse xml 15))        ; options: 15 (see below)
;->
((feed ((xmlns "http://www.w3.org/2005/Atom") (xml:lang "en-gb")) (link ((rel "self") 
    (type "application/atom+xml") 
    (href "http://newlispfanclub.alh.net/forum/feed.php"))) 
  (title "newlispfanclub.alh.net") 
  (subtitle "Friends and Fans of NewLISP") 
  (link ((href "http://newlispfanclub.alh.net/forum/index.php"))) 
  (updated "2010-01-11T09:51:39+00:00") 
  (author (name "newlispfanclub.alh.net")) 
  (id "http://newlispfanclub.alh.net/forum/feed.php") 
  (entry (author (name "kosh")) 
   (updated "2010-01-10T12:17:53+00:00") 
   (id "http://newlispfanclub.alh.net/forum/viewtopic.php?t=3447&amp;p=17471#p17471") 
   (link ((href "http://newlispfanclub.alh.net/forum/viewtopic.php?t=3447&amp;p=17471#p17471"))) 
   (title ((type "html")) "newLISP in the real world \226\128\162 Re: suggesting the newLISP manual as CHM") 
   (category ((term "newLISP in the real world") 
    (scheme "http://newlispfanclub.alh.net/forum/viewforum.php?f=16") 
     (label "newLISP in the real world"))) 
   (content ((type "html") 
   (xml:base "http://newlispfanclub.alh.net/forum/viewtopic.php?t=3447&amp;p=17471#p17471")) 
    "\nhello kukma.<br />\nI tried to make the newLISP.chm, and this is it.<br />\n
  <br />\n<!-- m --><a class=\"postlink\" 
; ...

现在,这是一个有用的 newLISP 列表,尽管它相当复杂,存储在一个名为sxml 的符号中。(这种表示 XML 的方式称为 S-XML)。

如果您想知道 15 在xml-parse 表达式中做了什么,它只是控制了转换多少辅助 XML 信息的一种方法:选项如下

  • 1 - 抑制空白文本标签
  • 2 - 抑制空属性列表
  • 4 - 抑制注释标签
  • 8 - 将字符串标签转换为符号
  • 16 - 添加 SXML(S 表达式 XML)属性标签

将它们加起来以获得选项代码编号——因此 15 (+ 1 2 4 8) 使用了前四个选项:抑制不需要的东西,并将字符串标签转换为符号。因此,新的符号已添加到 newLISP 的符号表中

(author category content entry feed href id label link rel scheme subtitle term 
 title type updated xml:base xml:lang xmlns)

这些对应于 XML 文件中的字符串标签,它们将几乎立即派上用场。

现在怎么办?

[edit | edit source]

到目前为止,故事基本上是这样的

(set 'xml (get-url "http://newlispfanclub.alh.net/forum/feed.php"))
; we stored this in a temporary file while exploring
(xml-type-tags nil nil nil nil)
(set 'sxml (xml-parse xml 15))

它为我们提供了存储在sxml 符号中的新闻提要的列表版本。

由于此列表具有复杂的嵌套结构,最好使用refref-all 而不是find 来查找内容。ref 在列表中找到表达式的第一个出现位置,并返回地址

(ref 'entry sxml)
;-> (0 9 0)

这些数字是在列表中item 符号的第一个出现位置的地址:(0 9 0) 表示从整个列表的第 0 项开始,然后转到该项的第 9 项,然后转到该项的第 0 项。(当然,基于 0 的索引!)。

要查找更高级别或封闭项,请使用chop 删除地址的最后一级

(chop (ref 'entry sxml))
;-> (0 9)

现在,它指向包含第一个项目的级别。就像从地址中删除门牌号一样,只留下街道名称。

现在,您可以将此地址与其他接受索引列表的表达式一起使用。最方便和最简洁的形式可能是隐式地址,它只是源列表的名称,后面跟着一个索引列表

(sxml (chop (ref 'entry sxml)))            ; a (0 9) slice of sxml
(entry (author (name "kosh")) (updated "2010-01-10T12:17:53+00:00") (id "http://newlispfanclub.alh.net/forum/viewtopic.php?t=3447&amp;p=17471#p17471") 
 (link ((href "http://newlispfanclub.alh.net/forum/viewtopic.php?t=3447&amp;p=17471#p17471"))) 
 (title ((type "html")) "newLISP in the real world \226\128\162 Re: suggesting the newLISP manual as CHM") 
 (category ((term "newLISP in the real world") (scheme "http://newlispfanclub.alh.net/forum/viewforum.php?f=16") 
   (label "newLISP in the real world"))) 
 (content ((type "html") (xml:base "http://newlispfanclub.alh.net/forum/viewtopic.php?t=3447&amp;p=17471#p17471")) 
...

它找到了entry 的第一个出现位置,并返回了 SXML 的封闭部分。

您还可以使用另一种技术,将列表的部分内容视为关联列表

(lookup 'title (sxml (chop (ref 'entry sxml))))
;-> 
newLISP in the real world • Re: suggesting the newLISP manual as CHM

在这里,我们像之前一样找到了第一个项目,然后使用lookup 查找了 title 的第一个出现位置。

使用ref-all 在列表中查找符号的所有出现位置。它返回一个地址列表

(ref-all 'title sxml)
;-> 
((0 3 0) 
(0 9 5 0) 
(0 10 5 0) 
(0 11 5 0) 
(0 12 5 0) 
(0 13 5 0) 
(0 14 5 0) 
(0 15 5  0) 
 (0 16 5 0) 
 (0 17 5 0) 
 (0 18 5 0))

通过简单的列表遍历,您可以快速显示文件中所有标题,无论它们位于哪个级别

(dolist (el (ref-all 'title sxml)) 
    (println (rest (rest (sxml (chop el))))))
;->
()
("newLISP in the real world \226\128\162 Re: suggesting the newLISP manual as CHM")
("newLISP newS \226\128\162 Re: newLISP Advocacy")
("newLISP in the real world \226\128\162 Re: newLISP-gs opens only splash picture")
("So, what can you actually DO with newLISP? \226\128\162 Re: Conception of Adaptive Programming Languages")
("Dragonfly \226\128\162 Re: Dragonfly 0.60 Released!")
("So, what can you actually DO with newLISP? \226\128\162 Takuya Mannami, Gauche Newlisp Library")
; ...

如果没有那两个rest,您会看到这个

(title "newlispfanclub.alh.net")
(title ((type "html")) "newLISP in the real world \226\128\162 Re: suggesting the newLISP manual as CHM")
(title ((type "html")) "newLISP newS \226\128\162 Re: newLISP Advocacy")
; ...

如您所见,有很多不同的方法可以访问 SXML 数据中的信息。要生成 XML 文件中新闻的简明摘要,一种方法是遍历所有项目,并提取标题和描述条目。由于描述元素是一堆转义实体,因此我们也会编写一个快速且简单的整理例程

(define (cleanup str)
 (let (replacements 
  '(({&amp;amp;}  {&amp;})
    ({&amp;gt;}   {>})
    ({&amp;lt;}   {<})
    ({&amp;nbsp;} { })
    ({&amp;apos;} {'})
    ({&amp;quot;} {"})
    ({&amp;#40;}  {(})
    ({&amp;#41;}  {)})
    ({&amp;#58;}  {:})
    ("\n"      "")))
 (and
  (!= str "")
  (map 
   (fn (f) (replace (first f) str (last f))) 
   replacements)
  (join (parse str {<.*?>} 4) " "))))

(set 'entries (sxml (chop (chop (ref 'title sxml)))))

(dolist (e (ref-all 'entry entries))
   (set 'entry (entries (chop e)))
   (set 'author (lookup 'author entry))
   (println "Author: " (last author))
   (set 'content (lookup 'content entry))
   (println "Post: " (0 60 (cleanup content)) {...}))
Author: kosh
Post: hello kukma. I tried to make the newLISP.chm, and this is it...
Author: Lutz
Post: ... also, there was a sign-extension error in the newLISP co...
Author: kukma
Post: Thank you Lutz and welcome home again, the principle has bec...
Author: Kazimir Majorinc
Post: Apparently,  Aparecido Valdemir de Freitas  completed his Dr...
Author: cormullion
Post: Upgrade seemed to go well - I think I found most of the file...
Author: Kazimir Majorinc
Post:   http://github.com/mtakuya/gauche-nl-lib   Statistics: Post...
Author: itistoday
Post: As part of my work on Dragonfly, I've updated newLISP's SMTP...
Author: Tim Johnson
Post:   itistoday wrote:     Tim Johnson wrote:  Have you done any...
; ...

更改 SXML

[edit | edit source]

您可以使用类似的技术来修改 XML 格式的数据。例如,假设您将元素周期表保存在 XML 文件中,并且您想更改元素熔点的相关数据,这些数据目前以开氏度为单位存储,改为摄氏度。XML 数据如下所示

<?xml version="1.0"?>
<PERIODIC_TABLE>
  <ATOM>
  ...
  </ATOM>
  <ATOM>
    <NAME>Mercury</NAME>
    <ATOMIC_WEIGHT>200.59</ATOMIC_WEIGHT>
    <ATOMIC_NUMBER>80</ATOMIC_NUMBER>
    <OXIDATION_STATES>2, 1</OXIDATION_STATES>
    <BOILING_POINT UNITS="Kelvin">629.88</BOILING_POINT>
    <MELTING_POINT UNITS="Kelvin">234.31</MELTING_POINT>
    <SYMBOL>Hg</SYMBOL>
...


当表格已加载到符号sxml 中时,使用(set 'sxml (xml-parse xml 15))(其中 xml 包含 XML 源代码),我们希望更改每个具有以下形式的子列表

(MELTING_POINT ((UNITS "Kelvin")) "629.88")

您可以使用set-ref-all 函数在一个表达式中查找和替换元素。首先,这里有一个用于将温度从开氏度转换为摄氏度的函数

(define (convert-K-to-C n)
 (sub n 273.15))

现在,set-ref-all 函数只需调用一次即可查找所有引用并就地修改它们,以便将所有熔点转换为摄氏度。其形式为

(set-ref-all key list replacement function)

其中函数是在给定键的情况下查找列表元素的方法。

(set-ref-all 
  '(MELTING_POINT ((UNITS "Kelvin")) *) 
  sxml 
  (list 
    (first $0) 
    '((UNITS "Celsius")) 
    (string (convert-K-to-C (float (last $0)))))
  match)

这里,match 函数使用通配符结构搜索 SXML 列表(MELTING_POINT ( (UNITS "Kelvin") ) *)以查找每个出现位置。替换表达式使用存储在 $0 中的匹配表达式构建替换子列表。在评估完此表达式后,SXML 将从以下内容

; ...
(ATOM
    (NAME "Mercury")
    (ATOMIC_WEIGHT "200.59")
    (ATOMIC_NUMBER "80")
    (OXIDATION_STATES "2, 1")
    (BOILING_POINT
        ((UNITS "Kelvin")) "629.88")
    (MELTING_POINT
        ((UNITS "Kelvin")) "234.31")
;  ...

更改为以下内容

; ...
(ATOM
    (NAME "Mercury")
    (ATOMIC_WEIGHT "200.59")
    (ATOMIC_NUMBER "80")
    (OXIDATION_STATES "2, 1")
    (BOILING_POINT
        ((UNITS "Kelvin")) "629.88")
    (MELTING_POINT
        ((UNITS "Celsius")) "-38.84")
; ...


XML 并不总是像这样容易操作——存在属性、CDATA 部分等等。

将 SXML 输出到 XML

[edit | edit source]

如果您想反过来将 newLISP 列表转换为 XML,则以下函数提出了一种可能的方法。它递归地遍历列表

(define (expr2xml expr (level 0))
 (cond 
   ((or (atom? expr) (quote? expr))
      (print (dup " " level))
      (println expr))
   ((list? (first expr))
      (expr2xml (first expr) (+ level 1))
      (dolist (s (rest expr)) (expr2xml s (+ level 1))))
   ((symbol? (first expr))
      (print (dup " " level))
      (println "<" (first expr) ">")
      (dolist (s (rest expr)) (expr2xml s (+ level 1)))
      (print (dup " " level))
      (println "</" (first expr) ">"))
   (true
    (print (dup " " level)) 
    (println "<error>" (string expr) "<error>"))))

(expr2xml sxml)
 <rss>
   <version>
   0.92
   </version>
  <channel>
   <docs>
   http://backend.userland.com/rss092
   </docs>
   <title>
   newLISP Fan Club
   </title>
   <link>
   http://www.alh.net/newlisp/phpbb/
   </link>
   <description>
     Friends and Fans of newLISP                                                         
   </description>
   <managingEditor>
   newlispfanclub-at-excite.com
   </managingEditor>
...


这几乎就是我们开始的地方!

一个简单的实用示例

[编辑 | 编辑源代码]

下面的例子最初是在一家小型企业的运输部门设置的。我已经将物品更改为水果。XML 数据文件包含所有销售商品的条目以及每个商品的费用。我们想生成一个报告,列出每个价格卖出了多少,以及总价值。

以下是 XML 数据的摘录

<FRUIT>
  <NAME>orange</NAME>
  <charge>0</charge>
  <COLOR>orange</COLOR>
</FRUIT>
<FRUIT>
  <NAME>banana</NAME>
  <COLOR>yellow</COLOR>
  <charge>12.99</charge>
</FRUIT>
<FRUIT>
  <NAME>banana</NAME>
  <COLOR>yellow</COLOR>
  <charge>0</charge>
</FRUIT>
<FRUIT>
  <NAME>banana</NAME>
  <COLOR>yellow</COLOR>
  <charge>No Charge</charge>
</FRUIT>


这是定义和组织任务的主要函数

(define (work-through-files file-list)
 (dolist (fl file-list)
   (set 'table '())
   (scan-file fl)
   (write-report fl)))

调用了两个函数:scan-file,它扫描 XML 文件并将所需信息存储在一个表中,该表将是某种 newLISP 列表,以及write-report,它扫描该表并输出一个报告。

scan-file 函数接收一个路径名,将文件转换为 SXML,找到所有charge 项目(使用ref-all),并记录每个值的计数。我们允许一些免费物品被各种方式标记为 No Charge 或 no charge 或 nocharge

(define (scan-file f)
  (xml-type-tags nil nil nil nil)
  (set 'sxml (xml-parse (read-file f) 15))
  (set 'r-list (ref-all 'charge sxml)) 
  (dolist (r r-list)
    (set 'charge-text (last (sxml (chop r))))
    (if (= (lower-case (replace " " charge-text "")) "nocharge")
        (set 'charge (lower-case charge-text))
        (set 'charge (float charge-text 0 10)))
    (if (set 'result (lookup charge table 1))
        ; if this price already exists in table, increment it
        (setf (assoc charge table) (list charge (inc result)))
        ; or create an entry for it
        (push (list charge 1) table -1))))

write-report 函数对表进行排序和分析,在运行时保持累积总数

(define (write-report fl)
 (set 'total-items 0 'running-total 0 'total-priced-items 0)
 (println "sorting")
 (sort table (fn (x y) (< (float (x 0)) (float (y 0)))))
 (println "sorted ")
 (println "File: " fl)
 (println " Charge           Quantity     Subtotal")
 (dolist (c table)
  (set 'price (float (first c)))
  (set 'quantity (int (last c)))
  (inc total-items quantity)
  (cond 
   ; do the No Charge items:
   ((= price nil)    (println (format " No charge  %12d" quantity)))
   ; do 0.00 items
   ((= price 0)      (println (format "    0.00    %12d" quantity)))
   ; do priced items:
   (true 
    (begin 
     (set 'subtotal (mul price quantity))
     (inc running-total subtotal)
     (if (> price 0) (inc total-priced-items quantity))
     (println (format "%8.2f        %8d  %12.2f" price quantity subtotal))))))
 ; totals
 (println (format "Total charged   %8d  %12.2f" total-priced-items  running-total))            
 (println (format "Grand Total     %8d  %12.2f"  total-items  running-total)))

该报告需要比scan-file 函数更多一点的调整,尤其是用户想要(出于某种原因)将 0 和无费用项目分开。


 Charge           Quantity     Subtotal
 No charge           138
    0.00             145
    0.11               1          0.11
    0.29               1          0.29
    1.89              72        136.08
    1.99              17         33.83
    2.99              18         53.82
   12.99              55        714.45
   17.99               1         17.99
Total charged        165        956.57
Grand Total          448        956.57

调试器

[编辑 | 编辑源代码]

本节简要介绍内置调试器。

关键函数是trace。要启动和停止调试器,请使用 true 或 nil

(trace true)  ; start debugging
(trace nil)   ; stop debugging

不带参数使用时,如果调试器当前处于活动状态,它将返回 true。

trace-highlight 命令允许您控制当前正在计算的表达式的显示,以及一些提示。我正在使用一个兼容 VT100 的终端,因此我可以使用奇怪的转义序列来设置颜色。在 newLISP 提示符下键入以下内容

(trace-highlight "\027[0;37m" "\027[0;0m")

但是因为我永远记不住这个,所以它在我的 .init.lsp 文件中,该文件在您启动 newLISP 时加载。如果您不能使用这些序列,则可以使用普通字符串代替。

另一个调试函数是debug。这实际上只是切换跟踪、在调试器中运行函数,然后再次切换跟踪的快捷方式。所以,假设我们要运行一个名为 old-file-scanner.lsp 的文件,其中包含以下代码

(define (walk-tree folder)
  (dolist (item (directory folder))
   (set 'item-name (string folder "/" item))
   (if (and (not (starts-with item ".")) (directory? item-name))
       ; folder 
       (walk-tree item-name)
       ; file
       (and
        (not (starts-with item "."))
        (set 'f-name (real-path item-name)) 
        (set 'mtime (file-info f-name 6))
        (if 
          (> (- (date-value) mtime) (* 5 365 24 60 60)) ; non-leap years :)
            (push (list mtime item-name) results))))))
(set 'results '())
(walk-tree {/usr/share})
(map (fn (i) (println (date (first i)) { } (last i))) (sort results))

这会扫描目录和子目录以查找 5 年或更早修改过的文件。(注意,该文件以将在文件加载时立即计算的表达式结尾。)首先,切换跟踪

(trace true)

然后加载并开始调试

(load {old-file-scanner.lsp})

或者,代替这两行,键入以下内容

(debug (load {old-file-scanner.lsp}))

无论哪种方式,您都将看到walk-tree 函数中的第一个表达式突出显示,等待计算。

现在您可以按 s、n 或 c 键(Step、Next 和 Continue)继续执行您的函数:Step 计算每个表达式,并在调用其他函数时进入其他函数;Next 计算所有内容,直到到达同一级别的下一个表达式;Continue 运行而不再次停止。

如果您很聪明,可以在要开始调试的地方之前放置一个(trace true) 表达式。如果可能,newLISP 将在该表达式之前停止,并向您显示它即将计算的函数。在这种情况下,您可以使用简单的 (load...) 表达式开始执行脚本 - 如果要跳过脚本的初步部分,请不要使用debug。我认为 newLISP 通常更喜欢在函数或函数调用的开头进入调试器 - 您可能无法中途进入函数。但是您可能能够组织事物,以便您可以做类似的事情。

以下是如何在循环中途进入调试器。这是一小部分代码

(set 'i 0)
(define (f1)
  (inc i))

(define (f2)
  (dotimes (x 100)
    (f1)
    (if (= i 50) (trace true))))

(f2)


从文件中加载它

> (load {simpleloop.lsp})

-----
(define (f1)
  (inc i))

[-> 5 ] s|tep n|ext c|ont q|uit > i

50

[-> 5 ] s|tep n|ext c|ont q|uit > 

注意f1 函数是如何出现的 - 您没有机会看到f2 中的任何内容。在调试器提示符下,您可以键入任何 newLISP 表达式,并计算任何函数。这里我键入了i 来查看该符号的当前值。newLISP 很乐意让您更改一些符号的值,例如,您可以更改循环变量的值。但不要重新定义任何函数...... 如果您试图从 newLISP 的脚下拉走地毯,您可能会成功地让它摔倒!

调试器显示的源代码不包含注释,因此如果您想在查看代码时给自己留下有用的备注或灵感,请使用文本字符串而不是注释

(define (f1)
  [text]This will appear in the debugger.[/text]
  ; But this won't.
  (inc i))

互联网

[编辑 | 编辑源代码]

HTTP 和网络

[编辑 | 编辑源代码]

大多数网络任务都可以使用 newLISP 的网络函数完成

  • base64-dec 将字符串从 BASE64 格式解码
  • base64-enc 将字符串编码为 BASE64 格式
  • delete-url 删除 URL
  • get-url 从网络读取文件或页面
  • net-accept 接受新的传入连接
  • net-close 关闭套接字连接
  • net-connect 连接到远程主机
  • net-error 返回最后一个错误
  • net-eval 在多个远程 newLISP 服务器上计算表达式
  • net-interface 定义默认网络接口
  • net-listen 监听对本地套接字的连接
  • net-local 连接的本地 IP 和端口号
  • net-lookup IP 号码的名称
  • net-peek 准备读取的字符数
  • net-peer net-connect 的远程 IP 和端口
  • net-ping 向一个或多个地址发送 ping 包(ICMP 回显请求)
  • net-receive 在套接字连接上读取数据
  • net-receive-from 在打开的连接上读取 UDP 数据报
  • net-receive-udp 在打开的连接上读取 UDP 数据报并关闭连接
  • net-select 检查套接字或套接字列表的状态
  • net-send 在套接字连接上发送数据
  • net-send-to 在打开的连接上发送 UDP 数据报
  • net-send-udp 发送 UDP 数据报并关闭连接
  • net-service 将服务名称转换为端口号
  • net-sessions 返回当前所有打开连接的列表
  • post-url 将信息发布到 URL 地址
  • put-url 将页面上传到 URL 地址。
  • xml-error 返回最后一个 XML 解析错误
  • xml-parse 解析 XML 文档
  • xml-type-tags 显示或修改 XML 类型标签

使用这些网络函数,您可以构建各种具有网络功能的应用程序。使用net-eval 等函数,您可以将 newLISP 作为守护程序启动在远程计算机上,然后在本地计算机上使用它通过网络发送 newLISP 代码进行计算。

访问网页

[编辑 | 编辑源代码]

以下是一个使用get-url 的非常简单的例子。给定网页的 URL,获取源代码,然后使用replace 及其列表构建功能生成该页面上所有 JPEG 图像的列表

(set 'the-source (get-url "http://www.apple.com"))
(replace {src="(http\S*?jpg)"} the-source (push $1 images-list -1) 0)
(println images-list)
("http://images.apple.com/home/2006/images/ipodhifititle20060228.jpg"
"http://images.apple.com/home/2006/images/ipodhifitag20060228.jpg"
"http://images.apple.com/home/2006/images/macminiwings20060228.jpg" 
"http://images.apple.com/home/2006/images/macminicallouts20060228.jpg" 
"http://images.apple.com/home/2006/images/ipodhifititle20060228.jpg" 
"http://images.apple.com/home/2006/images/ipodhifitag20060228.jpg")

一个简单的 HTML 表单

[编辑 | 编辑源代码]

最简单的搜索表单可能类似于以下内容。

(load "cgi.lsp")
(println (string "Content-type: text/html\r\n\r\n"
    [text]<!doctype html>
    <html>
     <head>
     <title>Title</title>
     </head>
    <body>
 [/text]))

(set 'search-string (CGI:get "userinput"))

(println (format [text]
        <form name="form" class="dialog" method="GET">
             <fieldset>
                 <input type="text" value="search" name="userinput" >
                 <input type="submit" style="display:none"/>
             </fieldset>
        </form>[/text]))

(unless (nil? search-string)
    (println " I couldn't be bothered to search for \"" search-string "\""))

(println [text]
      </body>
    </html>
 [/text])

一个简单的 IRC 客户端

[编辑 | 编辑源代码]

以下代码实现了一个简单的 IRC(互联网中继聊天)客户端,它展示了如何使用基本网络函数。该脚本使用给定的用户名登录到服务器,并加入 #newlisp 频道。然后脚本分为两个线程:第一个线程在循环中持续显示任何频道活动,而第二个线程等待控制台的输入。两个线程之间唯一的通信是通过共享的connected 标志。

(set 'server (net-connect "irc.freenode.net" 6667)) 
(net-send server "USER newlispnewb 0 * :XXXXXXX\r\n") 
(net-send server "NICK newlispnewb \r\n") 
(net-send server "JOIN #newlisp\r\n") 

(until (find "366" buffer)
  (net-receive server buffer 8192 "\n")
  (print buffer))

(set 'connected (share))
(share connected true)

(fork
    (while (share connected)
      (cond
        ((net-select server "read" 1000) ; read the latest
            (net-receive server buffer 8192 "\n")
            ; ANSI colouring: output in yellow then switch back
            (print "\n\027[0;33m" buffer "\027[0;0m"))
        ((regex {^PING :(.*)\r\n} buffer) ; play ping-pong
            (net-send server (append "PONG :" (string $1 ) "\r\n"))
            (sleep 5000))
        ((net-error) ; error
            (println "\n\027[0;34m" "UH-OH: " (net-error) "\027[0;0m")
            (share connected nil)))
     (sleep 1000)))
 
(while (share connected) 
   (sleep 1000)
   (set 'message (read-line))
   (cond
     ((starts-with message "/")  ; a command?
          (net-send server (append (rest message) "\r\n"))
          (if 
            (net-select server "read" 1000)
            (begin
                (net-receive server buffer 8192 "\n") 
                (print "\n\027[0;35m" buffer "\027[0;0m"))))
     ((starts-with message "quit") ; quit
            (share connected nil))
     (true  ; send input as message
          (net-send server (append "PRIVMSG #newlisp :" message "\r\n")))))

(println "finished; closing server")
(close server)
(exit)

更多示例

[编辑 | 编辑源代码]

本节包含一些 newLISP 运行的简单示例。您可以在网络上以及标准 newLISP 发行版中找到大量优秀的 newLISP 代码。

按您的方式

[编辑 | 编辑源代码]

您可能会发现您不喜欢某些 newLISP 函数的名称。您可以使用constantglobal 将另一个符号分配给该函数

(constant (global 'set!) setf)

您现在可以使用set! 代替setf。这样做不会造成速度损失。

还可以定义您自己对内置函数的替代方案。例如,前面我们定义了一个上下文和一个默认函数,它们与println 执行相同的工作,但记录了输出字符的数量。要计算此代码而不是内置代码,请执行以下操作。

首先,定义函数

(define (Output:Output)
 (if Output:counter
   (inc Output:counter (length (string (args))))
   (set 'Output:counter 0))
 (map print (args))
 (print "\n"))

通过为其定义别名,使 newLISP 的原始版本println 可用

(constant (global 'newLISP-println) println)

println 符号分配给您的 Output 函数

(constant (global 'println) Output)

现在您可以像往常一样使用println

(for (i 1 10)
 (println (inc i)))
2
3
4
5
6
7
8
9
10
11
(map println '(1 2 3 4 5))
1
2
3
4
5

它似乎与原始函数执行相同的工作。但现在您还可以利用您定义的替代println 的额外功能

Output:counter
;-> 36 
; or
println:counter
;-> 36

如果您仔细计算 - 计数器一直在计算提供给 Output 函数的参数的长度。当然,这些包括括号......

使用 SQLite 数据库

[编辑 | 编辑源代码]

有时,使用现有软件比自己编写所有例程更容易,尽管从头开始设计可能很有趣。例如,您可以使用现有的数据库引擎(如 SQLite)来节省大量时间和精力,而不是构建自定义数据结构和数据库访问函数。以下是如何在 newLISP 中使用 SQLite 数据库引擎。

假设您有一组要分析的数据。例如,我找到了一个存储为简单空格分隔表的元素周期表信息的列表

(set 'elements
  [text]1 1.0079 Hydrogen H -259 -253 0.09 0.14 1776 1 13.5984
  2 4.0026 Helium He -272 -269 0 0 1895 18 24.5874
  3 6.941 Lithium Li 180 1347 0.53 0 1817 1 5.3917
  ...
  108 277 Hassium Hs 0 0 0 0 1984 8 0
  109 268 Meitnerium Mt 0 0 0 0 1982 9 0[/text])

(您可以在 GitHub 上的此文件中找到该列表。)

此处的列为原子量、熔点、沸点、密度、地壳中的百分比、发现年份、族和电离能。(我用 0 表示不适用,事实证明这不是一个很好的选择)。

要加载 newLISP 的 SQLite 模块,请使用以下代码行

(load "/usr/share/newlisp/modules/sqlite3.lsp")

这将加载包含 SQLite 接口的 newLISP 源文件。它还会创建一个名为 sql3 的新上下文,其中包含用于处理 SQLite 数据库的函数和符号。

接下来,我们要创建一个新的数据库或打开一个现有的数据库

(if (sql3:open "periodic_table") 
   (println "database opened/created")
   (println "problem: " (sql3:error)))

这将创建一个名为 periodic_table 的新 SQLite 数据库文件并打开它。如果文件已存在,它将被打开并准备使用。您无需再次引用此数据库,因为 newLISP 的 SQLite 库例程在 sql3 上下文中维护一个 当前数据库。如果 open 函数失败,则将打印存储在 sql3:error 中的最新错误。

我刚刚创建了这个数据库,所以下一步是创建一个表。首先,我将定义一个包含列名称字符串和每个列应使用的 SQLite 数据类型的符号。我不必这样做,但它可能必须记录在某个地方,所以,与其写在纸上,不如用一个 newLISP 符号记录下来

(set 'column-def "number INTEGER, atomic_weight FLOAT,
element TEXT, symbol TEXT, mp FLOAT, bp FLOAT, density
FLOAT, earth_crust FLOAT, discovered INTEGER, egroup
INTEGER, ionization FLOAT")

现在我可以创建一个创建表的函数

(define (create-table)
 (if (sql3:sql (string "create table t1 (" column-def ")"))
    (println "created table ... OK")
    (println "problem " (sql3:error))))

这很容易,因为我刚刚创建了 column-def 符号,其格式完全正确!此函数使用 sql3:sql 函数创建一个名为 t1 的表。

我想要一个额外的函数:一个用列表元素中存储的数据填充 SQLite 表的函数。它不是一个漂亮的函数,但它可以完成任务,并且只需要调用一次。

(define (init-table)
 (dolist (e (parse elements "\n" 0))
 (set 'line (parse e))
 (if (sql3:sql 
  (format "insert into t1 values (%d,%f,'%s','%s',%f,%f,%f,%f,%d,%d,%f);" 
    (int (line 0))
    (float (line 1))
    (line 2) 
    (line 3) 
    (float (line 4)) 
    (float (line 5))
    (float (line 6))
    (float (line 7))
    (int (line 8))
    (int (line 9))
    (float (line 10))))
  ; success
  (println "inserted element " e)
  ; failure
  (println (sql3:error) ":" "problem inserting " e))))

此函数调用 parse 两次。第一个 parse 将数据分解为行。第二个 parse 将每行分解成一个字段列表。然后我可以使用 format 将每个字段的值用单引号括起来,记住根据列定义将字符串更改为整数或浮点数(使用 intfloat)。

现在是构建数据库的时候了

(if (not (find "t1" (sql3:tables)))
 (and
   (create-table)
   (init-table)))

- 如果 t1 表不存在于表列表中,则会调用创建和填充它的函数。

查询数据

[edit | edit source]

数据库现在可以使用了。但首先,我将编写一个简单的实用程序函数来简化查询

(define (query sql-text)
 (set 'sqlarray (sql3:sql sql-text))    ; results of query
 (if sqlarray
   (map println sqlarray)
   (println (sql3:error) " query problem ")))

此函数提交提供的文本,并通过对结果列表映射 println 来打印结果,或显示错误消息。

以下是一些示例查询。

查找所有在 1900 年之前发现且构成地球地壳 2% 以上的元素,并按其发现日期对结果进行排序

(query 
 "select element,earth_crust,discovered 
 from t1 
 where discovered < 1900 and earth_crust > 2 
 order by discovered")
("Iron" 5.05 0)
("Magnesium" 2.08 1755)
("Oxygen" 46.71 1774)
("Potassium" 2.58 1807)
("Sodium" 2.75 1807)
("Calcium" 3.65 1808)
("Silicon" 27.69 1824)
("Aluminium" 8.07 1825)

惰性气体(位于第 18 族)是什么时候被发现的?

(query 
 "select symbol, element, discovered 
 from t1 
 where egroup = 18")
("He" "Helium" 1895)
("Ne" "Neon" 1898)
("Ar" "Argon" 1894)
("Kr" "Krypton" 1898)
("Xe" "Xenon" 1898)
("Rn" "Radon" 1900)

所有符号以 A 开头的元素的原子量是多少?

(query 
 "select element,symbol,atomic_weight 
 from t1 
 where symbol like 'A%' 
 order by element")
("Actinium" "Ac" 227)
("Aluminium" "Al" 26.9815)
("Americium" "Am" 243)
("Argon" "Ar" 39.948)
("Arsenic" "As" 74.9216)
("Astatine" "At" 210)
("Gold" "Au" 196.9665)
("Silver" "Ag" 107.8682)

这很简单,亲爱的华生!也许外面的科学家可以提供一些更具科学意义的查询示例?

您也可以在网上找到用于 newLISP 的 MySQL 和 Postgres 模块。

简单的倒计时器

[edit | edit source]

接下来是一个简单的倒计时器,它作为命令行实用程序运行。此示例展示了一些访问脚本中命令行参数的技术。

要开始倒计时,请输入命令(newLISP 脚本的名称),后跟持续时间。持续时间可以是秒;分钟和秒;小时、分钟和秒;甚至可以是天、小时、分钟和秒,用冒号隔开。它也可以是任何 newLISP 表达式。

> countdown 30
Started countdown of 00d 00h 00m 30s at 2006-09-05 15:44:17
Finish time:       2006-09-05 15:44:47
Elapsed: 00d 00h 00m 11s Remaining: 00d 00h 00m 19s

> countdown 1:30
Started countdown of 00d 00h 01m 30s at 2006-09-05 15:44:47
Finish time:       2006-09-05 15:46:17
Elapsed: 00d 00h 00m 02s Remaining: 00d 00h 01m 28s

> countdown 1:00:00
Started countdown of 00d 01h 00m 00s at 2006-09-05 15:45:15
Finish time:       2006-09-05 16:45:15
Elapsed: 00d 00h 00m 02s Remaining: 00d 00h 59m 58s

> countdown 5:04:00:00
Started countdown of 05d 04h 00m 00s at 2006-09-05 15:45:47
Finish time:       2006-09-10 19:45:47
Elapsed: 00d 00h 00m 05s Remaining: 05d 03h 59m 55s

或者,您可以提供一个 newLISP 表达式,而不是数值持续时间。这可能是一个简单的计算,例如 π 分钟中的秒数

> countdown "(mul 60 (mul 2 (acos 0)))"
Started countdown of 00d 00h 03m 08s at 2006-09-05 15:52:49
Finish time:       2006-09-05 15:55:57
Elapsed: 00d 00h 00m 08s Remaining: 00d 00h 03m 00s

或者,更实用的是,一个倒计时到特定时间点的计时器,您可以通过从目标时间减去现在的时间来提供这个时间点

> countdown "(- (date-value 2006 12 25) (date-value))"
Started countdown of 110d 08h 50m 50s at 2006-09-05 16:09:10
Finish time:        2006-12-25 00:00:00
Elapsed: 00d 00h 00m 07s Remaining: 110d 08h 50m 43s

- 在此示例中,我们使用 date-value 指定了圣诞节,它返回自 1970 年以来指定日期和时间的秒数。

表达式的计算由 eval-string 完成,在这里,如果输入文本以 "(" 开头,则将其应用于输入文本 - 通常表明周围存在一个 newLISP 表达式!否则,假定输入是冒号分隔的,并由 parse 分割并转换为秒。

信息从命令行提供的参数中获取,并使用 main-args 提取,main-args 是程序运行时使用的参数列表


(main-args 2)

这将获取参数 2;参数 0 是 newLISP 程序的名称,参数 1 是脚本的名称,所以参数 2 是 countdown 命令后的第一个字符串。

将此文件另存为 countdown,并使其可执行。

#!/usr/bin/newlisp
(if (not (main-args 2))
 (begin 
   (println "usage: countdown duration [message]\n
    specify duration in seconds or d:h:m:s") 
   (exit)))
 
(define (set-duration)
; convert input to seconds
  (if (starts-with duration-input "(") 
      (set 'duration-input (string (eval-string duration-input))))
  (set 'duration 
   (dolist (e (reverse (parse duration-input ":"))) 
    (if (!= e) 
     (inc duration (mul (int e) ('(1 60 3600 86400) $idx)))))))
 
(define (seconds->dhms s)
; convert seconds to day hour min sec display
  (letn 
    ((secs (mod s 60)) 
     (mins (mod (div s 60) 60)) 
     (hours (mod (div s 3600) 24))
     (days (mod (div s 86400) 86400))) 
   (format "%02dd %02dh %02dm %02ds" days hours mins secs)))
 
(define (clear-screen-normans-way)
; clear screen using codes - thanks to norman on newlisp forum :-)
 (println "\027[H\027[2J"))
 
(define (notify announcement)
; MacOS X-only code. Change for other platforms.
  (and 
   (= ostype "OSX")
   ; beep thrice
   (exec (string {osascript -e 'tell application "Finder" to beep 3'}))
 
   ; speak announcment:
   (if (!= announcement nil) 
     (exec (string {osascript -e 'say "} announcement {"'})))
 
   ; notify using Growl:
   (exec (format 
		"/usr/local/bin/growlnotify %s -m \"Finished count down \"" 
      	(date (date-value) 0 "%Y-%m-%d %H:%M:%S")))))

(set 'duration-input (main-args 2) 'duration 0)

(set-duration)

(set 'start-time (date-value))

(set 'target-time (add (date-value) duration))

(set 'banner 
  (string  "Started countdown of " 
    (seconds->dhms duration) 
    " at " 
    (date start-time 0 "%Y-%m-%d %H:%M:%S")
    "\nFinish time:                            " 
    (date target-time 0 "%Y-%m-%d %H:%M:%S")))

(while (<= (date-value) target-time)
  (clear-screen-normans-way)
  (println 
     banner 
     "\n\n" 
    "Elapsed: " 
    (seconds->dhms (- (date-value) start-time )) 
    " Remaining: " 
    (seconds->dhms (abs (- (date-value) target-time))))
  (sleep 1000))

(println 
  "Countdown completed at " 
  (date (date-value) 0 
  "%Y-%m-%d %H:%M:%S") "\n")

; do any notifications here
(notify (main-args 3))

(exit)

在文件夹和层次结构中编辑文本文件

[edit | edit source]

以下是一个简单的函数,它通过查找封闭标签并更改它们之间的文本,更新文件夹中每个文件中的某些文本日期戳。例如,您可能有一对标签,用于保存上次编辑文件的日期,例如 <last-edited> 和 </last-edited>。

(define (replace-string-in-files start-str end-str repl-str folder)
  (set 'path (real-path folder))
  (set 'file-list (directory folder {^[^.]}))
  (dolist (f file-list)
    (println "processing file " f)
    (set 'the-file (string path "/" f))
    (set 'page (read-file the-file))
    (replace
      (append start-str "(.*?)" end-str)  ; pattern 
       page                               ; text 
      (append start-str repl-str end-str) ; replacement 
       0)                                 ; regex option number
    (write-file the-file page)
   ))

可以这样调用它

(replace-string-in-files 
 {<last-edited>} {</last-edited>} 
 (date (date-value) 0 "%Y-%m-%d %H:%M:%S") 
 "/Users/me/Desktop/temp/")

replace-string-in-files 函数接受一个文件夹名称。第一个任务是提取一个合适的文件列表 - 我们使用 directory 和正则表达式{^[^.]}来排除所有以点开头的文件。然后,对于每个文件,内容都被加载到一个符号中,replace 函数替换用指定字符串括起来的文本,最后修改后的文本被保存回磁盘。要调用该函数,请指定起始标签和结束标签,后跟文本和文件夹名称。在此示例中,我们只使用由 datedate-value 提供的简单 ISO 日期戳。

递归版本

[edit | edit source]

假设我们现在想要让它适用于文件夹中的文件夹中的文件夹,即遍历文件的层次结构,更改沿途的每个文件。为此,重新构建 replace-string 函数,使其对传递的路径名起作用。然后编写一个递归函数来查找文件夹中的文件夹,并生成所有必需的路径名,将每个路径名传递给 replace-string 函数。这种重新构建本身可能是一件好事:一方面,它使第一个函数更简单。

(define (replace-string-in-file start-str end-str repl-str pn)
 (println "processing file " pn)
 (set 'page (read-file pn)) 
 (replace
  (append start-str "(.*?)" end-str)   ; pattern 
  page                                 ; text 
  (append start-str repl-str end-str)  ; replacement 
  0)                                   ; regex option number
 (write-file pn page))

接下来是该递归树遍历函数。它查看文件夹/目录中的每个正常条目,并测试它是否为目录(使用 directory?)。如果是,replace-in-tree 函数会调用自身并从新位置开始。如果不是,则将文件的路径名传递给 replace-string-in-file 函数。

(define (replace-in-tree dir s e r)
 (dolist (nde (directory dir {^[^.]}))
   (if (directory? (append dir nde))
       (replace-in-tree (append dir nde "/") s e r)
       (replace-string-in-file (append dir nde) s e r))))

要更改一棵树中的所有文件,请按如下方式调用该函数

(replace-in-tree 
  {/Users/me/Desktop/temp/} 
  {<last-edited>}  
  {</last-edited>} 
  (date (date-value) 0 "%Y-%m-%d %H:%M:%S"))

在测试区域中首先测试这些内容非常重要;代码中一个小错误可能会对您的数据产生重大影响。注意 newLISPer!

与其他应用程序通信(MacOS X 示例)

[edit | edit source]

newLISP 为将应用程序程序中具有自身脚本语言的功能粘合在一起提供了良好的环境。它速度快,体积小,不会妨碍脚本解决方案的其他组件,而且非常适合在信息通过工作流程时处理信息。

以下是如何使用 newLISP 脚本将非 newLISP 脚本命令发送到应用程序的示例。任务是在 Adobe Illustrator 中构建一个圆形,给定圆周上的三个点。

解决方案分为三个部分。首先,我们从应用程序获取所选内容的坐标。接下来,我们计算通过这些点的圆的半径和中心点。最后,我们可以绘制圆形。第一部分和最后部分使用 AppleScript,它使用 osascript 命令运行,因为 Adobe Illustrator 不理解任何其他脚本语言(在 Windows 上,您使用的是 Visual Basic,而不是 AppleScript)。

Using a newLISP script in Adobe Illustrator

计算和一般接口使用 newLISP 完成。这通常比使用原生 AppleScript 更好,因为 newLISP 提供了许多在默认 AppleScript 系统中找不到的强大的字符串和数学函数。例如,如果我想使用三角函数,我将不得不找到并安装一个额外的组件 - AppleScript 根本不提供任何三角函数。

newLISP 脚本可以放在菜单栏上的脚本菜单中;将其放入库 > 脚本 > 应用程序 > Adobe Illustrator 文件夹,该文件夹接受文本文件和 AppleScript)。然后,您可以在 Illustrator 中工作时选择它。要使用它,只需选择一个至少有三个点的路径,然后运行该脚本。前三个点定义新圆的位置。

#!/usr/bin/newlisp

; geometry routines from
; http://cgafaq.info/wiki/Circle_Through_Three_Points
; given three points, draw a circle through them

(set 'pointslist 
  (exec 
    (format [text]osascript  -e 'tell application "Adobe Illustrator 10"
  tell front document
    set s to selection
    repeat with p in s
    set firstItem to p
    set pathinfo to entire path of firstItem
    set pointslist to ""
    repeat with p1 in pathinfo
    set a to anchor of p1
    set pointslist to pointslist & " " & item 1 of a
    set pointslist to pointslist & " " & item 2 of a
    end repeat
    end repeat
  end tell
end tell
pointslist' 
[/text])))

; cleanup
(set 'points 
  (filter float? 
    (map float (parse (first pointslist) { } 0))))

(set  'ax (points 0) 
      'ay (points 1) 
      'bx (points 2) 
      'by (points 3) 
      'cx (points 4) 
      'cy (points 5))

(set  'A (sub bx ax)
      'B (sub by ay)  
      'C (sub cx ax)  
      'D (sub cy ay)
      'E (add 
          (mul A (add ax bx)) 
          (mul B (add ay by)))
      'F (add 
          (mul C (add ax cx)) 
          (mul D (add ay cy)))
      'G (mul 2 
            (sub 
              (mul A (sub cy by)) 
              (mul B (sub cx bx)))))

(if (= G 0) ; collinear, forget it
  (exit))

(set  'centre-x (div (sub (mul D E) (mul B F)) G)
      'centre-y (div (sub (mul A F) (mul C E)) G)
      'r 
        (sqrt 
          (add 
            (pow (sub ax centre-x)) 
            (pow (sub ay centre-y)))))

; we have coords of centre and the radius 
; in centre-x, centre-y, and r
; Illustrator bounds are left-x, top-y, right-x, bottom-y 
; ie centre-x - r, centre-y + r, centre-x + r, centre-y -r 

(set 'bounds-string 
  (string "{" (sub centre-x r) ", " 
   (add centre-y r) ", " 
   (add centre-x r) ", " 
   (sub centre-y r) "}"))

(set 'draw-circle 
  (exec (format [text]osascript  -e 'tell application "Adobe Illustrator 10"
  tell front document
    set e to make new ellipse at beginning with properties {bounds:%s}
  end tell
end tell
' 
[/text] bounds-string)))
(exit)

此脚本几乎没有错误处理!在第一个阶段中应该添加更多错误处理(因为所选内容可能不适合后续处理)。

图形界面

[编辑 | 编辑源代码]

使用 newLISP,您可以轻松地为您的应用程序构建图形界面。这份入门文档已经足够长了,所以我不会详细描述 newLISP-GS 的功能集。但这里有一个简短的示例,让您体验它的工作原理。

newLISP-GS 的基本组件是 **容器**、**小部件**、**事件** 和 **标签**。您的应用程序由容器组成,容器包含小部件和其他容器。通过为所有内容赋予一个标签(一个符号),您可以轻松地控制它们。当应用程序的用户点击、按下和滑动东西时,事件会发送回 newLISP-GS,您可以编写代码来处理每个事件。

一个简单的应用程序

[编辑 | 编辑源代码]

为了介绍基本概念,本章将展示如何轻松构建一个简单的应用程序,一个颜色混合器。

a simple colour mixer

您可以移动滑块来改变窗口中央区域的颜色。颜色组件(红色、绿色和蓝色的数字介于 0 和 1 之间)在窗口底部的文本字符串中显示。

在 newLISP-GS 中,容器的内容根据您选择的布局管理器类型进行排列 - 目前您可以使用 **流动**、**网格** 或 **边框** 布局。

下图显示了应用程序界面的结构。主容器(在本例中是一个名为 'Mixer' 的框架)包含其他容器和小部件。窗口的顶部区域(包含滑块)由一个名为 'SliderPanel' 的面板组成,该面板又包含三个面板,每个面板对应一个滑块,分别称为 'RedPanel'、'GreenPanel' 和 'BluePanel'。在下方,中间区域包含一个名为 'Swatch' 的画布,用来显示颜色,在底部区域有一个名为 'Value' 的文本标签,以文本形式显示 RGB 值。每个区域都使用不同的布局管理器进行布局。

The structure of the mixer window

只需要一个处理程序。它分配给滑块,并在每次移动滑块时触发。

第一步是加载 newLISP-GS 模块。

#!/usr/bin/env newlisp

(load (append (env "NEWLISPDIR") "/guiserver.lsp"))

它提供了所有必需的对象和函数,在一个名为 gs 的上下文中。

图形系统使用单个函数初始化。

(gs:init)

可以逐个添加界面的各个部分。首先,我定义主窗口,并选择边框布局。边框布局允许您将每个组件放置到五个区域中的一个,分别标记为 "north"、"west"、"center"、"east" 和 "south"。

(gs:frame 'Mixer 200 200 400 300 "Mixer")
(gs:set-resizable 'Mixer nil)
(gs:set-border-layout 'Mixer)

现在可以添加用于容纳滑块的顶部面板。我希望滑块垂直堆叠,因此我将使用 3 行 1 列的网格布局。

(gs:panel 'SliderPanel)
(gs:set-grid-layout 'SliderPanel 3 1)

定义三个颜色面板中的每一个,以及它们相应的标签和滑块。滑块被分配了 slider-handler 函数。我可以在完成界面定义后编写它。

(gs:panel  'RedPanel)
(gs:panel  'GreenPanel)
(gs:panel  'BluePanel)

(gs:label  'Red   "Red"   "left" 50 10 )
(gs:label  'Green "Green" "left" 50 10 )
(gs:label  'Blue  "Blue"  "left" 50 10 )

(gs:slider 'RedSlider   'slider-handler "horizontal" 0 100 0)
(gs:slider 'GreenSlider 'slider-handler "horizontal" 0 100 0)
(gs:slider 'BlueSlider  'slider-handler "horizontal" 0 100 0)

(gs:label  'RedSliderStatus   "0"  "right" 50 10)
(gs:label  'GreenSliderStatus "0"  "right" 50 10)
(gs:label  'BlueSliderStatus  "0"  "right" 50 10)

gs:add-to 函数使用已分配给它的布局将组件添加到容器中。如果没有分配布局,则使用流动布局(一种简单的顺序布局)。首先指定目标容器,然后给出要添加的组件。因此,标记为 'Red'、'RedSlider' 和 'RedSliderStatus' 的对象将逐个添加到 'RedPanel' 容器中。完成三个面板后,可以将它们添加到 SliderPanel 中。

(gs:add-to 'RedPanel 'Red 'RedSlider 'RedSliderStatus) 
(gs:add-to 'GreenPanel 'Green 'GreenSlider 'GreenSliderStatus)
(gs:add-to 'BluePanel 'Blue 'BlueSlider 'BlueSliderStatus)

(gs:add-to 'SliderPanel 'RedPanel 'GreenPanel 'BluePanel)

您可以在画布上绘制各种图形,尽管在这个应用程序中,我只打算使用画布作为色板,一个单一颜色的区域。

(gs:canvas 'Swatch)

(gs:label 'Value "")
(gs:set-font 'Value "Sans Serif" 16)

现在,三个主要组件 - 滑块面板、颜色色板和值的标签 - 可以添加到主框架中。因为我将边框布局分配给了框架,所以可以使用方向来放置每个组件。

(gs:add-to 'Mixer 'SliderPanel "north" 'Swatch "center" 'Value "south")

我们没有使用 east 和 west 区域。

默认情况下,框架和窗口在创建时是不可见的,所以现在是让我们的主框架可见的好时机。

(gs:set-visible 'Mixer true)

这完成了应用程序的结构。现在需要进行一些初始化,以便在应用程序启动时显示一些有意义的内容。

(set 'red 0 'green 0 'blue 0)
(gs:set-color 'Swatch (list red green blue))
(gs:set-text  'Value (string (list red green blue)))

最后,我不能忘记为滑块编写处理程序代码。处理程序传递了生成事件的对象的 ID 以及滑块的值。代码将值(小于 100 的整数)转换为 0 到 1 之间的数字。然后可以使用 **set-color** 函数将画布的颜色设置为显示新的混合颜色。

(define (slider-handler id value)
  (cond 
     ((= id "MAIN:RedSlider") 
        (set 'red (div value 100))
        (gs:set-text 'RedSliderStatus (string red)))
     ((= id "MAIN:GreenSlider") 
       (set 'green (div value 100))
        (gs:set-text 'GreenSliderStatus (string green)))
     ((= id "MAIN:BlueSlider") 
       (set 'blue (div value 100))
       (gs:set-text 'BlueSliderStatus (string blue)))
     )
  (gs:set-color 'Swatch (list red green blue))
  (gs:set-text  'Value (string (list red green blue))))

只有一行代码是必要的,我们就完成了。gs:listen 函数监听事件并将它们分派给处理程序。它会持续运行,因此您不需要执行任何其他操作。

(gs:listen)

这个小小的应用程序只触及了 newLISP-GS 的皮毛,所以请查看文档并尝试一下吧!

华夏公益教科书