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

Mas*_*uki 2 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))
Run Code Online (Sandbox Code Playgroud)

任何人都可以教我吗?谢谢.

tfb*_*tfb 6

是的,MAPCAN可以更改其参数的值.要知道为什么会这样,我会尝试解释它对什么有用,以及为实现这一点而进行的优化.(注意:我假设你理解修改引用对象的问题:我的例子通过新鲜处理所有内容来避免这些问题.)

首先,考虑一下MAPCAR:这样做是将函数映射到列表上,构造一个新的列表,其中每个元素都是应用于原始列表的相应元素的函数的结果.如果这就是你想做的事情那就太好了:但是如果你想要为原始列表中的每个元素生成一个包含多个元素的结果,那么"几个"可能意味着"无".例如,您可能希望编写一些过滤列表的函数,以仅生成数字的元素.

好吧,一种自然的方法是期望你映射的函数会产生一个结果列表,然后映射函数将获取这些列表并将它们附加在一起.这是做什么的MAPCAN.以下是两个如何"正确"使用它的示例.首先,这是一个过滤数字条目列表的函数:

(defun numbers-of (l)
  (mapcan (lambda (e)
            (if (numberp e) (list e)
              '()))
          l))
Run Code Online (Sandbox Code Playgroud)

现在

> (numbers-of '(1 2 3 4 a () b (1) 9))
(1 2 3 4 9)
Run Code Online (Sandbox Code Playgroud)

(很明显,这个函数可以很容易地推广到一般滤波器.)

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

(defun plistify (alist)
  (mapcan (lambda (e)
            (list (car e) (cdr e)))
          alist))
Run Code Online (Sandbox Code Playgroud)

> (plistify '((a . 1) (b . 2)))
(a 1 b 2)
Run Code Online (Sandbox Code Playgroud)

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

但是有一件事要注意:在这两个函数中,被映射的函数返回的列表结构的位是完全短暂的:它在生活中的唯一目的是告诉MAPCAN结果列表中你想要多少元素.还记得它是1960年:你想要运行它的机器每秒可以执行几千条指令并且有几千字的内存.垃圾收集意味着你可以去喝一杯茶:短暂的消费是完全自由的概念,现在这不是真的,当然不是真的.

所以你可以做一个技巧:不是构建一个新的结果列表,而是破坏性地修改NCONC映射函数给你的列表.这意味着MAPCANconses之外不超过函数的更多被映射conses之外(这是MAPCARconses之外!).

这是一个绝妙的技巧,但它有一个缺点:映射函数返回的列表结构被破坏性地修改,所以如果那个结构不新鲜,那么引用它的任何其他东西也会被破坏性地修改.所以在你的例子中,你是映射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))
Run Code Online (Sandbox Code Playgroud)

这构造了一个列表,其中包含两个元素的十个(不同的)子列表,然后CDR在其上进行映射MAPCAN,返回结果和(已修改的)原始列表.结果相当令人惊讶!如果您要求系统显示共享结构,这会有所帮助:

> (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#))
Run Code Online (Sandbox Code Playgroud)

好吧:它在"获得漂亮的输出"方面没有帮助,但是你可以看到正在进行的所有共享:这里没有那么多的结果.

所以,MAPCAN是在两种情况下极大:

  • 如果你确定你返回的清单是新鲜的;
  • 如果你了解它的作用,不要采用新的结构,并以某种方式利用副作用.

我不认为我做了第二件事,但我打赌别人也有.

我很相信,如果Lisp今天被发明(当然,它是),MAPCAN要么不存在,要么存在于某个低级库中:相反,有一个像亚历山大的MAPPEND功能,如提到的那样评论.但我认为,事实上,MAPCAN它有其用途.


为了获得额外的乐趣,请尝试预测这两个看似相似的呼叫的结果.您将希望绑定其中一个或两个*PRINT-LENGTH*以及*PRINT-CIRCLE*阻止系统运行的值(并知道如何中断您的Lisp).

例1:

(let* ((e (list 1 1))
       (l (list e e)))
  (mapcan #'cdr l))
Run Code Online (Sandbox Code Playgroud)

例2:

(let* ((e (list 1 1))
       (l (list e e e)))
  (mapcan #'cdr l))
Run Code Online (Sandbox Code Playgroud)

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