实用zhlisp编程05:函数

  • 0

实用zhlisp编程05:函数

Category:中文学习 Tags : 

第5章 函数

有了语法和语义规则以后,所有 Lisp 程序的三个最基本组成部分就是函数、变量和宏。在第 3 章里构建数据库时已经全部用到了这三个组件,但是我没有详细提及它们是如何工作的以及如何更好使用它们细节。接下来的几章将专门讲解这三个主题,先从函数开始——就像在其他语言里那样,函数提供了用于抽象和功能化的基本方法。

Lisp 本身是由大量函数组成的。其语言标准中有超过四分之三的名字用于定义函数。所有内置的数据类型纯粹是用操作它们的函数来定义的。甚至连 Lisp 强大的对象系统也是构建在函数的概念性扩展——广义函数(generic function)之上的,第 16 章将会介绍它们。

并且,尽管宏对于 Lisp 风格有着重要的作用,但最终所有实际的功能还是由函数来提供的。宏运行在编译期,因此它们生成的代码,即当所有宏被展开后将实际构成程序的那些代码,将完全由对函数和特殊操作符的调用所构成。更不用说,宏本身也是函数了——尽管这种函数是用来生成代码,而不是用来完成实际的程序操作的。

5.1 定义新函数

函数一般使用函数 宏来定义。函数 的基本结构看起来像这样:

(函数 name (参数*)
"可选的文档字符串。"
身体形态*)

任何符号都可被用作函数名。 通常函数名仅包含字典字符和连字符,但是在特定的命名约定里,其他字符也被允许使用。例如,将值的一种类型转换成另一种的函数有时会在名字中使用 ->。例如,一个将字符串转换成微件(widget)的函数可能被叫做 string->widget。最重要的一个命名约定是在第2章里提到的那个,即要用连字符而不是下划线或内部大写来构造复合名称。因此,frob-widget 比 frob_widget 或 frobWidget 具有 Lisp 风格。

一个函数的形参列表定义了一些变量,将被用来保存函数在调用时所传递的实参。如果函数不带有实参,则该列表就是空的,写成 ()。不同种类的形参分别负责处理必要的、可选的、多重的以及关键字实参。我将在下一节里讨论相关细节。

如果一个字符串紧跟在形参列表之后,那么它应该是一个用来描述函数用途的文档字符串。当定义函数时,该文档字符串将被关联到函数名上,并且以后可以通过 函数说明 函数来获取。

最后,一个 函数 的主体可由任意数量的 Lisp 表达式所构成。它们将在函数被调用时依次求值,而最后一个表达式的值将被作为整个函数的值返回。另外 返回出口 特殊操作符可被用于从函数的任何位置立即返回,我很快就会谈及它。

在第 2 章里我们写过一个 你好-世界 函数,看起来像这样:

(函数 你好-世界 () (格式 t "你好,世界"))

现在可以分析一下该程序的各个部分了。它的名字是 你好-世界;形参列表为空,因此不接受任何参数;它没有文档字符串;并且它的函数体由一个表达式所构成:

(格式 t "你好,世界")

下面是一个稍微更复杂一些的函数:

(函数 变量求和 (x y)
"执行函数后,将任意两个数字相加。"
(格式 t "求两个数的和 ~d 与 ~d.~%" x y)
(+ x y))

这个函数称为 变量求和,它接受的两个实参分别与形参 x 和 y 一一对应,并且带有一个文档字符串,以及一个由两个表达式所组成的主体。由 + 调用所返回的值将成为 变量求和 的返回值。

5.2 函数形参列表

关于函数名或文档字符串就没有更多可说的了,而本书其余部分将用很多篇幅来描述所有可在一个函数体里做的事情,因此就只需讨论形参列表了。

很明显,一个形参列表的基本用途是为了声明一些变量,用来接收传递给函数的实参。当形参列表是一个由变量名所组成的简单列表时,如同在 变量求和 里那样,这些形参被称为必要形参。当函数被调用时,必须为它的每一个必要形参都提供一个实参。每一个形参被绑定到对应的实参上。如果一 个函数以过少或过多的实参来调用的话,Lisp 就会报错。

但是,Common Lisp 的形参列表也给了你更灵活的方式将函数调用实参映射到函数形参。除了必要形参以外,一个函数还可以有可选形参,或是也可以用单一形参绑定到含有任意多个额外参数的列表上。最后,参数还可以通过关键字而不是位置来映射到形参上。这样,Common Lisp 的形参列表对于几种常见的编码问题提供了一种便利的解决方案。

5.3 可选形参

虽然许多像 变量求和 这样的函数只有必要形参,但并非所有函数都如此简单。有时一个函数将带有一个只有特定调用者才会关心的形参,这可能是因为它有一个合理的默认值。例如一个可以创建按需增长的数据结构的函数。由于数据结构可以增长,那么从正确性角度来说,它的初始尺寸就无关紧要了。那些清楚知道自己打算在数据结构中放置多少个元素的调用者们,可以通过设置特定的初始尺寸来改进其程序的性能,而多数调用者只需让实现数据结构的代码自行选择一个好的通用值就可以了。在 Common Lisp 中,你可以使用可选形参,从而使两类调用者都满意。不在意的调用者们将得到一个合理的默认值,而其他调用者们有机会提供一个指定的值。

为了定义一个带有可选形参的函数,在必要形参的名字之后放置符号 &optional,后接可选形参的名字。下面就是一个简单的例子:

(函数 临时 (a b &optional c d) (列表 a b c d))

当该函数被调用时,实参被首先绑定到必要形参上。在所有必要形参都被赋值以后,如果还有任何实参剩余,它们的值将被赋给可选形参。如果实参在所有可选形参被赋值之前用完了,那么其余的可选形参将自动绑定到值 NIL 上。这样,前面定义的函数会给出下面的结果:

(临时 1 2)     ==> (1 2 NIL NIL)
(临时 1 2 3)   ==> (1 2 3 NIL)
(临时 1 2 3 4) ==> (1 2 3 4)

Lisp 仍然可以确保适当数量的实参被传递给函数——在本例中是 2 到 4 个。而如果函数用太少或太多的参数来调用的话,将会报错。

当然,你会经常想要一个不同于 NIL 的默认值。这时可以通过将形参名替换成一个含有名字跟一个表达式的列表来指定该默认值。只有在调用者没有传递足够的实参来为可选形参提供值的时候,这个表达式才会被求值。通常情况只是简单地提供一个值作为表达式:

(函数 临时 (a &optional (b 10)) (列表 a b))

上述函数要求将一个实参绑定到形参 a 上。当存在第二个实参时,第二个形参 b 将使用其值,否则使用 10。

(临时 1 2) ==> (1 2)
(临时 1)   ==> (1 10)

不过有时可能需要更灵活地选择默认值。比如可能想要基于其他形参来计算默认值。默认值表达式可以引用早先出现在形参列表中的形参。如果要编写一个返回矩形的某种表示的函数,并且想要使它可以特别方便地产生正方形,那么可以使用一个像这样的形参列表:

(函数 制作矩形 (宽 &optional (高 宽)) ...)

除非明确指定否则这将导致 形参带有和  形参相同的值。

有时,有必要去了解一个可选形参的值究竟是被调用者明确指定还是使用了默认值。除了通过代码来检查形参的值是否为默认值(假如调用者碰巧显式传递了默认值,那么这样做终归是无效的)以外,你还可以通过在形参标识符的默认值表达式之后添加另一个变量名来做到这点。该变量将在调用者实际为该形参提供了一个实参时被绑定到真值,否则为 NIL。通常约定,这种变量的名字与对应的真实形参相同,但是带有一个 -supplied-p 后缀。例如:

(函数 临时 (a b &optional (c 3 c-supplied-p))
(列表 a b c c-supplied-p))

这将给出类似下面的结果:

(临时 1 2)   ==> (1 2 3 NIL)
(临时 1 2 3) ==> (1 2 3 T)
(临时 1 2 4) ==> (1 2 4 T)

5.4 剩余形参

可选形参仅适用于一些较为分散并且不能确定调用者是否会供值的形参。但某些函数需要接收可变数量的实参,比如说前文已然出现过的一些内置函数。格式 有两个必要实参,即流和控制串。但在这两个之后,它还需要一组可变数量的实参,这取决于控制串需要插入多少个值。+ 函数也接受可变数量的实参——没有特别的理由限制它只能在两个数之间相加,它对任意数量的值做加法运算(它甚至可以没有实参,此时返回 0——加法的底数)。下面这些都是这两个函数的合法调用:

(格式 t "你好,世界")
(格式 t "你好, ~a" name)
(格式 t "x: ~d y: ~d" x y)
(+)
(+ 1)
(+ 1 2)
(+ 1 2 3)

很明显,也可以通过简单地给它一些可选形参来写出接受可变数量实参的函数,但这样将会非常麻烦——光是写形参列表就已经足够麻烦了,何况还要在函数体中处理所有这些形参。为了做好这件事,还将不得不使用一个合法的函数调用所能够传递的那么多的可选形参。这一具体数量与具体实现相关,但可以保证至少有 50 个。在当前所有实现中,它的最大值范围从 4096 到 536,870,911。这种绞尽脑汁的无聊事情绝对不是 Lisp 风格。

相反,Lisp 允许在符号 &rest 之后包括一揽子形参。如果函数带有 &rest 形参,那么任何满足了必要和可选形参之后的其余所有实参就将被收集到一个列表里成为该 &rest 形参的值。这样,格式 和 + 的形参列表可能看起来会是这样:

(函数 格式 (流 字符串 &rest 值) ...)
(函数 + (&rest 数字) ...)

5.5 关键字形参

尽管可选形象和剩余形参带来了很大的灵活性,但两者却都不能帮助应对下面的情形。假设有一个接受四个可选形参的函数,如果在多数函数的调用中,调用者只想为四个参数中的一个提供值,并且更进一步,不同的调用者甚至可能将分别选择使用其中一个参数。

想为第一个形参提供值的调用者将会很方便——只需传递一个可选实参,然后忽略其他就好了。但是所有其他的调用者将不得不为所不关心的一到三个形参传递一些值。这不正是可选形参想来解决的问题吗?

当然是。问题在于可选形参仍然是位置相关的——如果调用者想要给第四个可选形参传递一个显式的值,就会导致前三个可选形参对于该调用者来说变成了必要形参。幸好我们有另一种形参类型,关键字形参,它可以允许调用者指定具体形参相应所使用的值。

为了使函数带有关键字形参,在任何必要的、&optional 和 &rest 形参之后,可以加上符号 &key 以及任意数量的关键字形参标识符,后者的格式类似于可选形参标识符。下面就是一个只有关键字形参的函数:

(函数 临时 (&key a b c) (列表 a b c))

当调用这个函数时,每一个关键字形参将被绑定到紧跟在同名键字后面的那个值上。如第 4 章所述,关键字是以冒号开始的名字,并且它们被自动定义为自求值常量。

如果一个给定的关键字没有出现在实参列表中,那么对应的形参将被赋予其默认值,如同可选形参那样。因为关键字实参带有标签,所以它们在必要实参之后可按任意顺序进行传递。例如 临时 可以用下列形式调用:

(临时)                ==> (NIL NIL NIL)
(临时 :a 1)           ==> (1 NIL NIL)
(临时 :b 1)           ==> (NIL 1 NIL)
(临时 :c 1)           ==> (NIL NIL 1)
(临时 :a 1 :c 3)      ==> (1 NIL 3)
(临时 :a 1 :b 2 :c 3) ==> (1 2 3)
(临时 :a 1 :c 3 :b 2) ==> (1 2 3)

如同可选形参那样,关键字形参也可以提供一个默认值形式以及一个 supplied-p 变量名。在关键字形参和可选形参中,这个默认值形式都可以引用那些早先出现在形参列表中的形参。

(函数 临时 (&key (a 0) (b 0 b-supplied-p) (c (+ a b)))
(列表 a b c b-supplied-p))

(临时 :a 1)           ==> (1 0 1 NIL)
(临时 :b 1)           ==> (0 1 1 T)
(临时 :b 1 :c 4)      ==> (0 1 4 T)
(临时 :a 2 :b 1 :c 4) ==> (2 1 4 T)

同样,如果出于某种原因想让调用者用来指定形参的关键字不同于实际形参名,那么可以将形参名替换成一个列表,令其含有调用函数时使用的关键字以及用作形参的名字。比如说下面这个 临时 的定义:

(函数 临时 (&key ((:苹果 a)) ((:盒 b) 0) ((:查理 c) 0 c-supplied-p))
(列表 a b c c-supplied-p))

可以让调用者这样调用它:

(临时 :苹果 10 :盒 20 :查理 30) ==> (10 20 30 T)

这种风格在想要完全将函数的公共 API 与其内部细节相隔离时特别有用,通常是因为想要在内部使用短变量名,而不是 API 中的描述性关键字。不过该特性不常被用到。

5.6 混合不同的形参类型

单一函数里使用所有四种类型形参的情况虽然罕见,但也是可能的。无论何时,当用到多种类型的形参时,它们必须以这样的顺序声明:首先是必要形参,其次是可选形参,再次是剩余形参,最后才是关键字形参。但在使用多种类型形参的函数中,一般情况是将必要形参和另外一种类型的形参组合使用,或者可能是组合 &optional 形参和 &rest 形参。其他两种组合方式,无论是 &optional 形参还是 &rest 形参,当与 &key 形参组合使用时,都可能导致某种奇怪的行为。

将 &optional 形参和 &key 形参组合使用时会产生令人惊奇的结果,因此也许应该避免将它们一起使用。问题出在如果调用者没有为所有可选形参提供值时,那么没有得到值的可选形参将吃掉原本用于关键字形参的关键字和值。例如,下面这个函数很不明智地混合了 &optional 和 &key 形参:

(函数 临时 (x &optional y &key z) (列表 x y z))

如果像这样调用的话,就没问题:

(临时 1 2 :z 3) ==> (1 2 3)

这样也可以:

(临时 1)  ==> (1 nil nil)

但是这样的话将会报错:

(临时 1 :z 3) ==> 错误

这是因为关键字 :z 被作为一个值填入到可选的 y 形参中了,只留下了参数 3 被处理。在这里,Lisp 期待一个成对的键/值,或者什么也没有,否则就会报错。也许更坏的是,如果该函数带有两个 &optional 形参,上面最后一个调用将导致值 :z 和 3 分别被绑定到两个 &optional 形参上,而 &key 形参 z 将得到默认值 NIL,而不声明缺失了东西。

一般而言,如果正在编写一个同时使用 &optional 形参和 &key 形参的函数,可能就应该将它变成全部使用 &key 形参的形式——它们更灵活,并且总会可以在不破坏该函数的已有调用的情况下添加新的关键字形参。也可以移除关键字形参,只要没人在使用它们。 一般而言,使用关键字形参将会使代码相对易于维护和拓展——如果需要为函数添加一些需要用到新参数的新行为,就可以直接添加关键字形参,而无需修改甚至重新编译任何调用该函数的已有代码。

虽然可以安全地组合使用 &rest 形参和 &key 形参,但其行为初看起来可能会有一点奇怪。正常地来讲,无论是 &rest 还是 &key 出现在形参列表中,都将导致所有出现在必要形参和 &optional 形参之后的那些值被特别处理——要么作为 &rest 形参被收集到一个形参列表中,要么基于关键字被分配到适当的 &key 形参中。如果 &rest 和 &key 同时出现在形参列表中,那么两件事都会发生——所有剩余的值,包括关键字本身,都将被收集到一个列表里,然后被绑定到 &rest 形参上;而适当的值,也会同时被绑定到 &key 形参上。因此,给定下列函数:

(函数 临时 (&rest rest &key a b c) (列表 rest a b c))

你将得到如下结果:

(临时 :a 1 :b 2 :c 3)  ==> ((:A 1 :B 2 :C 3) 1 2 3)

5.7 函数返回值

目前写出的所有函数都使用了默认的返回值行为,即最后一个表达式的值被作为整个函数的返回值。这是从函数中返回值的最常见方式。

但某些时候,尤其是想要从嵌套的控制结构中脱身时,如果有办法从函数中间返回。那将是非常便利的。在这种情况下,你可以使用返回出口 特别操作符,它能够立即以任何值从函数中间返回。

在第 20 章将会看到,返回出口 事实上不只用于函数,它还可以被用来从一个由 出口 特别操作符所定义的代码块中返回。不过 函数 会自动将其整个函数体包装在一个与其函数同名的代码块中。因此,对一个带有当前函数名和想要返回的值的 返回出口进行求值将导致函数立即以该值退出。返回出口 是一个特殊操作符,其第一个 “参数” 是它想要返回的代码块名。该名字不被求值,因此无需引用。

下面这个函数使用了嵌套循环来发现第一个数对——每个都小于 10,并且其乘积大于函数的参数,它使用 返回出口 在发现之后立即返回该数对:

(函数 临时 (n)
(计数循环 (i 10)
(计数循环 (j 10)
(如果真 (> (* i j) n)
(返回出口 临时 (列表 i j))))))

必须承认的是,不得不指定正在返回的函数名多少会有些不便——比如改变了函数的名字,就需要同时改变 返回出口 中所使用的名字。 但在事实上,显式的 返回出口 调用在 Lisp 中出现的频率远小于 return 语句在源自 C 的语言里所出现的频率,因为所有的 Lisp 表达式,包括诸如循环和条件语句这样的控制结构,都会在求值后得到一个返回值。因此在实践中这不是什么问题。

5.8 作为数据的函数——高阶函数

使用函数的主要方式是通过名字来调用它们,但有时将函数作为数据看待也是很有用的。例如,可以将一个函数作为参数传给另一个函数,从而能写出一个通用的排序函数,允许调用者提供一个比较任意两元素的函数,这样同样的底层算法就可以跟许多不同的比较函数配合使用了。类似地,回调函数(callback)和钩子(hook)也需要能够保存代码引用便于以后运行。由于函数已经是一种对代码比特进行抽象的标准方式,因此把允许函数视为数据也是合理的。

在 Lisp 中,函数只是另一种类型的对象。被用 函数 定义一个函数时,实际上做了两件事:创建一个新的函数对象以及赋予其一个名字。在第 3 章里我们看到,也可以使用 表达式 表达式来创建一个函数而无需为其指定一个名字。一个函数对象的实际表示,无论是有名的还是匿名的,都只是一些二进制数据——以原生编译的 Lisp 形式存在,可能大部分是由机器码构成。只需要知道如何保持它们以及需要时如何调用它们。

特别操作符 函数 提供了用来获取一个函数对象的方法。它接受单一实参并返回该参数同名的函数。这个名字是不被引用的。因此如果一个函数 foo 的定义如下:

CL-USER> (函数 临时 (x) (* 2 x))
临时

就可以得到如下的函数对象:

CL-USER> (函数 临时)
#<Interpreted 函数 临时>

事实上,你已经用过 函数 了,但它是以伪装的形式出现的。第 3 章里用到的 #' 语法就是 函数 的语法糖,正如 '引用 的语法糖一样。 因此也可以像这样得到 临时 的函数对象。

CL-USER> #'临时
#<Interpreted 函数 临时>

一旦得到了函数对象,就只剩下一件事可做了——调用它。Common Lisp 提供了两个函数用来通过函数对象调用函数:函数调用函数调用列表。 它们的区别仅在于如何获取传递给函数的实参。

函数调用 用于在编写代码时确切知道传递给函数多少实参时。函数调用 的第一个实参是被调用的函数对象,其余的实参被传递到该函数中。因此,下面两个表达式是等价的:

(临时 1 2 3) === (函数调用 #'临时 1 2 3)

不过,用 函数调用 来调用一个写代码时名字已知的函数毫无意义。事实上,前面的两个表达式将很可能被编译成相同的机器指令。

下面这个函数演示了 函数调用 的另一个更有建设性的用法。它接受一个函数对象作为实参,并使用实参函数在 最小值最大值 之间以 步进 为步长的返回值来绘制一个简单的 ASCII 艺术条形图:

(函数 情节 (新值 最小值 最大值 步进)
(循环 for i from 最小值 to 最大值 by 步进 do
(循环 repeat (函数调用 新值 i) do (格式 t "*"))
(格式 t "~%")))

函数调用 表达式在每个 i 值上计算函数的值。内层 循环 循环使用计算得到的值来决定向标准输出打印多少星号。

请注意,不需要使用 函数 或 #' 来得到 fn 的函数值。因为它是作为函数对象的变量的值,所以你需要它被解释成一个变量。可以用任何接受单一数值实参的函数来调用 plot,例如内置的函数 EXP,它返回以 e 为底而以实参为指数的值。

CL-USER> (情节 #'exp 0 4 1/2)
*
*
**
****
*******
************
********************
*********************************
******************************************************
NIL

然而,当实参列表只在运行期已知时,函数调用 的表现不佳。例如,为了再次调用 情节 函数,假设你已有一个列表,其包括一个函数对象,一个最小值和一个最大值以及一个步长。换句话说,这个列表包含了你想要作为实参传给 情节 的所有的值。假设这个列表保存在变量 情节-数据 中,可以像这样用列表中的值来调用 情节

(情节 (first 情节-数据) (second 情节-数据) (third 情节-数据) (fourth 情节-数据))

这样固然可以,但仅仅为了将实参传给 情节 而显式地将其解开,看起来相当讨厌。

这就是需要 函数调用列表 的原因。和 函数调用 一样,函数调用列表 的第一个参数是一个函数对象。但在这个函数对象之后,它期待一个列表而非单独的实参。它将函数应用在列表中的值上,这就使你可以写出下面的替代版本:

(函数调用列表 #'情节 情节-数据)

更方便的是,函数调用列表 还接受 “孤立”(loose)的实参只要最后一个参数是个列表。因此,假如 plot-data 只含有最小、最大和步长值,那么你仍然可以像这样来使用 函数调用列表 在该范围上绘制 EXP 函数:

(函数调用列表 #'情节 #'exp 情节-数据)

函数调用列表 并不关心所用的函数是否接受 &optional&rest 或是 &key 实参——由任何孤立实参和最后的列表所组合而成的实参列表必定是一个合法的实参列表,其对于该函数来说带有足够的实参用于所有必要形参和适当的关键字形参。

5.9 匿名函数

一旦开始编写或只是使用那些可以接受其他函数作为实参的函数,你就必然发现,有时不得不去定义和命名一个仅使用一次的函数是(尤其是你可能从不用名字来调用它时),这会让人相当恼火。

觉得没必要用 函数 来定义一个新函数时,可以使用一个 表达式 表达式来创建匿名的函数。第 3 章里讨论过,一个 表达式 表达式形式如下:

(表达式 (参数) body)

可以将 表达式 表达式视为一种特殊类型的函数名,其名字本身直接描述函数的用途。这就解释了为什么可以使用一个带有 #'表达式 表达式来代替一个函数名。

(函数调用 #'(lambda (x y) (+ x y)) 2 3) ==> 5

甚至还可以在一个函数调用表达式中将 表达式 表达式用作函数名。由此一来,我们也可以在需要时以更简洁方式来书写前面的函数调用 表达式如下:

((表达式 (x y) (+ x y)) 2 3) ==> 5

但几乎没人这样做。它唯一的用途是来强调将 表达式 表达式用在任何一个正常函数名可以出现的场合都是合法的。

在需要传递一个作为参数的函数给另一个函数,并且需要传递的这个函数简单到可以内联表达时,匿名函数特别有用。例如,假设想要绘制函数 2x,你可以定义下面的函数:

(函数 加倍 (x) (* 2 x))

并随后将其传给 情节

CL-USER> (情节 #'加倍 0 10 1)

**
****
******
********
**********
************
**************
****************
******************
********************
NIL

但如果写成这样将会更简单和清晰:

CL-USER> (情节 #'(lambda (x) (* 2 x)) 0 10 1)

**
****
******
********
**********
************
**************
****************
******************
********************
NIL

表达式 表达式的另一项重要用途是制作闭包(closure),即捕捉了其创建时环境信息的函数。你在第 3 章里使用了一点儿闭包,但要深入了解闭包的工作原理及其用途,更多的还是要从变量而非函数的角度去考察,因此我将在下一章里讨论它们。

1尽管Common Lisp中的函数很重要,但将它描述为函数式语言并不十分准确。的确,Common Lisp的一些功能,例如它的列表操作功能,被设计成以体形*风格使用,而且Lisp在函数式编程的历史中占有突出地位 – 麦卡锡引入了许多现在被认为是的想法在函数式编程中很重要 – 但Common Lisp是专门为支持许多不同类型的编程而设计的。在Lisp系列中,Scheme是最接近“纯粹”功能语言的东西,甚至它还有一些功能,与Haskell和ML等语言相比,它使其与绝对纯度相悖。

2好吧,几乎任何符号。如果您使用语言标准中定义的任何名称作为您自己的某个函数的名称,则会定义会发生什么。但是,正如您将在第21章中看到的那样,Lisp包系统允许您在不同的命名空间中创建名称,因此这不是一个真正的问题。

3由于Lisp函数概念与表达式演算之间的历史关系,参数列表有时也称为表达式列表

4例如,以下内容:

(函数说明 ‘临时 ‘函数)

返回该函数的文档字符串临时。但请注意,文档字符串仅供人类使用,而非程序访问。存储它们不需要 Lisp实现 ,并且允许它们随时丢弃它们,因此便携式程序不应该依赖它们的存在。在一些实现中,需要在存储文档字符串之前设置实现定义的变量。

5在不直接支持可选参数的语言中,程序员通常会找到模拟它们的方法。一种技术是使用调用者可以传递的区别“无值”值来指示他们想要给定参数的默认值。例如,在C中,通常使用空表判断这样的区分值。但是,函数与其调用者之间的这种协议是临时的 – 在某些函数中或某些参数 空表判断可能是区分值,而在其他函数或其他参数中,魔术值可能是-1或某些#defined常量。

6常量CALL-ARGUMENTS-LIMIT告诉您特定于实现的值。

7四个标准功能都采取 &optional&keyarguments– 读-FROM-字符串PARSE-名字字符串WRITE-LINE,和WRITE-字符串。在标准化过程中,它们被保留了这种方式,以便与早期的Lisp方言向后兼容。读-FROM-字符串往往是最频繁捕获新Lisp程序员的一个 – 一个 (读-from-字符串 s :开始 10)似乎忽略 :开始关键字参数的调用,从索引0而不是10读取。这是因为读-FROM-字符串 还有两个&optional 参数吞噬了参数:开始和10。

8另一个宏,返回不需要名称。但是,您不能使用它而不是返回出口 为了避免必须指定函数名称; 它是从一个名为的块返回的语法糖NIL。我将它与细节一起出口,并返回出口在第20章。

9 Lisp中,当然是不以治疗功能数据的唯一语言。C使用函数指针,Perl使用子程序引用,Python使用类似于Lisp的方案,C#引入委托,本质上是类型化的函数指针,作为对Java的相当笨重的反射和匿名类机制的改进。

10函数对象的精确打印表示将从实现到实现不同。

11最好的思考方式函数对象是作为一种特殊的引用。 引用一个符号可以防止它被评估,导致符号本身而不是该符号命名的变量的值。函数对象也避开了正常的评估规则,但是,它不会阻止对符号进行评估,而是将其作为函数的名称进行计算,就像它在函数调用表达式中用作函数名时一样。

12实际上有第三个是特殊运算符 MULTIPLE-值-CALL,但是当我讨论第20章中返回多个值的表达式时,我会保存它。

13在Common Lisp中,也可以使用表达式表达式作为参数 函数调用(或其他一些带有函数参数的函数,如排序映射函数),#’之前没有,如下所示:

(函数调用 (表达式 (x y) (+ x y)) 2 3)

这是合法的,并且相当于带有#’但是有一个棘手的原因的版本。历史上,表达式表达式本身并不是可以评估的表达式。那 表达式不是函数,宏或特殊运算符的名称。相反,以符号开头的列表表达式是一种特殊的句法结构,Lisp将其识别为一种函数名称。

但如果仍然如此,那么(函数调用 (表达式 (…) …)) 这将是非法的,因为它函数调用是一个函数,函数调用的正常评估规则需要对表达式 表达式进行求值。然而,在ANSI标准化过程的后期,为了能够实现ISLISP,同时标准化的另一个Lisp方言,严格地说是Common Lisp之上的用户级兼容层,表达式 定义了一个宏扩展到一个函数对象包围表达式表达式的调用。换句话说,以下 表达式表达式:

(表达式 () 42)

当它在评估的上下文中发生时会扩展到以下内容:

(函数对象 (表达式 () 42))   ; 或 #'(lambda () 42)

这使得它在价值位置使用,例如函数调用法律的参数 。换句话说,它是纯粹的语法糖。大多数人要么总是#’在表达式表达式中使用值位置,要么永远不会。在这本书中,我总是使用#’。


Leave a Reply

搜索

分类目录

公 告

本网站学习论坛:

www.zhlisp.com

lisp中文学习源码:

https://github.com/zhlisp/

欢迎大家来到本站,请积极评论发言;

加QQ群学习交流。