mapcan在常见的lisp中是否会改变参数的值?

时间:2017-11-23 09:34:51

标签: 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))

有人可以教我吗? 感谢。

2 个答案:

答案 0 :(得分:6)

是的,MAPCAN可以更改其参数的值。为了解其中的原因,我将尝试解释它对它有用的内容,以及为实现此目的而进行的优化。 (注意:我假设你理解修改引用对象的问题:我的例子通过新鲜处理所有内容来避免这些问题。)

首先,考虑MAPCAR:这样做是将函数映射到列表上,构造一个新列表,其中每个元素都是应用于原始列表的相应元素的函数的结果。如果这是你想要做的事情那就太棒了:但是如果你想要为原始列表中的每个元素生成几个元素的结果,那会怎么样呢? '数'可能意味着没有#39;例如,您可能希望编写一些过滤列表的函数,以仅生成数字的元素。

嗯,一种自然的方法是期望您映射的函数会生成结果的列表,然后映射函数将获取这些列表并将它们附加在一起。这就是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)

(很明显,这个函数很容易推广到创建一般过滤器。)

其次,这是一个函数,它接受一个关联列表并返回一个属性列表,通过返回原始alist中每个cons的两元素列表:

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

> (plistify '((a . 1) (b . 2)))
(a 1 b 2)

所以这一切都很容易理解。

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

所以你可以做的一个技巧:你可以用MAPCAN破坏性地修改映射函数给你的列表,而不是构造一个新的结果列表。这意味着NCONC只包含被映射的函数conses( less 而不是MAPCAN conses!)。

这是一个绝妙的技巧,但它有一个缺点:映射函数返回的列表结构被破坏性地修改,因此如果该结构不是新鲜的,那么引用它的任何其他东西也会被破坏性地修改。因此,在您的示例中,您正在映射MAPCAR,并且它返回与原始列表的部分共享的结构。而且这些部分也在进行修改。所以你可以得到相当惊人的结果:

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)) CDR映射到它上面,返回结果和(修改的)原始列表。结果相当令人惊讶!如果您要求系统显示共享结构,这会有所帮助:

MAPCAN

嗯:在获得漂亮的输出方面没有帮助,但是你可以看到正在进行的所有共享:这里的内容并不像它看起来那么多。< / p>

所以> (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#)) 在两种情况下很棒:

  • 如果您确定您返回的列表是新鲜的;
  • 如果你明白它的作用,不要使用新的结构,并以某种方式利用副作用。

我不认为我做了第二件事,但我打赌其他人也有。

我很确定,如果Lisp今天被发明(当然,它是),MAPCAN要么不存在,要么存在于某个低级库中:而是在那里&#39; d是一个像Alexandria's MAPPEND这样的函数,如评论中所述。但我认为,实际上,MAPCAN有其用途。

为了获得额外的乐趣,请尝试预测这两个看似相似的电话的结果。您需要将MAPCAN*PRINT-LENGTH*之一或两者绑定到阻止系统运行的值(并知道如何中断Lisp)。

示例1:

*PRINT-CIRCLE*

示例2:

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

我完全不确定这些示例中的任何一个的行为都是明确定义的。

答案 1 :(得分:2)

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

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

(b) nil (2 3)

但这不是新结构。例如,形成(b)的cons细胞与(ab)中的第二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。

PS。还要注意引用列表上的破坏性操作。对于我的实验,我使用了:

(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)