了解Common Lisp中的tailp

时间:2018-05-26 11:51:23

标签: lisp common-lisp clisp

在浏览Bert Burgemeister的“Common Lisp Quick Reference”时,我偶然发现了tailp

首先,我误解了这个函数的定义。我试过了:

(tailp '(3 4 5) '(1 2 3 4 5))

但它返回了

NIL

CLTL2说,tailp是真的 iff 第一个参数是现有(nthcdr n list)的任何n

(nthcdr 2 '(1 2 3 4 5))
;; (3 4 5)

我进一步尝试了:

(tailp '(3 4 5) '(1 2 3 4 5))
;; NIL - and I would expect: T following the definition above.

(tailp '() '(1 2 3 4 5))
;; T
(tailp '5  '(1 2 3 4 . 5))
;; T

在我尝试之前(然后理解tailp查找l的cdr,即使是相同的地址。

(defparameter l '(1 2 3 4 5 6))
(tailp (nthcdr 3 l) l)
;; T

但接下来我提出了下一个问题:

For what such a function is useful at all?

查看子列表是否属于alist的一部分是不是更有用的函数? (或看起来像列表的一部分,而不是它必须共享相同的地址?)

注:

啊,好吧,我开始明白,也许这对于列表的eq部分来说是cdr ......有点......“任何cdr - 给定列表eq的衍生物到第一个参数?“。

但也许有人可以解释我在哪种情况下这样的测试非常有用?

4 个答案:

答案 0 :(得分:6)

tailp的基本目的是检查是否共享了列表结构。这意味着 cons cells 是否相同(这意味着EQL作为谓词) - 而不仅仅是缺点单元格的内容。

还可以检查某个项目是否在最后cdr

CL-USER 87 > (tailp t '(1 2 3 4 . t))
T

CL-USER 88 > (tailp nil '(1 2 3 4 . nil))
T

CL-USER 89 > (tailp nil '(1 2 3 4))
T

CL-USER 90 > (tailp #1="e" '(1 2 3 4 . #1#))
T

这是Common Lisp中很少使用的函数之一。

答案 1 :(得分:3)

我非常确定(tailp '(3 4 5) '(1 2 3 4 5))的答案可以是tnil,因为智能编译器可以(tailp '#1=#(3 4 5) '(1 2 . #1#))来减少内存占用。引用的东西是不可变的文字,所以为什么不两次使用相同的内存?

以下是tailp的工作原理:

(defparameter *tail* (list 3 4 5))
(defparameter *larger* (list* 1 2 *tail*))
(defparameter *replica* (copy-list *larger*))

(tailp *tail* *replica*) ; ==> nil
(tailp *tail* *larger*)  ; ==> t

由于copy-list为列表中的每个元素创建了新的缺点,因此它将共享 除了任何其他列表的空列表。它是独一无二的。

*larger*使用*tail*的公共尾部进行制作,因此(tailp *tail* *larger*)将为t

将参数作为相同的对象进行比较非常重要。由于尾部不需要是列表,因此它与eql进行比较。在比较内容是否相同时,您使用equal,因此tailp更具体。它必须是指针等于(eq)或eql原子值。

那么你在哪里使用它?我正在考虑一种功能数据结构,您通常会重用共享结构。 tailp可能用于标识父项的子树。它基本上是一个更高性能的版本:

(defun my-tailp (needle haystack)
  (cond ((eql needle haystack) t)
        ((atom haystack) nil)
        (t (my-tailp needle (cdr haystack)))))

答案 2 :(得分:3)

以下是tailp有用的案例:

(defun circular-list-p (l)
  (and (consp l)
       (tailp l (rest l))))

几个笔记。

这在所有情况下终止:如果第一个参数不是第二个参数的尾部(即没有要求它检查循环性),则允许tailp不在循环列表上终止,但是如果它不需要检查循环,则它必须终止。第一个参数第二个的尾巴。但是,如果列表是循环的,那正是我们在这里检查的,所以它终止了。 (我有一段时间对此感到困惑)。

consp检查结果是(circular-list-p nil)是错误的:我认为这是实用的选择,尽管你可能会认为nil是循环的。

答案 3 :(得分:1)

@Sylwester:

谢谢@Sylwester!

我最近在Edi Weitz的书中读过关于尾部摇摆的书:

(defparameter *list* (list 'a 'b 'c 'd))
(defparameter *tail* (cdddr *list*))

这将列表中最后一个cons单元格的汽车命名为*tail* - 现在,可以添加一个新元素并重命名列表中最后一个cons单元格的新最后一辆汽车{{1 }}。

*tail*

现在列表是:

(setf (cdr *tail*) (cons 'e 'nil) 
      *tail*       (cdr *tail*)) 
;; (E)

并且可以通过*list* ;; (A B C D E) 将其他内容添加到setf,而无需再次遍历列表。 (因此提高了性能。但当然有警告,因为这是一种破坏性行为)。

也许,如果有人像你那样命名一个列表:

*tail*

(defparameter *my-new-tail* '(F G H)) 它到新列表的末尾

tail wagg

啊或者:

(setf (cdr *tail*) *my-new-tail*
      *tail* (cddr *my-new-tail*))

然后

(defparameter *tail-of-my-new-tail* (cddr *my-new-tail*))
;; and then
(setf (cdr *tail*) *my-new-tail*
      *tail*       *tail-of-my-new-tail*)
  • 将是测试,此程序是否正确完成
  • 也是一个测试,我是否添加了(tailp *my-new-tail* *list*) 之外的新尾部,或者*my-new-tail*是尾部摇摆到*my-new-tail*的最后尾部。 ..
  • 因此*list*在尾部摆动的情况下是一个非常有用的测试......
  • 或者像你说的那样,如果一个实现因为性能原因而使用尾部摇摆(并且可能在这个上下文中不断跟踪列表的尾部),那么实现可以使用tailp来测试是否一个给定列表作为最近添加的尾部贡献给另一个列表......

在阅读你的答案时,我想到了这一点,@ Sylwester! (在阅读书中的尾巴摇摆时我没有意识到这一点 - (这是一个非常有用的方式!)谢谢你的答案!