用少量的运算符表达很多东西

时间:2019-05-31 23:39:18

标签: macros common-lisp

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添加布尔关键字,而其他关键字可能与非序列参数有关。感谢您提供任何有经验的见解。

2 个答案:

答案 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-functioncoerce是否涵盖所有可能的情况:

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