踩坑分享: 小心 mapcan 的副作用

长话短说,一个局部变量在经过某个函数的执行之后被莫名其妙修改了,而这个变量并没有作为参数传递到这个函数中。

经过很长时间的排查(真的很长时间😭),真是个大坑,,,我发现这个函数中使用了 mapcan ,而它是有副作用的。我之前一直以为它是使用的 append 来连接列表的,看文档发现使用的是 nconc,副作用的罪魁祸首就是它。

也只能怪自己想当然了,使用时没有仔细看文档哈哈哈。把 mapcan 换为 seq-mapcat 就好了。和大家分享这个踩坑的经历,共勉!

mapcan is a built-in function in ‘C source code’.

(mapcan FUNCTION SEQUENCE)

Apply FUNCTION to each element of SEQUENCE, and concatenate
the results by altering them (using 'nconc’).
SEQUENCE may be a list, a vector, a bool-vector, or a string.

虽然解决了,我还是没理解为啥 mapcan 要使用一个有副作用的 函数来连接列表。因为我觉得 mapcan 就是 mapcar 的一种变体:将每个元素执行函数后返回的列表连接起来。因此只关心返回的结果,不应该引入副作用。当然这是想当然的想法,因此我想了解需要引入副作用的原因是什么,有什么场景它会很有用?

以此类推,大家写代码的时候也要关心那些有副作用的函数哈哈,虽然是函数式语言

不知道大家有没有类似踩坑的经历~~

4 个赞

这还真是emacs的坑,mapcan在cl里面分出来mapcanmapcon,但是在古老的maclisp里面它最早就用的nconc,我也不知道为啥。

我估计是为了性能。比较明显的是,如果用cons那么每次都要先拷贝filter的结果,然后才能接,如果用nconc就不需要考虑了。但是如果这样为什么不让用户自己去选择cons or nconc呢?我怀疑在早期机器上使用nconc是有性能问题的。

鉴于早期lisp都是手搓的,我们有理由怀疑当时的nconc做不了动态寄存器分配。这导致执行filter会无差别产生写入,即使结果是nil。nconc往cdr上追加,所以需要把上一次filter写入的结果重新读取到寄存器里,修改cdr后再写回去。

可能的优化是,我们不去做写后读,而是直接把储存上一次结果的寄存器拿过来用,因为空列表可以用位操作直接判断出来所以就可以回避写入,这对稀疏矩阵之类的结构来说大利好。

2 个赞

能否举个具体的例子?

(setq tmp '((1 2 3) (4 5) (6 7 8)))
((1 2 3) (4 5) (6 7 8))

(mapcan (lambda (z) z) tmp)
(1 2 3 4 5 6 7 8)

tmp
((1 2 3 4 5 6 7 8) (4 5 6 7 8) (6 7 8))
1 个赞