为什么 list-tail 引发异常?为什么我们没有关闭 属性 关于 cdr?

Why do list-tail raise-exception? Why do we haven't closure property with respect to cdr?

如果你评估 (list-tail '(1 2) 3) 诡计。你会得到一个例外。 将 '() 作为答案会更聪明。 总的来说,为什么我们没有关闭 属性 关于 cdr 组合器?可能会出现什么并发症?

让我的观点更清楚的例子 现在 (cdr (cdr (cdr '(1 2))) -> 引发异常 应该是 (cdr (cdr (cdr ... (cdr '(1 2))...))) -> ()

然后我们将自动拥有正常工作的 list-tail

(define (list-tail list n) 
  (if (= n 0)
      list
      (list-tail (cdr list) (- n 1)))

Group-by 可以写得优雅无异常

(define (group-by list-arg n)
  (if (null? list-arg)
      '()
      (cons (list-head n) (list-tail n))))

cdr 只允许成对出现。到达列表末尾时,值为(),不是一对,所以报错

您可以在您的 list-tail 程序中对此进行检查,以使其更加宽松。

(define (list-tail list n)
  (if (or (= n 0) (not (pair? list)))
      list
      (list-tail (cdr list) (- n 1)))

使用 (not (pair? list)) 也可以使其适用于 (1 2 . 3) 等不正确的列表。对于任何 n >= 2.

,它将继续返回 3

“你会得到一个例外。”

这是许多核心库的问题,而不仅仅是 Scheme。取Haskell的核心库:

 tail [1] -- []
 tail []  -- error

 head [1] -- 1
 head []  -- error

如您所知,像这样的函数的技术名称是部分函数。该函数不适用于某些输入,导致错误。

所以,是的,您可以定义自己的版本。不过有一件事——在结束条件下应该 return 编辑什么?应该 (list-tail '(1 2) 3) return () 还是 return 0?如果我试图获取一个值以添加到另一个数字,那么 0 将是合适的。如果我使用 cons 来收集值,那么 () 将是合适的。我想这就是为什么该函数保留为部分函数的原因。

“是的。我很感兴趣为什么 scheme 是这样设计的,没有 car/cdr 闭包 属性。它是特性还是只是设计缺陷。它更像是 scheme 不如 Common Lisp 一致,相当严格。"

Common Lisp return当它用完列表时为 NIL:

(car '(1)) ; 1
(car '())  ; NIL
(cdr '(1)) ; 1
(cdr '())  ; NIL

在这种情况下,您必须测试 NIL,如果您想要零,则进行替换。

历史答案是:

  • 最初,John MacCarthy 创建的 Lisp 1 和 1.5 语言不允许 (CDR NIL)CDR 函数需要一个 cons 单元参数。

  • (CDR NIL) 只用 return NIL 会很方便的想法来自一种叫做 Interlisp 的方言(但可能已经存在于其他地方)。

  • 在 1960 年代,出现了另一种主要的 Lisp 方言,称为 MacLisp(比 Apple Mac 早了二十年,无关)。

根据 Peter Gabriel 和 Guy Steele 的 The Evolution of Lisp,一些 MacLisp 人在 1974 年与 Interlisp 人举行了一次交流会:

In 1974, about a dozen persons attended a meeting at MIT between the MacLisp and Interlisp implementors, including Warren Teitelman, Alice Hartley, Jon L White, Jeff Golden, and Guy Steele. There was some hope of finding substantial common ground, but the meeting actually served to illustrate the great chasm separating the two groups, in everything from implementation details to overall design philosophy. [...] In the end only a trivial exchange of features resulted from “the great MacLisp/Interlisp summit”: MacLisp adopted from Interlisp the behavior (CAR NIL)NIL and (CDR NIL)NIL, and Interlisp adopted the concept of a read table.

Interlisp 和 MacLisp 都是 Common Lisp 的祖先方言,Common Lisp 也有宽容的 carcdr.

以上论文对此事作了进一步说明,开头为:

The adoption of the Interlisp treatment of NIL was not received with universal warmth.

由此可见,在五十、六十年前,Lisp人就已经分成了阵营,大家意见不一。空列表的 car 是否应该只生成空列表,或者错误输出是一个非常古老的问题。

Ashwin Ram,现任 Google 的 AI 主管,在 1986 年发表了自己的观点,支持宽恕 cdr,当时他创作了 this poem.

它仍然是一个存在分歧的问题,见仁见智。

不可否认的是,灵活的carcdr以及它们的派生词可以帮助你“代码高尔夫”列表处理代码。

确实,这种代码打架的代码有时只处理满意的情况而没有错误检查,这在某些情况下可能会导致问题。

例如,某些假定始终包含三个项目的列表受制于 (caddr list) 以获得第三个项目。但是,由于一些错误,它只有两个。现在代码只是 运行 关闭 nil 值,这可能会导致其他问题。例如,假设该值应该是一个字符串,并且在其他地方的一些完全不同的函数中,nil 被传递给一些需要字符串并爆炸的 API。现在您正在搜索代码以发现此 nil 的来源。

编写依赖于 carcdr 执行的宽容解构的 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 严格检查。它在后台生成带有 carcaddr 和其他函数的代码,以及错误检查代码。列表 (1 2 3) 不会被模式 (a b).

解构

您必须查看整个语言及其使用方式以及其中的其他内容。

将宽容 carcdr 引入 Scheme 可能会给您带来比您想象的更少的里程数。还有一个问题,就是Scheme中唯一的布尔假值是#f。 Scheme中的空列表()不假

因此,即使 car 是宽容的,这样的代码也行不通。

假设列表的第三个元素总是一个数字,否则它不存在。在 Lisp 中,我们可以这样做来默认为零:

(or (third list) 0)

为了在默认 0 情况下在 Scheme 中工作,(third list) 必须 return 布尔假值 #f.

一个合理的方法可能是为 carcdr 设置不同的默认值:

(car ()) -> #f
(cdr ()) -> ()

然而,这是相当随意的:它在某些情况下有效,但在以下情况下失败:

;; if list has more than two items ...
(if (cddr list) ...)

If cddr returns () 默认情况下,那么它总是正确的,所以测试是无用的。 carcdr 的不同默认值可能比普通默认值更容易出错。

在 Lisp 中,宽容列表访问器以协同方式工作,空列表为假,这就是为什么曾几何时我很惊讶地得知宽容列表访问器进入游戏的时间相当晚。

早期的 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中,class null就是那个class,它的唯一实例是对象nil。当方法参数专用于 null 时,该方法仅在该参数的参数 nil.

时才有效

class t 是一切的超级class:类型主轴的顶部。 (还有一个类型主轴的底部,名为 nil 的 class,它不包含任何实例,并且是所有内容的子 class。)

null 上特化方法让我们可以捕获带有 nil 参数的方法调用。由于 CLOS 多重分派,因此可以在任何参数位置处理这些。因此,null 是一个 class,空对象模式在 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
}

为什么 Scheme 没有这个是因为它的简约设计。这份报告太不明确了,你可以做指针运算,让程序出现段错误,因为任何错误的方案代码都被认为不是方案,猪会飞。后来的报告,如 R7RS,需要更多的错误检查,因为它需要在许多情况下发出错误信号,而在早期报告中只有未定义的行为是可以的。

使用今天的 Scheme,我们可以轻松创建 carcdr 来满足您的需求:

#!r7rs

(define-library
  (sylwester pair-accessors)
  (export car cdr)
  (import (rename (scheme base) (car base:car) (cdr base:cdr))
          (except (scheme base) (car cdr)))
  (begin
    (define (car v)
      (if (pair? v)
          (base:car v)
          '()))
    (define (cdr v)
      (if (pair? v)
          (base:cdr v)
          '()))))

因此,在您的库或程序中,您只需导入 (scheme)(或 (scheme base))而无需 carcdr,并且还导入 (sylwester pair-accessors) 并且您'重新开始营业。或者,您可以制作一个 (scheme base)(scheme),用您自己的安全访问器替换所有访问器,使用宏生成它们。

你唯一不能做的就是将你的 car/cdr 版本注入到已经定义的库中,因为这需要一些后期绑定或猴子修补,但这不受支持语言。我对这些东西着迷,并且很想制作一个 OO 方案,您可以在其中使用一些类似 CLOS 的后期绑定来增强标准过程,其中所有核心功能实际上都是方法,以便您可以定义自己的对象和访问器,并且为普通对创建的标准库和用户库将开箱即用,适用于具有对等功能的新数据结构。