common lisp中的mapcan是否更改参数的值?

common lisp中的mapcan是否更改参数的值?,lisp,common-lisp,Lisp,Common Lisp,我不知道发生了什么事 (setf x '((a b) (c) (1 2 3))) x ;;=> ((A B) (C) (1 2 3)) (mapcan #'cdr x) ;;=> (B 2 3) x ;;=> ((A B 2 3) (C) (1 2 3)) 有人能教我吗? 谢谢。是的,MAPCAN可以更改其参数的值。为了了解这是为什么,我将尝试解释它的有用之处,以及为实现这一点所做的优化。(注意:我假设您了解修改引用对象的问题:我的示例通过重新考虑所有内容来避免这些问题。)

我不知道发生了什么事

(setf x '((a b) (c) (1 2 3)))
x
;;=> ((A B) (C) (1 2 3))

(mapcan #'cdr x)
;;=> (B 2 3)
x
;;=> ((A B 2 3) (C) (1 2 3))
有人能教我吗?
谢谢。

是的,
MAPCAN
可以更改其参数的值。为了了解这是为什么,我将尝试解释它的有用之处,以及为实现这一点所做的优化。(注意:我假设您了解修改引用对象的问题:我的示例通过重新考虑所有内容来避免这些问题。)

首先,考虑<代码> MAPCAR < /代码>:这是将一个函数映射到一个列表上,构造一个新的列表,其中每个元素是应用于原始列表的相应元素的函数的结果。如果这正是您想要做的,那就太好了:但是如果您想要,例如,为原始列表中的每个元素生成一个包含多个元素的结果,其中“几个”可能意味着“无”。例如,您可能希望编写一些函数来过滤列表,以便只生成列表中的数字元素

一种自然的方法是,你映射的函数会产生一个结果列表,然后映射函数会把这些列表附加在一起。这就是
MAPCAN
所做的。这里有两个例子说明如何“正确”使用它。首先,这里有一个函数,用于过滤列表中的数字条目:

(defun numbers-of (l)
  (mapcan (lambda (e)
            (if (numberp e) (list e)
              '()))
          l))
现在呢

> (numbers-of '(1 2 3 4 a () b (1) 9))
(1 2 3 4 9)
(显然,可以很容易地将此函数推广到创建通用过滤器。)

其次,这里有一个函数,它获取一个关联列表并返回一个属性列表,方法是为原始列表中的每个cons返回一个两元素列表:

(defun plistify (alist)
  (mapcan (lambda (e)
            (list (car e) (cdr e)))
          alist))

所以这些都很容易理解

但这里有一点需要注意:在这两个函数中,被映射函数返回的列表结构位是完全短暂的:它在生命中的唯一目的是告诉
MAPCAN
在结果列表中需要多少元素。还要记住,它是1960年的:你想要运行它的机器每秒可以执行几千条指令,并且有几千个字的内存。垃圾收集意味着你可以去泡杯茶:短暂的思考是免费的,这在现在不是真的,在那时肯定不是真的

因此,您可以使用一个技巧:不构建新的结果列表,而是使用
ncoc
破坏性地修改映射函数提供给您的列表。这意味着
MAPCAN
conses不超过正在映射的函数conses(小于
MAPCAR
conses!)

这是一个绝妙的技巧,但它有一个缺点:映射函数返回的列表结构被破坏性修改,因此如果该结构不新鲜,那么引用它的任何其他内容也会被破坏性修改。因此,在您的示例中,您映射的是
CDR
,它返回的结构与原始列表的部分共享。这些部分也正在被修改。因此,您可以得到非常令人惊讶的结果:

> (let ((a (loop repeat 10 collect (list 1 1))))
    (values (mapcan #'cdr a) a))
(1 1 1 1 1 1 1 1 1 1)
((1 1 1 1 1 1 1 1 1 1 1)
 (1 1 1 1 1 1 1 1 1 1)
 (1 1 1 1 1 1 1 1 1)
 (1 1 1 1 1 1 1 1)
 (1 1 1 1 1 1 1)
 (1 1 1 1 1 1)
 (1 1 1 1 1)
 (1 1 1 1)
 (1 1 1)
 (1 1))
这将构造一个包含两个元素的十个(不同)子列表的列表,然后使用
MAPCAN
CDR
映射到该列表上,返回结果和(修改的)原始列表。结果相当令人惊讶!如果您要求系统向您显示共享结构,则会有所帮助:

> (let ((*print-circle* t)
        (a (loop repeat 10 collect (list 1 1))))
    (pprint (mapcan #'cdr a))
    (pprint a))

(1 1 1 1 1 1 1 1 1 1)
((1
  1
  . #1=(1
        . #2=(1
              . #3=(1
                    . #4=(1
                          . #5=(1
                                . #6=(1 . #7=(1 . #8=(1 . #9=(1))))))))))
 (1 . #1#)
 (1 . #2#)
 (1 . #3#)
 (1 . #4#)
 (1 . #5#)
 (1 . #6#)
 (1 . #7#)
 (1 . #8#)
 (1 . #9#))
嗯:这对“获得漂亮的输出”没有帮助,但是你可以看到所有正在进行的共享:这里没有看起来那么多的conse

因此,
MAPCAN
在两种情况下都很好:

  • 如果您确保您返回的列表是全新的
  • 如果你了解它的作用,就不要反对新的结构,并以某种方式利用它的副作用
我不认为我做了第二件事,但我打赌其他人做了

我相当确信,如果Lisp是今天发明的(当然是这样),
MAPCAN
要么不存在,要么存在于某个低级库中:相反,会有一个函数,如注释中所述。但我认为,事实上,
MAPCAN
有它的用途


为了增加乐趣,试着预测这两个显然相似的电话的结果。您需要将
*PRINT-LENGTH*
*PRINT-CIRCLE*
中的一个或两个绑定到阻止系统运行的值(并知道如何中断Lisp)

例1:

(let* ((e (list 1 1))
       (l (list e e)))
  (mapcan #'cdr l))
例2:

(let* ((e (list 1 1))
       (l (list e e e)))
  (mapcan #'cdr l))
我完全不确定这两个示例的行为是否定义良好。

Mapcan是“破坏性的”,将破坏性的“NCOC”应用于中间映射结果

如果我们将其分解,首先使用CDR进行映射会产生:

(b) nil (2 3)
但这并不是新的结构。例如,cons单元形成(b)与(a)和(b)中的第二cons单元相同。如果你无法想象第二个cons单元格,我们可以避开Lisp语法糖,这样写:

(a . (b . nil))
回到我们的中间结果…:

(b . nil) nil (2 . (3 . nil))
…当我们要求mapcan将这些组合起来,它最终将(b.nil)的cdr更改为(2.(3.nil)),它也在替换(a.(b.nil))的cdr,因为它是相同的cdr!吓人,对吧?这就是为什么人们在使用破坏性操作时需要考虑一下(以及为什么我们称之为“破坏性”)

现在,为了好玩,请尝试以下方法:

(setf (caddar x) 42)
现在计算x

另外,还要注意引用列表上的破坏性操作。在我的实验中,我使用了:

(defparameter abc (copy-list '((a b) (c) (1 2 3))))
(defparameter ab (mapcan #'cdr abc))
(print ab)
(print abc)
(setf (caddar abc) 42)
(print abc)

MAPCAN
使用
ncoc
以破坏性方式连接结果。该库有一个非破坏性的备选方案
MAPPEND
(defparameter abc (copy-list '((a b) (c) (1 2 3))))
(defparameter ab (mapcan #'cdr abc))
(print ab)
(print abc)
(setf (caddar abc) 42)
(print abc)