我已经编写了两个版本的lisp函数。两者之间的主要区别在于,一种是通过递归完成的,另一种是通过迭代完成的。
这是递归版本(无副作用!):
(defun simple-check (counter list)
"This function takes two arguments:
the number 0 and a list of atoms.
It returns the number of times the
atom 'a' appears in that list."
(if (null list)
counter
(if (equal (car list) 'a)
(simple-check (+ counter 1) (cdr list))
(simple-check counter (cdr list)))))
以下是迭代版本(带有副作用):
(defun a-check (counter list)
"This function takes two arguments:
the number 0 and a list of atoms.
It returns the number of times the
atom 'a' appears in that list."
(dolist (item list)
(if (equal item 'a)
(setf counter (+ counter 1))
(setf counter (+ counter 0))))
counter)
据我所知,它们都起作用。但是我真的很想避免迭代版本的副作用。我想回答两个问题:
答案 0 :(得分:3)
为完整起见,请注意Common Lisp具有内置的COUNT
:
(count 'a list)
答案 1 :(得分:2)
在某些方面,副作用或没有副作用之间的差异有些模糊。采用以下loop
版本(忽略loop
也有更好的方法):
(loop :for x :in list
:for counter := (if (eq x 'a) (1+ counter) counter)
:finally (return counter))
是counter
设置了,还是反弹?即,是否修改了现有变量(如在setf
中),还是创建了新的变量绑定(如在递归中)?
此do
版本非常类似于递归版本:
(do ((list args (rest list))
(counter 0 (+ counter (if (eq (first list) 'a) 1 0))))
((endp list) counter))
与上述相同。
现在是“显而易见的” loop
版本:
(loop :for x :in list
:count (eq x 'a))
甚至没有用于计数器的显式变量。有副作用吗?
在内部,当然会产生影响:创建环境,建立绑定,尤其是在进行尾部调用优化的情况下,甚至在每个步骤都已销毁/替换的递归版本中。
我认为只有副作用会影响超出某些定义范围的事物。当然,如果您还可以在内部定义的级别上避免显式设置事物,而是使用一些更具声明性的表达式,则事物看起来会更加优雅。
答案 2 :(得分:2)
您还可以与map
,mapcar
和朋友进行迭代。
https://lispcookbook.github.io/cl-cookbook/iteration.html
我还建议您看看remove-if[-not]
以及其他reduce
和apply
:
(length (remove-if-not (lambda (x) (equal :a x)) '(:a :b :a))) ;; 2
答案 3 :(得分:1)
将计数器传递给递归过程是启用尾递归定义的一种方法。对于迭代定义,这是不必要的。 正如其他人指出的那样,有几种语言构造可以很好地解决所述问题。
我认为您从更一般的意义上对此感兴趣,例如当您找不到 一种直接解决问题的语言功能。 通常,可以通过将突变保持私有状态来维护功能接口,如下所示:
(defun simple-check (list)
"return the number of times the symbol `a` appears in `list`"
(let ((times 0))
(dolist (elem list times)
(when (equal elem 'a)
(incf times)))))