历史的答案是:
-
最初,John MacCarthy 创建的 Lisp 1 和 1.5 语言不允许(CDR NIL)。 CDR 函数需要一个 cons 单元格参数。
-
(CDR NIL) 只返回 NIL 会很方便的想法来自一种称为 Interlisp 的方言(但可能已经存在于其他地方)。
-
在 1960 年代,还有一种主要的 Lisp 方言,称为 MacLisp(比 Apple Mac 早了二十年,不相关)。
根据 Peter Gabriel 和 Guy Steele 的 The Evolution of Lisp 的说法,一些 MacLisp 人在 1974 年与 Interlisp 人举行了仪式:
1974 年,大约有十几人参加了在麻省理工学院举行的 MacLisp 和 Interlisp 实施者之间的会议,其中包括 Warren Teitelman、Alice Hartley、Jon L White、Jeff Golden 和 Guy Steele。有一些希望找到实质性的共同点,但这次会议实际上说明了两个团队之间的巨大鸿沟,从实施细节到整体设计理念。 [...] 最后,“伟大的 MacLisp/Interlisp 峰会”只产生了微不足道的功能交换:MacLisp 采用了 Interlisp 的行为(CAR NIL) → NIL 和 (CDR NIL) → NIL,而 Interlisp 采用了这个概念
读表。
Interlisp 和 MacLisp 都是 Common Lisp 的祖传方言,Common Lisp 也有宽大的 car 和 cdr。
在上述论文中对此事做了进一步的说明,开头是:
采用 Interlisp 处理 NIL 并没有受到普遍的欢迎。
从中可以看出,五十、六十年前,Lisp 人已经分门别类,各方面意见不一。空列表的car 是否应该只产生空列表,或者错误输出是一个非常古老的问题。
现任 Google 人工智能总监的 Ashwin Ram 在 1986 年撰写 this poem 时发表了自己的观点,支持宽恕 cdr。
这仍然是一个意见分歧的问题。
不可否认,灵活的car、cdr及其衍生产品可以帮你“打码高尔夫”列表处理代码。
确实,这样的代码打高尔夫球的代码有时只处理快乐的情况而没有错误检查,这在某些情况下可能会导致问题。
例如,假设某些列表始终包含三个项目,则需要通过(caddr list) 才能获得第三个项目。但是,由于一些错误,它只有两个。现在代码刚刚以nil 值运行,这可能会在其他地方引起问题。例如,假设值应该是一个字符串,而在其他地方的一些完全不同的函数中,nil 被传递给需要一个字符串的某个 API 并崩溃了。现在您正在搜索代码以发现这个nil 的来源。
编写依赖于car 和cdr 执行的宽容解构的Lisp 解释器或编译器的人最终会产生一些默默接受错误语法的东西。
例如
(defun interpret-if (form env)
(let ((test (car form))
(then (cadr form))
(else (caddr form)))
(if (interpret-expr test env)
(interpret-expr then env)
(interpret-expr else env))))
这实际上是讨论问题双方的一个很好的例子。
一方面,代码简洁,并且很好地支持可选的 else 子句:这个解释器的用户可以做:
(if (> x y)
(print "x is greater than y"))
在interpret-if 中,else 变量将取出一个nil,并将其传递给(eval expr else env),它的计算结果为nil,一切都很酷;由于caddr 没有抱怨,else 的可选性是免费获得的。
另一方面,解释器没有诊断出这个:
(if) ;; no arguments at all
或者这个:
(if (> x y)) ;; spec says "then" is required, but no error!
然而,所有这些问题都有很好的解决方案和工作方式,不需要收紧列表访问器功能,因此我们可以在需要时使用简洁的编码。例如,解释器可以使用某种模式匹配,比如 Common Lisp 的基本 destructuring-bind:
(defun interpret-if (form env)
(destructuring-bind (test then &optional else) form
(if (interpret-expr test env)
(interpret-expr then env)
(interpret-expr else env))))
destructuring-bind 有严格的检查。它在后台生成具有car、caddr 和其他功能的代码,以及错误检查代码。列表(1 2 3) 不会被(a b) 模式解构。
您必须查看整个语言及其使用方式以及其中的其他内容。
在 Scheme 中引入宽容的 car 和 cdr 可能会比你想象的少。还有一个问题,Scheme 中唯一的 Boolean false 值是#f。 Scheme中的空列表()不为假。
因此,即使car 宽容,这样的代码也无法工作。
假设列表的第三个元素总是一个数字,否则它不存在。在 Lisp 中,我们可以将其默认为零:
(or (third list) 0)
为了在默认的 0 情况下在 Scheme 中工作,(third list) 必须返回布尔 false 值 #f。
一种合理的方法可能是为car 和cdr 设置不同的默认值:
(car ()) -> #f
(cdr ()) -> ()
但是,这是相当随意的:它在某些情况下有效,但在以下情况下失败:
;; if list has more than two items ...
(if (cddr list) ...)
如果cddr 默认返回(),那么这总是正确的,所以测试是没有用的。 car 和 cdr 的不同默认值可能比普通默认值更容易出错。
在 Lisp 中,宽恕列表访问器以协同方式工作,空列表为假,这就是为什么从前我很惊讶地发现宽恕列表访问器在游戏中出现得相当晚。
Early Scheme 是作为一个用 Lisp 编写的项目实现的,因此为了与宿主语言顺利互操作,它使用相同的约定:() 是 NIL 是空列表和 false。这最终被改变了,所以如果你希望恢复它,你要求 Scheme 恢复一个几十年前的决定,这现在几乎是不可能的。
面向对象的编程也重视这一点。 (car nil) 做某事而不是失败的事实是空对象模式的一个实例,它很有用而且很好。我们可以在 Common Lisp 对象系统中表达这一点,在其中它实际上消失了:
假设我们有一个 car 函数,它在非 conses 上崩溃了。我们可以写一个泛型函数kar,它没有,像这样:
;; generic fun
(defgeneric kar (obj))
;; method specialization for cons class: delegate to car.
(defmethod kar ((obj cons))
(car obj))
;; specialization for null class:
(defmethod kar ((obj null))) ;; return nil
;; catch all specialization for any type
(defmethod kar ((obj t))
:oops)
测试:
[1]> (kar nil)
NIL
[2]> (kar '(a . b))
A
[3]> (kar "string")
:OOPS
在 CLOS 中,类 null 是唯一实例是对象 nil 的类。当方法参数专用于null 时,该方法仅在该参数的参数nil 时才符合条件。
t 类是所有东西的超类:类型主轴的顶部。 (类型轴也有一个底部,名为nil 的类,它不包含实例,是所有事物的子类。)
在null 上专门化方法让我们捕获带有nil 参数的方法调用。由于 CLOS 多重分派,因此可以在任何参数位置处理这些。因此,null 是一个类,空对象模式在 CLOS 中消失了。
如果您正在与 OOP 人员讨论 Lisp,您可以提出 (car nil) 可以说是 Null Object 模式。
在许多较新的编程语言中都认识到显式 null 处理的不便。
现在的一个共同特点是拥有空安全对象访问。比如
foo.bar
如果foo 为空,可能会爆炸。所以给定的语言提供了
foo?.bar
或类似的,仅当foo 不为nil 时才会取消引用.bar,否则表达式产生nil。
当语言添加foo?.bar 时,它们不会丢弃foo.bar,或使foo.bar 表现得像foo?.bar。有时您想要错误(foo 为 nil 是您想要在测试中捕获的编程错误),有时您想要默认值。
有时您需要默认值,因此您可以折叠多个级别的默认值并捕获错误:
if (foo?.bar?.xyzzy?.fun() == nil) {
// we coudn't have fun(); handle it
// this was because either foo was nil, or else bar was nil,
// or else xyzzy was nil, or else fun() returned nil.
} else {
// happy case
}