Paul Graham在他的书On Lisp中强调说Lisp是“可扩展语言”。他说,这意味着逐步建立更高的语言界面,朝着可以有效地讨论或分析应用程序的语言发展。这样就产生了一种正交语言……您可以通过以多种不同方式组合少量运算符来表达多种语言。作为实验,我想尝试扩展一个更有用的序列函数之一,即remove
。
暂时不考虑涉及非序列数据类型的扩展(例如从数组,哈希表或prop列表中删除元素),仍然有扩展关键字选择的空间。例如,没有内置的规定可根据元素的索引从序列中除去元素。遵循这些原则,程序员可能希望使用类似(lambda (elt idx) (= elt idx))
的测试来删除其值与其索引相同的元素。非扩展方法是简单地滚动自己的迭代函数(在其他一百个难以记忆的实用程序中再添加一个),但是利用内置函数和方法似乎确实更简洁,可重用和高效。扩展它们。
直接的问题是,remove
仅在存在给定的搜索项时适用,并且remove-if
要求谓词仅使用一个元素作为参数(而不是带有索引的元素)。我想探索的方法是尝试将不同的选项合并到一个remove-sequence
函数中,在该函数中,序列是唯一必需的参数,而其他所有参数都根据所需的特定移除类型定制。因此,在:item关键字中指定了搜索项,并且根据需要,一个或两个参数boolean:test可以同时包含元素和索引。在后一种情况下,一个简单的调用可能看起来像(remove-sequence '(3 1 2 4) :test (lambda (elt idx) (= x i)))
,删除了第三个元素。
我从一个似乎可以在SBCL中的以下示例上使用的功能开始:
(require :sb-introspect)
(defun remove-sequence (sequence &key item (test #'eql) from-end (start 0)
(end (length sequence)) (count (length sequence)) (key #'identity))
(cond (item
(remove item sequence :test test :from-end from-end
:start start :end end :count count :key key))
((= (length (sb-introspect:function-lambda-list test)) 1)
(remove-if test sequence :from-end from-end
:start start :end end :count count :key key))
((= (length (sb-introspect:function-lambda-list test)) 2)
(let* ((delta (if from-end -1 +1))
(initial-index (if from-end (length sequence) -1))
(closure (let ((index initial-index))
(lambda (element)
(setf index (+ index delta))
;(print (list element index))
(funcall test element index)))))
(remove-if closure sequence :from-end from-end
:start start :end end :count count :key key)))
(t (error "in remove-sequence macro"))))
(remove-sequence '(1 2 4 1 3 4 5) :test #'> :item 3) => (4 3 4 5)
(remove-sequence '(1 2 3 4 5 6) :test #'evenp :count 2 :from-end t) => (1 2 3 5)
(remove-sequence '(3 1 2 4) :test #'(lambda (elt idx) (= elt idx))) => (3 1 4)
但是,我在将其转换为宏时遇到了麻烦,到目前为止,该宏如下所示。 (在宏扩展过程中会产生错误。)
(defmacro remove-sequence (sequence &key item test from-end start end count key)
(let ((tst (when test `(:test ,test)))
(frm-nd (when from-end `(:from-end ,from-end)))
(strt (when start `(:start ,start)))
(nd (when end `(:end ,end)))
(cnt (when count `(:count ,count)))
(ky (when key `(:key ,key)))
(test-fn (if test test #'eql)))
(cond (`,item
`(remove ,item ,sequence ,@tst ,@frm-nd ,@strt ,@nd ,@cnt ,@ky))
((= (length (sb-introspect:function-lambda-list test-fn)) 1)
`(remove-if ,test-fn ,sequence ,@frm-nd ,@strt ,@nd ,@cnt ,@ky))
((= (length (sb-introspect:function-lambda-list test-fn)) 2)
(let* ((delta (if `,from-end -1 +1))
(initial-index (if `,from-end (length `,sequence) -1))
(closure (let ((index initial-index))
(lambda (element)
(setf index (+ index delta))
;(print (list element index))
(funcall test-fn element index)))))
`(remove-if ,closure ,sequence ,@frm-nd ,@strt ,@nd ,@cnt ,@ky)))
(t (error "in remove-sequence macro")))))
这可以解决吗?还是有更好的书写方式?而且,更普遍地说,添加大约十二个关键字是否有不利之处?例如,我至少想为:duplicates和:destructive添加布尔关键字,而其他关键字可能与非序列参数有关。感谢您提供任何有经验的见解。
答案 0 :(得分:1)
这是您的功能签名:
(sequence &key
item
(test #'eql)
from-end
(start 0)
(end (length sequence))
(count (length sequence))
(key #'identity))
为许多不同的操作提供高级接口是有好处的,但是您还必须注意性能。在上面的代码中,每次调用函数时,都会两次调用(length sequence)
。如果该函数仅打算与向量一起使用,那很好,但是对于列表,您要进行两个线性遍历。就算法复杂度而言,这并不是问题,因为在最坏的情况下,移除在时间和空间上都是线性的。但是就运行时间而言,在很多情况下不会发生最坏的情况,但是在这种情况下,您的实现会花费太多时间。
在标准REMOVE
函数中,:END
的默认值为nil,在这里(序列的末尾)具有特殊含义,而无需实际计算索引。处理列表的功能可以利用该信息,而不必遍历整个列表。例如,当count为1时,应该大致执行以下操作:
(defun remove-first (item list)
(if (endp list)
nil
(if (equalp (first list) item)
(rest list)
(cons (first list)
(remove-first item (rest list))))))
实际上,您可以期望代码不依赖尾递归消除:
(defun remove-first (item list)
(loop
with stack = nil
for (head . tail) on list
do (if (equalp head item)
(return
(dolist (e stack tail)
(push e tail)))
(push head stack))))
您甚至可以使用无限列表:
USER> (setf *print-circle* t)
T
USER> (remove-first 3 '#1=(1 2 3 4 5 6 . #1#))
(1 2 . #1=(4 5 6 1 2 3 . #1#))
因此,可以得出结论,Common Lisp中非常令人高兴的一件事是,更高级别的标准功能/抽象具有可预测的,毫无意外的资源使用情况。即使没有这样指定,我也希望非玩具实现中的map
不会由于递归调用等导致大列表上的堆栈溢出。当库导出后面的函数和/或宏时,这很好同样的方法。那可能是改善现有代码的一种方法。
答案 1 :(得分:0)
正如Rainer Joswig上面评论的那样,宏:test参数是一个列表,可以理解为函数指定符,而不是函数对象。在将其传递给sb-introspect:function-lambda-list之前将其转换为函数应该可以修复该错误。经验丰富的人也许可以评论symbol-function
和coerce
是否涵盖所有可能的情况:
(defmacro remove-sequence (sequence &key item (test '(function eql)) from-end start end count (key '(function identity)))
(let ((tst (when test `(:test ,test)))
(frm-nd (when from-end `(:from-end ,from-end)))
(strt (when start `(:start ,start)))
(nd (when end `(:end ,end)))
(cnt (when count `(:count ,count)))
(ky (when key `(:key ,key)))
(test-fn (cond ((symbolp (second test)) (symbol-function (second test)))
((eq (first test) 'lambda) (coerce test 'function))
((eq (first test) 'function) (coerce (second test) 'function))
(t (error "Malformed :test function ~A" test))))
(key-fn (cond ((symbolp (second key)) (symbol-function (second key)))
((eq (first key) 'lambda) (coerce key'function))
((eq (first key) 'function) (coerce (second key) 'function))
(t (error "Malformed :key function ~A" key)))))
(cond (`,item
`(remove ,item ,sequence ,@tst ,@frm-nd ,@strt ,@nd ,@cnt ,@ky))
((= 1 (length (sb-introspect:function-lambda-list test-fn)))
`(remove-if ,test ,sequence ,@frm-nd ,@strt ,@nd ,@cnt ,@ky))
(t (let* ((delta (if `,from-end -1 +1))
(initial-index (if `,from-end (length `,sequence) -1))
(closure (let ((index initial-index))
(lambda (element)
(setf index (+ index delta))
;(print (list element index))
(funcall test-fn (funcall key-fn element) index)))))
`(remove-if ,closure ,sequence ,@frm-nd ,@strt ,@nd ,@cnt ,@ky))))))
请注意,在下一个草案中仍存在变量捕获问题。
另一个问题涉及sb-introspect:function-lambda-list返回的arg列表的长度。如果:test函数只有一个参数,则remove-if
是正确的扩展名。如果它具有两个或多个参数,则扩展为remove
(如果还有一个:item关键字),或者为remove-if
(带闭包)(否则)。无需检查两个参数。实际上,许多可接受的:test函数的lambda列表长于2(例如#'>)。
* (remove-sequence '(1 2 4 1 3 4 5) :test #'> :item 3)
(4 3 4 5)
* (remove-sequence '(1 2 3 4 5 6) :test #'evenp :count 2 :from-end t)
(1 2 3 5)
* (remove-sequence '(3 1 2 4) :test (lambda (elt idx) (= elt idx)))
(3 4)
* (defun element=index (elt idx)
(= elt idx)) ELEMENT=INDEX
* (remove-sequence '(3 1 2 4) :test 'element=index)
(3 4)
*