使用函数中的lambda值作为列表的第一个元素

7
我是一个有用的助手,可以为您翻译文本。
我正在阅读Peter Norvig的《人工智能编程范例》,遇到了一个问题,但我自己无法解决(这是我接触Lisp的入门)。实际上,这个问题非常小,但显然我的小脑袋无法解决。
为什么当一个函数的值是lambda时,将该函数用作列表的第一个元素会导致错误。例如:
Lisp:
(defun some-func ()
  #'(lambda (x) x))

;; At REPL
;; Does not work
> ((some-func) 1)
;; Does work
> ((lambda (x) x) 1)
;; Also works
> (funcall (some-func) 1)

我希望你能理解!
4个回答

6
这是一个好问题,也是Common Lisp比较令人困惑的地方。由于历史原因,Common Lisp有两个命名空间——一个用于函数,另一个用于值。为了实现这一点,对于函数应用的头位置和其余部分有两种不同的求值规则——第一种将符号作为函数名进行求值,第二种将符号作为变量引用进行求值。显然,在某些情况下,该值实际上是一个函数——例如,如果您编写一个mapcar函数,您需要做一些类似的事情。
(defun my-mapcar (f l)
  (if (null l)
    '()
    (cons (f (car l)) (my-mapcar f (cdr l)))))

但是这样做行不通 - 它会抱怨 f 是一个未知的函数。对于这些情况,有一个特殊的函数叫做funcall,它接受一个函数和该函数的参数,并像往常一样应用该函数 - 由于funcall是一个普通函数,其所有参数都按照通常方式计算(作为值)。因此,上述问题可以通过使用它来解决:
(defun my-mapcar (f l)
  (if (null l)
    '()
    (cons (funcall f (car l)) (my-mapcar f (cdr l)))))

正如您现在可能怀疑的那样,有镜像情况--您想将某些内容作为函数而不是值进行评估。例如,以下内容无法正常工作:

(my-mapcar 1+ '(1 2 3))

因为它引用的是1+变量而不是函数。对于这些情况,有一种特殊形式叫做function,它将其内容作为函数进行评估并将其作为值返回:

(my-mapcar (function 1+) '(1 2 3))

而且它可以使用#'进行缩写:

(my-mapcar #'1+ '(1 2 3))

这个故事还没有结束——举几个例子:
  • 在某些情况下,一个简单的引用名称可以作为函数使用——例如,最后一个示例中的'1+可行——但这是一种只能查看全局绑定名称的hack,因此#'几乎总是更好的选择。

  • 类似的hack可以与lambda一起使用——因此,您可以使用(my-mapcar '(lambda (x) (1+ x)) '(1 2 3)),实际上,您可以使用(list 'lambda '(x) '(1+ x)),但这甚至更糟糕(并且IIRC,不可移植),但使用(lambda (x) (1+ x))可以工作,因为它隐式包装在#'中(尝试将lambda表单扩展为宏,你会看到它)。 相关的hack使得可以将lambda表达式用作函数应用的头部(这是你尝试过的事情之一)。

  • let等绑定局部值,在某些情况下,您将希望绑定局部函数——为此,有新的绑定构造: fletlabels

如果所有这些看起来都很奇怪和/或过于复杂,那么你并不孤单。 这是Common Lisp和Scheme之间的主要区别之一。 (然后差异导致两种语言中常见习惯用法的变化:Scheme代码倾向于更频繁地使用高阶函数,而Common Lisp代码则相反。像这样的信仰问题通常存在争议,有些人支持CL所做的事情,声称高阶函数很困惑,因此他们喜欢明确的代码提醒。)


谢谢Eli抽出时间写这篇文章,它帮了我很多!我现在不能给你点赞,但是等我可以的时候,我一定会的。再次感谢。 - Mark Skilbeck
7
我觉得将函数和变量的命名空间分开非常有用,我经常创建和使用高阶函数。 - Svante
7
在Common Lisp中,quote lambda不能正常工作。使用(my-mapcar '(lambda (x) ...) ...)会产生错误,这不是Common Lisp的语法。将其作为列表传入也不可行。我猜想Common Lisp中至少和Scheme一样多地使用高阶函数。例如,许多函数接受像KEY和TEST这样的函数作为参数,在Scheme中提供了EQ、EQL和EQUAL等多个版本。 - Rainer Joswig
1
Svante:是的,我确实说过偏好是一种信仰问题。至于你使用多少HO函数与其他人相比的任何轶事比较——这正是导致无处可谈的“讨论”的类型。我的“倾向于”显然也是主观的;它基于CL程序员经常声称他们更喜欢明确的区分这一事实。我清楚地知道你会如何反驳,既然这是主观的,那就这样吧。主要问题(将变得更加强烈)是这首先是一个信仰问题。 - Eli Barzilay
1
Rainer:你说得对——我应该澄清一下,“IIRC,不可移植”适用于在引用的lambda表达式之前的两种形式。 (顺便说一句,在Allegro中这对我有用,在CLISP中(如预期)不起作用。)(顺便说一句#2,我考虑过提到newLISP作为促进其成为基本功能的有趣实验,但那可能偏离了主题。) - Eli Barzilay

4

((lambda (x) ...) ...) 是求值规则中的硬编码特殊情况。它不会对形式中的第一个元素进行求值,并将结果用作一般情况。使用funcallapply调用通过求值某些其他形式得到的函数是很常见的。


2
我认为这就是 CLHS 3.1.2.1.2.4 所描述的内容: "lambda 表达式等同于在给定参数上使用词法闭包的 funcall。" http://www.lispworks.com/documentation/HyperSpec/Body/03_ababd.htm - Ken

2
Common Lisp不允许一个返回lambda函数的函数作为表达式中的第一项的原因与Lisp-1和Lisp-2之间的区别有关。如果Common Lisp允许((some-func) 1)等价于(funcall (some-func) 1),那么就可能被认为是不一致的,因为它不允许像(let ((f #'some-func)) (f 1))这样的用法,而要求使用(funcall f 1)。

1

不支持此类表单实际上有非常好的理由:它们是模棱两可的。考虑Common Lisp中function names的定义方式:

函数名 n。1.(在一个环境中)是该环境中一个函数名称符号列表(setf symbol)。2. 一个符号列表(setf symbol)

鉴于此,像下面这样的表单应该如何行为?

((setf car) 10 x)

这个代码应该将 x 的车设置为值 10,还是应该执行表单 (setf car)(这将是一个错误),并尝试使用其返回值作为要调用的函数与参数 10x?Common Lisp 没有指定任何行为,而且我并不确定这是一个坏选择。毕竟,就我所看到的,标准中没有阻止符合规范的实现扩展有效形式的定义以支持更广泛的运算符名称(因此,在这里特殊处理 setf 函数也不会有帮助)。


有趣。我本来以为内部表单会被执行并将其值用作函数(或函数名称——我仍然有点困惑两者之间的区别以及它们在哪里使用)。然而,当然,我错了,正如这个线程所证明的那样!嘿。考虑到评估规则,我想这是有道理的;不过,一开始可能有点困惑(甚至在此后的一段时间内也可能如此)。 - Mark Skilbeck

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接