【问题标题】:Filter a list into two parts by a predicate通过谓词将列表过滤成两部分
【发布时间】:2013-08-09 15:07:28
【问题描述】:

我想做

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

根据谓词的计算结果是否为真,一个列表被分成两个子列表。定义这样的函数很容易:

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

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

【问题讨论】:

    标签: filter lisp common-lisp


    【解决方案1】:

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

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

    【讨论】:

    • 啊,谢谢,从来不知道你可以返回两个值!与返回列表相比有什么优势?似乎提取多个值会更麻烦。
    • 当有一个主要的返回值和在大多数情况下会被丢弃的附加值时,多个值很有用。除法相关的数学函数都以这种方式工作,将商作为主要值返回,余数作为次要值返回。当很难确定哪个返回值是“主要的”时,我发现最好以列表或自我描述的结构返回值。调用站点需要使用笨拙的机制处理多个值,而列表可以很容易地传递。
    • 模式匹配减少了返回多个值的需要,因为您可以很好地解构对。
    【解决方案2】:

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

    【讨论】:

      【解决方案3】:

      dash.el 中有一个函数-separate 完全按照您的要求执行:

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

      如果您使用-separate,则可以忽略帖子的其余部分。我必须在Elisp 中实现Haskell 的partition 函数。 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 是一个接受:initial-value 关键字的高阶函数4,它允许多种用途。 append 连接可变数量的列表。

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

      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

      【讨论】:

        【解决方案4】:

        我认为通用 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"))
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2018-04-08
          • 1970-01-01
          • 1970-01-01
          • 2022-11-26
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多