【问题标题】:A lisp function refinementlisp 函数改进
【发布时间】:2010-12-21 19:09:16
【问题描述】:

我已经完成了 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 (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 关键字参数,与其他序列函数一样,具有相同的默认初始值。length 最多需要调用一次。"

我正在查阅 Common Lisp 教科书和谷歌,但在这方面似乎没有什么帮助:我不知道他所说的“使用 :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 测试”

【问题讨论】:

    标签: lisp common-lisp


    【解决方案1】:

    此类函数的典型参数列表是:

    (defun preceders (item vector 
                      &key (start 0) (end (length vector))
                           (test #'eql))
        ...
    )
    

    如您所见,它具有 START 和 END 参数。

    TEST 是默认的比较函数。使用 (funcall test item (aref vector i))。 往往还有一个KEY参数...

    每次递归调用 PRECEDERS 都会重复调用 LENGTH。

    我会做非递归版本并在向量上移动两个索引:一个用于第一项,一个用于下一项。每当下一个项目是您要查找的项目的 EQL 时,将第一个项目推到结果列表中(如果它不是那里的成员)。

    对于递归版本,我将编写由 PRECEDERS 调用的第二个函数,它接受两个以 0 和 1 开头的索引变量,并使用它。我不会打电话给 POSITION。通常这个函数是通过 PRECEDERS 中的 LABELS 的局部函数,但是为了更容易编写,辅助函数也可以在外部。

    (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
          ...
      ))
    

    这有帮助吗?

    这是使用 LOOP 的迭代版本:

    (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)))
    

    【讨论】:

    • 谢谢 Rainer,对语法很有帮助,在网上很难找到如此精确的序列处理函数定义,非常感谢。我现在正在努力获取我的代码更简单,我会让你们知道我的老师会批评我什么。
    【解决方案2】:

    既然你已经有一个可行的解决方案,我会放大Rainer Joswig's solution,主要是为了制作相关的风格cmets。

    (defun preceders (obj seq &key (start 0) (end (length seq)) (test #'eql))          
      (%preceders obj seq nil start end test))
    

    拥有单独的辅助函数(我称之为%PRECEDERS,表示函数是“私有”的常用约定)的主要原因是消除结果的可选参数。以这种方式使用可选参数通常是可以的,但可选参数和关键字参数一起使用非常糟糕,并且将两者放在一个函数中是一种非常有效的方式来创建各种难以调试的错误。

    将辅助函数设为全局(使用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 函数更改了一些内容以删除重复的先行者。这有可能比重复使用ADJOINPUSHNEW 快得多(即指数级),因为它可以在内部使用散列集表示,至少对于像EQEQLEQUAL

    【讨论】:

    • Pillsy,非常感谢。你的代码片段真的帮助我形成了这个函数的新结构。现在我可以使用 :start :end 和 :test 来构建我的功能。唯一现在我面临的问题是我倾向于使用pushnew来消除过程中可能出现的重复项目。但是,我的老师不喜欢pushnew,他说没有必要使用“Pushnew”或“Setf” " 在任何递归函数中,所以我真的需要想一些其他方法来摆脱重复项。
    • 另外:像 RESULT 这样的内部辅助变量不应该是公共参数列表的一部分,甚至不能作为 &optional 变量。
    • foo* 命名通常用于不同的事物,例如“传播”与“非传播”参数。
    • 也许您想要“ADJOIN”而不是 PUSHNEW?谷歌搜索“clhs adjoin”。
    【解决方案3】:

    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)))
    

    这更多地使用了循环指令,除其他外,只访问向量中的每个元素一次(我们将前一个元素保留在前一个元素变量中)。

    【讨论】:

      【解决方案4】:

      回答您的第一次更新。

      第一个问题:

      看到这个

      (if (foo)
        (bar (+ 1 baz))
        (bar baz))
      

      同理:

      (bar (if (foo)
              (+ 1 baz)
              baz))
      

      或:

      (let ((newbaz (if (foo)
                       (+ 1 baz)
                       baz)))
        (bar newbaz))
      

      第二:

      为什么不从 I = 1 开始?

      另请参阅我的其他答案中的迭代版本...

      【讨论】:

        【解决方案5】:

        Rainer 提出的迭代版本非常好,因为你只遍历序列一次,所以它更紧凑更高效; 与在每次迭代时调用 position 并因此每次遍历子序列的递归版本相比。编辑:对不起,我完全错了关于这最后一句话,请参阅 Rainer 的评论)

        如果需要递归版本,另一种方法是推进start,直到遇到end,沿途收集结果。

        (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)))))
        

        当然这只是loop 版本的另一种表达方式。

        测试:

        [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)
        

        另外,我很感兴趣 Robert,你的老师有没有说他为什么不喜欢在递归算法中使用 adjoinpushnew

        【讨论】:

        • 使用 POSITION 没问题,如果您相应地移动 :START - 尽管矢量元素不会被触摸两次或更多次。在一个练习的解决方案中,我会避​​免 POSITION,因为它是一个相对复杂的库函数(有很多 args 和处理序列),而且它在这里并不是真正需要的。不过,在“真实”代码中,我经常使用 POSITION - 试图确保它不会在相同的元素上运行两次。
        • Rainer,我现在明白了,我没有给予足够的关注。你是对的,使用 POSITION 应该没有问题。谢谢!
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-11-13
        • 2012-06-15
        • 2012-07-20
        • 2017-10-19
        • 2013-12-23
        • 2017-07-27
        相关资源
        最近更新 更多