这段Common Lisp代码发生了什么?

10
我编写了以下代码来模拟投掷一个六面骰子多次,并计算每个面出现的次数:
(defun dice (num)
  (let ((myList '(0 0 0 0 0 0)))
    (progn (format t "~a" myList)
           (loop for i from 1 to num do
                 (let ((myRand (random 6)))
                   (setf (nth myRand myList) (+ 1 (nth myRand myList)))))
           (format t "~a" myList))))

这个函数第一次调用时运行得很好,但在随后的调用中,变量myList从上次调用结束时的值开始,而不是像我以为的那样使用let语句重新初始化为全部为零的值。为什么会发生这种情况?


1
顺便提一下,现在大多数写Common Lisp的人都使用像my-list这样的名称,而不是myList。此外,我认为你不需要那个progn,因为“let的主体是一个隐式的progn”http://www.lispworks.com/documentation/HyperSpec/Body/s_let_l.htm(就像progn已经存在一样)。 - Samuel Edwin Ward
3个回答

13

这是在初始化器中使用常量列表的结果:

(let ((myList '(0 0 0 0 0 0)))

将那行代码改为:

(let ((myList (list 0 0 0 0 0 0)))

代码中的第一行只会分配一次内存(因为它是一个常量列表),但是通过调用list,您强制每次进入函数时进行分配。

编辑: 这可能对后面部分有帮助。Successful Lisp

这个问题的答案也可能有所帮助。

这里使用了loop关键字collecting,它将每次迭代的结果收集到一个列表中,并将该列表作为loop的值返回。


谢谢Andrew和Michael。这解决了我的问题。我是Lisp的新手(这可能很明显),我会查看你们提供的链接。 - Dustin
没问题,Lisp是一门非常有趣的语言 :) 正如Michael所指出的,SBCL非常有帮助,它非常积极地警告可能不安全的操作。 - asm
1
通常情况下,如果您打算通过位置重复索引而不是频繁增长数组,则使用数组而不是列表是一个好习惯。 - Vatine
这是一个很好的观点,@Dustin,“nth”是一个线性时间操作,因为它必须遍历列表到所选索引。 - asm
最后一个例子完全不同...它只返回一个包含n个0-5随机数的列表,而不是每个六个面出现次数的计数器列表。 - 6502

8

SBCL会告诉你错在哪里:

* (defun dice (num)
  (let ((myList '(0 0 0 0 0 0)))
    (progn (format t "~a" myList)
           (loop for i from 1 to num do
                 (let ((myRand (random 6)))
                   (setf (nth myRand myList) (+ 1 (nth myRand myList)))))
           (format t "~a" myList))))
; in: DEFUN DICE
;     (SETF (NTH MYRAND MYLIST) (+ 1 (NTH MYRAND MYLIST)))
; ==>
;   (SB-KERNEL:%SETNTH MYRAND MYLIST (+ 1 (NTH MYRAND MYLIST)))
; 
; caught WARNING:
;   Destructive function SB-KERNEL:%SETNTH called on constant data.
;   See also:
;     The ANSI Standard, Special Operator QUOTE
;     The ANSI Standard, Section 3.2.2.3
; 
; compilation unit finished
;   caught 1 WARNING condition

DICE

因此,本质上来说: 不要在常量数据上调用破坏性函数(这里是setf)。

-1

像上面的帖子一样,编译器将0分配为常量空间。我曾经知道一些技巧,其中之一是将其制作为宏,如下:

`',(list 0 0 0 0 0)
=>
 ?? (I forget and don't have the other machine on to check)

或者包裹在(eval-when (compile)) ... )

同样也

 (list 0 0 0 0 0) 
=>
  #.(list 0 0 0 0)

我不知道这个是否仍然有效(或曾经有效过)。还有一些实现宏或编译器宏可以帮助保持分配大小恒定,但数据变量不同。我记不清了。

记得使用填充(例如C语言中的bzero)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接