跳转到内容

newLISP 简介/控制流程

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

控制流程

[编辑 | 编辑源代码]

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

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

(keyword expression1 expression2 expression3 ...)

测试:if...

[编辑 | 编辑源代码]

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

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

第二个表达式,对launch-missile函数的调用,只有当符号button-pressed?计算为true时才会计算。1 为真。0 为真 - 毕竟它是一个数字。-1 为真。newLISP 已经知道的大部分东西都是真的。newLISP 知道有两个重要的东西是假而不是真:nil和空列表 ()。任何 newLISP 不知道值的都是假的。

(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

你可以使用任何计算为真或假的东西作为测试。

(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 认为它是假的,并且测试返回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 ...) - 但只有一个会被计算。

如果你没有集中精力,可能会被其他语言中常见的指示词(例如thenelse)的缺失所迷惑!但你可以轻松地添加注释。

你可以使用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的值。

有时你想多次重复一系列操作,进入循环。有多种可能性。你可能想对以下内容执行操作

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

newLISP 对所有这些问题都有解决方案,而且不止这些。

遍历列表

[编辑 | 编辑源代码]

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

在下面的示例中,我在定义一个局部循环变量i之前,还设置了另一个名为counter的符号,它将保存由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函数和许多其他控制词允许你在一个列表中写一系列表达式:这里,printlninc(增量)函数都被调用了,用于列表中的每个元素。

有一个有用的快捷方式可以访问系统维护的循环计数器。现在我使用了一个计数器符号,每次循环时都会增加它,以便跟踪我们已经遍历了列表的多少次。但是,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函数相同的输出。我定义了一个临时打印和增加函数,它包含两个表达式,并将此函数应用于由sequence生成的列表中的每个元素。

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

经验丰富的 LISP 程序员可能更熟悉lambdafnlambda的同义词:使用你喜欢的任何一个。

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

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

例如,

(1 2 3 4 5 6)

请参阅flat

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

遍历字符串

[编辑 | 编辑源代码]

(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


你可以使用等效于dolistdostring来遍历字符串中的每个字符。

这些数字是 ASCII/Unicode 代码。

特定次数

[编辑 | 编辑源代码]

(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


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

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

记住这一点的一个方法是想想生日。当你完成第一年时,你庆祝你的第一个生日,在这段时间里你 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

有一个逃生路线可用

[编辑 | 编辑源代码]

fordotimesdolist 喜欢循环,反复执行一组操作。通常重复会一直持续,直到达到极限——最后的数字或列表中的最后一个项目——为止。但是,你可以指定一个紧急逃生路线,形式为在下一个循环开始之前要执行的测试。如果该测试返回真值,则不会启动下一个循环,表达式将在通常结束之前完成。这为你提供了一种在官方最终迭代之前停止的方法。

例如,假设你想将列表中的每个数字减半,但(出于某种原因)你想在其中一个数字为奇数时停止。比较此 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) 返回真值,则第二个版本停止循环。

请注意这里使用了仅整数的除法。在示例中,我使用了 / 而不是浮点除法运算符 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

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

直到发生某事,或者某事为真

[编辑 | 编辑源代码]

你可能有一个测试,用于测试一个情况,当发生有趣的事情时,该测试返回 nil 或 (),否则返回一个你并不感兴趣的真值。若要重复执行一系列操作,直到测试失败,请使用 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- 变体,以便在评估测试之前执行操作块。

块:表达式组

[编辑 | 编辑源代码]

许多 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

[编辑 | 编辑源代码]

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

以下是一个 and 的示例,它测试 disk-item 是否包含一个有用的目录

(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。这始终会返回真值或 nil,具体取决于 c 的值。

在某些情况下,and 可以产生比 if 更简洁的代码。你可以将上一页的示例改写为

(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 更容易满足。表达式序列将逐个进行评估,直到一个表达式返回一个真值。然后忽略其余的表达式。你可以使用它来遍历一系列重要条件,任何一个失败都足以放弃整个操作。或者,相反地,使用 or 来遍历一个列表,其中任何一个成功都是继续进行的理由。无论如何,请记住,一旦 newLISP 获得一个非 nil 结果,or 函数就会完成。

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

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

[编辑 | 编辑源代码]

你可能不会找到 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

[编辑 | 编辑源代码]

若要测试一系列替代值,可以使用 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 会执行第一个测试,如果测试为真,则执行动作,然后忽略其余的测试/动作循环。测试通常是一个列表或列表表达式,但它也可以是一个符号或一个值。

一个典型的例子如下所示

(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) 返回的值,该值可能只是一个真值或假值。

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

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

(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 而感到困惑,但您可能会感到困惑!因此,最好使用更长且更具说明性的符号名称,并使用局部变量而不是全局变量。如果您这样做,您犯错或在以后误读代码的可能性较小。一般来说,不要在函数中引用未定义的符号,除非您确切地知道它来自哪里以及它的值是如何确定的。

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

华夏公益教科书