为啥 list-tail 会引发异常?为啥我们没有关于 cdr 的闭包属性?

Posted

技术标签:

【中文标题】为啥 list-tail 会引发异常?为啥我们没有关于 cdr 的闭包属性?【英文标题】:Why do list-tail raise-exception? Why do we haven't closure property with respect to cdr?为什么 list-tail 会引发异常?为什么我们没有关于 cdr 的闭包属性? 【发布时间】:2022-01-19 18:40:47 【问题描述】:

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

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

然后我们将自动拥有正常工作的列表尾

(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 仅适用于配对。当您到达列表末尾时,您不能一直调用cdr Common Lisp 允许 (cdr nil) => nil),但 Scheme 更严格。 我不会将cdr 称为combinator。这是combinationcombinator 之间的区别。 【参考方案1】:

Scheme 之所以没有这个是因为它的简约设计。该报告的详细说明如此之低,您可以进行指针运算并让程序段错误,因为任何错误的方案代码都被认为不是方案并且猪会飞。后来的报告(如 R7RS)需要更多的错误检查,因为在许多情况下需要发出错误信号,而早期报告中只是未定义的行为是可以的。

使用今天的方案,我们可以轻松创建 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 式后期绑定来增加标准过程,其中所有核心功能确实是方法,以便您可以定义自己的对象和访问器和为普通对创建的标准库和用户库对于具有类似对特征的新数据结构开箱即用。

【讨论】:

【参考方案2】:

历史的答案是:

最初,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 也有宽大的 carcdr

在上述论文中对此事做了进一步的说明,开头是:

采用 Interlisp 处理 NIL 并没有受到普遍的欢迎。

从中可以看出,五十、六十年前,Lisp 人已经分门别类,各方面意见不一。空列表的car 是否应该只产生空列表,或者错误输出是一个非常古老的问题。

现任 Google 人工智能总监的 Ashwin Ram 在 1986 年撰写 this poem 时发表了自己的观点,支持宽恕 cdr

这仍然是一个意见分歧的问题。

不可否认,灵活的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) 模式解构。

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

在 Scheme 中引入宽容的 carcdr 可能会比你想象的少。还有一个问题,Scheme 中唯一的 Boolean false 值是#f。 Scheme中的空列表()不为假。

因此,即使car 宽容,这样的代码也无法工作。

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

(or (third list) 0)

为了在默认的 0 情况下在 Scheme 中工作,(third list) 必须返回布尔 false 值 #f

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

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

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

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

如果cddr 默认返回(),那么这总是正确的,所以测试是没有用的。 carcdr 的不同默认值可能比普通默认值更容易出错。

在 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

【讨论】:

IIRC,MacLisp 中的“Mac”来自 MIT 的“Project MAC”。【参考方案3】:

“你会得到一个例外。”

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

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

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

如您所知,像这样的函数的技术名称是偏函数。这是一个对某些输入不起作用的函数,会导致错误。

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

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

Common Lisp 在列表用完时返回 NIL:

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

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

【讨论】:

在 Lisp 中,如果你返回 nil,那么很容易做到这一点:(or (list-tail '(1 2 3) 3) 0) 如果你想要一个零而不是 nil。这在 Scheme 中不起作用,因为 () 不是假的。要允许控制流继续到0list-tail 必须返回#f【参考方案4】:

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

【讨论】:

对。我很感兴趣为什么方案是这样设计的,没有 car/cdr 闭包属性。它是功能还是设计缺陷。它更像是 scheme 不如 Common Lisp 一致,相当严格。 我认为他们认为这是一个特性,修复了 Lisp 出错的地方。列表不是无限的,当你到达末尾时请求下一个元素是没有意义的。

以上是关于为啥 list-tail 会引发异常?为啥我们没有关于 cdr 的闭包属性?的主要内容,如果未能解决你的问题,请参考以下文章

为啥numpy在尝试预测回归时会引发异常错误:“ufunc'add'没有包含具有签名匹配类型的循环”?

为啥引发异常会产生副作用?

为啥在启动 REST 服务时添加注解 FormDataParam 会引发异常?

当 cp 没有时,为啥 shutil.copy() 会引发权限异常?

WiX:在 ProgramFilesFolder 中安装应用程序会引发 AccessDenied 异常。为啥?

为啥 AWS Lambda 环境中的 EPPlus Excel 库会引发“'Gdip' 的类型初始化程序引发异常”