lisp函数的精化
我已经完成了Graham Common Lisp第5章练习5,它需要一个函数,该函数接受一个对象X和一个向量V,并返回一个在V中紧跟X之前的所有对象的列表 它的工作原理如下:lisp函数的精化,lisp,common-lisp,Lisp,Common Lisp,我已经完成了Graham Common Lisp第5章练习5,它需要一个函数,该函数接受一个对象X和一个向量V,并返回一个在V中紧跟X之前的所有对象的列表 它的工作原理如下: > (preceders #\a "abracadabra") (#\c #\d #r) 我已经完成了递归版本: (defun preceders (obj vec &optional (result nil) &key (startt 0)) (let ((l (leng
> (preceders #\a "abracadabra")
(#\c #\d #r)
我已经完成了递归版本:
(defun preceders (obj vec &optional (result nil) &key (startt 0))
(let ((l (length vec)))
(cond ((null (position obj vec :start startt :end l)) result)
((= (position obj vec :start startt :end l) 0)
(preceders obj vec result
:startt (1+ (position obj vec :start startt :end l))))
((> (position obj vec :start startt :end l) 0)
(cons (elt vec (1- (position obj vec :start startt :end l)))
(preceders obj vec result
:startt (1+ (position obj vec
:start startt
:end l))))))))
这是正确的,但我的老师给了我以下的批评:
“这会反复调用长度。向量还不错,但仍然没有必要。更高效、更灵活(对于用户而言)代码将像其他序列处理函数一样对此进行定义。使用:start和:end关键字参数,就像其他序列函数一样,使用相同的默认初始值。长度最多需要调用一次。”
我正在查阅常见的Lisp教科书和google,但在这方面似乎没有什么帮助:我不知道他所说的“使用:start和:end关键字参数”是什么意思,我也不知道如何“只调用一次长度”。如果你们能告诉我如何改进我的代码以满足老师发布的要求,我将不胜感激
更新:
现在我想出了以下代码:
(defun preceders (obj vec
&optional (result nil)
&key (start 0) (end (length vec)) (test #'eql))
(let ((pos (position obj vec :start start :end end :test test)))
(cond ((null pos) result)
((zerop pos) (preceders obj vec result
:start (1+ pos) :end end :test test))
(t (preceders obj vec (cons (elt vec (1- pos)) result)
:start (1+ pos) :end end :test test)))))
我得到这样的批评:
当一个复杂的递归调用在多个分支中重复时,通常更简单的做法是先调用,将其保存在局部变量中,然后在更简单的IF或COND中使用该变量
另外,对于我的函数迭代版本:
(defun preceders (obj vec)
(do ((i 0 (1+ i))
(r nil (if (and (eql (aref vec i) obj)
(> i 0))
(cons (aref vec (1- i)) r)
r)))
((eql i (length vec)) (reverse r))))
我得到了批评
“在更好的点启动DO并删除重复的>0测试”此类功能的典型参数列表如下:
(defun preceders (item vector
&key (start 0) (end (length vector))
(test #'eql))
...
)
如您所见,它有开始和结束参数
TEST是默认的比较函数。使用(funcall测试项目(aref向量i))。
通常还有一个关键参数
每次递归调用前导,都会重复调用LENGTH
我将执行非递归版本,并在向量上移动两个索引:一个用于第一项,另一个用于下一项。每当下一个项目是您要查找的项目的EQL时,将第一个项目推送到结果列表(如果它不是那里的成员)
对于递归版本,我将编写第二个由前导调用的函数,该函数使用两个从0和1开始的索引变量,并使用它们。我不会叫这个位置。通常,此函数是通过前置词内部的标签实现的局部函数,但为了更易于编写,辅助函数也可以在外部
(defun preceders (item vector
&key (start 0) (end (length vector))
(test #'eql))
(preceders-aux item vector start end test start (1+ start) nil))
(defun preceders-aux (item vector start end test pos0 pos1 result)
(if (>= pos1 end)
result
...
))
这有用吗
以下是使用循环的迭代版本:
(defun preceders (item vector
&key (start 0) (end (length vector))
(test #'eql))
(let ((result nil))
(loop for i from (1+ start) below end
when (funcall test item (aref vector i))
do (pushnew (aref vector (1- i)) result))
(nreverse result)))
既然你已经有了一个有效的解决方案,我将详细介绍,主要是发表相关的风格评论
(defun preceders (obj seq &key (start 0) (end (length seq)) (test #'eql))
(%preceders obj seq nil start end test))
使用单独的helper函数(我称之为%prefers
,这是一种表示函数为“private”的常用约定)的主要原因是为了消除结果的可选参数。一般来说,使用可选参数是可以的,但是可选参数和关键字参数一起使用非常糟糕,在一个函数中同时使用这两种参数是创建各种难以调试的错误的非常有效的方法
将helper函数设置为全局(使用DEFUN
)还是本地(使用LABELS
)是一个品味问题。我更喜欢全局性的,因为这意味着更少的缩进和更容易的交互调试。YMMV
助手功能的一种可能实现方式是:
(defun %preceders (obj seq result start end test)
(let ((pos (position obj seq :start start :end end :test test)))
;; Use a local binding for POS, to make it clear that you want the
;; same thing every time, and to cache the result of a potentially
;; expensive operation.
(cond ((null pos) (delete-duplicates (nreverse result) :test test))
((zerop pos) (%preceders obj seq result (1+ pos) end test))
;; I like ZEROP better than (= 0 ...). YMMV.
(t (%preceders obj seq
(cons (elt seq (1- pos)) result)
;; The other little bit of work to make things
;; tail-recursive.
(1+ pos) end test)))))
此外,在所有这些之后,我想我应该指出,我也同意Rainer的建议,使用显式循环而不是递归来实现这一点,前提是递归地实现它不是练习的一部分
编辑:我切换到更常见的辅助函数“%”约定。通常,您使用的任何约定都只是增加了这样一个事实,即您只显式导出构成公共接口的函数,但是一些标准函数和宏使用尾随的“*”来表示变体功能
我使用标准的
delete-DUPLICATES
函数来删除重复的前置词。这可能比重复使用ADJOIN
或PUSHNEW
要快得多(即指数),因为它可以在内部使用散列集表示,至少对于像EQ
、EQL
和EQUAL
这样的常见测试函数是这样的 回答您的第一次更新
第一个问题:
看到这个了吗
(if (foo)
(bar (+ 1 baz))
(bar baz))
这与:
(bar (if (foo)
(+ 1 baz)
baz))
或:
第二:
为什么不从I=1开始呢
另请参见我的另一个答案中的迭代版本…Rainer提出的迭代版本非常好,它非常紧凑,效率更高,因为您只需遍历序列一次;与递归版本不同,递归版本在每次迭代时调用
位置
,因此每次都遍历子序列。(编辑:对不起,最后一句话我完全错了,请看雷纳的评论)
如果需要递归版本,另一种方法是推进开始
,直到它满足结束
,同时收集结果
(defun precede (obj vec &key (start 0) (end (length vec)) (test #'eql))
(if (or (null vec) (< end 2)) nil
(%precede-recur obj vec start end test '())))
(defun %precede-recur (obj vec start end test result)
(let ((next (1+ start)))
(if (= next end) (nreverse result)
(let ((newresult (if (funcall test obj (aref vec next))
(adjoin (aref vec start) result)
result)))
(%precede-recur obj vec next end test newresult)))))
还有,我很感兴趣,罗伯特,你的老师有没有说为什么他不喜欢在递归算法中使用
邻接
或推新
?一个稍微修改过的Rainer循环版本:
(defun preceders (item vector
&key (start 0) (end (length vector))
(test #'eql))
(delete-duplicates
(loop
for index from (1+ start) below end
for element = (aref vector index)
and previous-element = (aref vector (1- index)) then element
when (funcall test item element)
collect previous-element)))
这将更多地使用循环指令,除其他外,只访问向量中的每个元素一次(我们将前一个元素保留在前一个元素变量中)。感谢Rainer,这是语法上非常有用的一点,在web上很难找到序列处理函数的精确定义,非常感谢。我现在正在努力使我的代码变得更简单,我会让你们知道我的老师会对我的批评。Pillsy,非常感谢。你的代码片段真的帮助我形成了这个函数的新结构。现在我可以使用:start:end和:test来构建我的函数。唯一的
[49]> (precede #\a "abracadabra")
(#\r #\c #\d)
[50]> (precede #\a "this is a long sentence that contains more characters")
(#\Space #\h #\t #\r)
[51]> (precede #\s "this is a long sentence that contains more characters")
(#\i #\Space #\n #\r)
(defun preceders (item vector
&key (start 0) (end (length vector))
(test #'eql))
(delete-duplicates
(loop
for index from (1+ start) below end
for element = (aref vector index)
and previous-element = (aref vector (1- index)) then element
when (funcall test item element)
collect previous-element)))