通过谓词将列表过滤为两部分

时间:2013-08-08 02:25:17

标签: filter lisp common-lisp

我想做

(filter-list-into-two-parts #'evenp '(1 2 3 4 5))
; => ((2 4) (1 3 5))

其中列表被分成两个子列表,具体取决于谓词的计算结果是否为true。定义这样的函数很容易:

(defun filter-list-into-two-parts (predicate list)
  (list (remove-if-not predicate list) (remove-if predicate list)))

但是我想知道Lisp中是否有内置函数可以做到这一点,或者更好的方法是编写这个函数?

4 个答案:

答案 0 :(得分:6)

我认为没有内置版本,你的版本不是最理想的,因为它遍历列表两次并在每个列表元素上调用谓词两次。

(defun filter-list-into-two-parts (predicate list)
  (loop for x in list
    if (funcall predicate x) collect x into yes
    else collect x into no
    finally (return (values yes no))))

我返回两个值而不是其列表;这是更惯用的(您将使用multiple-value-bind从返回的多个值中提取yesno,而不是使用destructuring-bind来解析列表,它会减少和更快)。

更通用的版本是

(defun split-list (key list &key (test 'eql))
  (let ((ht (make-hash-table :test test)))
    (dolist (x list ht)
      (push x (gethash (funcall key x) ht '())))))
(split-list (lambda (x) (mod x 3)) (loop for i from 0 to 9 collect i))
==> #S(HASH-TABLE :TEST FASTHASH-EQL (2 . (8 5 2)) (1 . (7 4 1)) (0 . (9 6 3 0)))

答案 1 :(得分:5)

使用REDUCE

(reduce (lambda (a b)
          (if (evenp a)
              (push a (first b))
            (push a (second b)))
          b)
        '(1 2 3 4 5)
        :initial-value (list nil nil)
        :from-end t)

答案 2 :(得分:2)

dash.el中,有一个函数-separate完全符合您的要求:

(-separate 'evenp '(1 2 3 4)) ; => '((2 4) (1 3))

如果您使用-separate,则可以忽略帖子的其余部分。我必须在partition中实现Haskell的Elisp函数。 Elisp在很多方面与Common Lisp类似 1 ,所以这个答案对两种语言的编码器都很有用。我的代码受到similar implementations for Python

的启发
(defun partition-push (p xs)
  (let (trues falses) ; initialized to nil, nil = '()
    (mapc (lambda (x) ; like mapcar but for side-effects only
            (if (funcall p x)
                (push x trues)
              (push x falses)))
          xs)
    (list (reverse trues) (reverse falses))))

(defun partition-append (p xs)
  (reduce (lambda (r x)
            (if (funcall p x)
                (list (append (car r) (list x))
                      (cadr r))
              (list (car r)
                    (append (cadr r) (list x)))))
          xs
          :initial-value '(() ()) ; (list nil nil)
          ))

(defun partition-reduce-reverse (p xs)
  (mapcar #'reverse ; reverse both lists
          (reduce (lambda (r x)
                    (if (funcall p x)
                        (list (cons x (car r))
                              (cadr r))
                      (list (car r)
                            (cons x (cadr r)))))
                  xs
                  :initial-value '(() ())
                  )))

push是一个破坏性的函数,它预先列出一个元素。我没有使用Elisp' add-to-list,因为它只添加了一次相同的元素。 mapc是一个地图函数 2 ,它不会累积结果。由于Elisp与Common Lisp一样,具有函数和变量 3 的单独命名空间,因此必须使用funcall来调用作为参数接收的函数。 reduce是一个高阶函数 4 ,它接受:initial-value关键字,允许多种用途。 append连接可变数量的列表。

在代码partition-push中,使用广泛的"push and reverse"惯用法的命令式Common Lisp,首先通过在O(1)中的列表前面并在O(n)中反转来生成列表。由于列表实现为cons cells,因此在列表中追加一次将为O(n),因此追加n项将为O(n²)partition-append说明了添加到最后。由于我是functional programming粉丝,我在reduce中使用partition-reduce-reverse编写了没有副作用的版本。

Emacs有profiling tool。我针对这3个函数运行它。返回的列表中的第一个元素是总秒数。正如您所看到的,附加到列表的工作速度非常慢,而功能变体是最快的。

ELISP> (benchmark-run 100 (-separate #'evenp (number-sequence 0 1000)))
(0.043594004 0 0.0)
ELISP> (benchmark-run 100 (partition-push #'evenp (number-sequence 0 1000)))
(0.468053176 7 0.2956386049999793)
ELISP> (benchmark-run 100 (partition-append #'evenp (number-sequence 0 1000)))
(7.412973128 162 6.853687342999947)
ELISP> (benchmark-run 100 (partition-reduce-reverse #'evenp (number-sequence 0 1000)))
(0.217411618 3 0.12750035599998455)

参考

  1. Differences between Common Lisp and Emacs Lisp
  2. Map higher-order function
  3. Technical Issues of Separation in Function Cells and Value Cells
  4. Fold higher-order function

答案 3 :(得分:1)

我认为普通的lisp标准中没有分区功能,但有libraries提供了这样的实用程序(带有文档和source)。

CL-USER> (ql:quickload :arnesi)
CL-USER> (arnesi:partition '(1 2 3 4 5) 'evenp 'oddp)
((2 4) (1 3 5))
CL-USER> (arnesi:partition '(1 2 b "c") 'numberp 'symbolp 'stringp)
((1 2) (B) ("c"))