Common Lisp:Zip任意数量的列表

时间:2018-06-03 21:59:48

标签: functional-programming common-lisp

假设您有一个列表列表,例如'(("abc" "def" "ghi") ("012" "345" "678") ("jkl" "mno" "pqr"))'(("ab" "cd" "ef") ("01" "23" "45"))

在给定列表中压缩列表的规范方法是什么? 即如何定义func以便

(func '(("ab" "cd" "ef") ("01" "23" "45")) :sep "|" :combiner #'concat) 
  ;; => ("ab|01" "cd|23" "ef|45")

(func '(("abc" "def" "ghi") ("012" "345" "678") ("jkl" "mno" "pqr")) ...)
  ;; => ("abc|012|jkl" "def|345|mno" "ghi|678|pqr")

其中concat := (lambda (args) ...)是组合各个列表的头部的函数。

据推测,这种类型的操作称为 rotation zipMany (根据answers针对不同语言的相关问题)。

我有类似的东西(双 - apply

(apply #'mapcar #'(lambda (&rest args) (apply #'concatenate 'string args)) lst)

其中lst'(("ab" "cd" "ef") ("01" "23" "45"))  例如。 combiner将是concatenate。请注意,此示例实现中未给出分隔符。

但这似乎非常令人费解。

那么,这种操作的规范实现是什么

6 个答案:

答案 0 :(得分:5)

对于任意数量的列表,您需要删除apply #'mapcar,否则列表的数量将受call-arguments-limit的限制。

一种典型的方法是使用reduce代替,一次合并两个列表:

(defun zip (list-of-lists &key (combiner #'concat))
  (reduce (lambda (list-a list-b)
            (mapcar combiner list-a list-b))
          list-of-lists))

如果您不喜欢明确的lambda表单,可能需要curry,e。 G。来自alexandria

(defun zip (list-of-lists &key (combiner #'concat))
  (reduce (curry #'mapcar combiner)
          list-of-lists))

其他循环结构是{​​{1}},loopdo,还有一些循环库,例如: G。 dolistiterate

答案 1 :(得分:4)

<强>加入

(defun %d (stream &rest args)
  "internal function, writing the dynamic value of the variable
DELIM to the output STREAM. To be called from inside JOIN."
  (declare (ignore args)
           (special delim))
  (princ delim stream))

(defun join (list delim)
  "creates a string, with the elements of list printed and each
element separated by DELIM"
  (declare (special delim))
  (format nil "~{~a~^~/%d/~:*~}" list))

<强>目标

请勿使用&rest args或apply - 这会限制我们的列表长度。

递归MAPCARS

(defun mapcars (f l)
  (if (null (car l))
      '()
    (cons (funcall f (mapcar #'car l))
          (mapcars f (mapcar #'cdr l)))))

迭代MAPCARS

(defun mapcars (f l)
  (loop for l1 = l then (mapcar #'cdr l1)
        while (car l1)
        collect (funcall f (mapcar #'car l1))))

<强>用法

CL-USER 10 > (mapcars (lambda (l) (join l "|")) l)
("abc|012|jkl" "def|345|mno" "ghi|678|pqr")

答案 2 :(得分:3)

您的功能有效,但依赖于APPLY。 您传递给apply的列表数量受CALL-ARGUMENTS-LIMIT限制,在给定的实施中可能足够大。但是,您的函数应该接受任意数量的列表,因此,如果您想要移植代码,则不应在您的情况下使用apply

邮编

zip函数组合了列表列表中的所有第一个元素,然后是所有第二个元素等。 如果您的输入列表列表如下:

'((a b c d)
  (0 1 2 3)
  (x y z t))

然后zip将构建以下列表:

(list (combine (list a 0 x))
      (combine (list b 1 y))
      (combine (list c 2 z))
      (combine (list d 3 t)))

仔细观察,您可以看到您可以将combine功能的映射分割为初始列表的转置列表(以矩阵形式查看):

 ((a b c d)            ((a 0 x) 
  (0 1 2 3)    ===>     (b 1 y) 
  (x y z t))            (c 2 z) 
                        (d 3 t))

因此,zip可以定义为:

(defun zip (combine lists)
  (mapcar #'combine (transpose lists)))

或者,您可以使用MAP-INTO,如果您关心重用调用transpose时分配的内存:

(defun zip (combiner lists &aux (transposed (transpose lists)))
  (map-into transposed combiner transposed))

移调

有不同的方法来转置列表列表。在这里,我将使用REDUCE,在其他一些函数语言中称为 fold 。在每个缩小步骤中,中间缩减函数将取部分结果和列表,并产生新结果。我们的结果也是一份清单。 下面,我将显示每个缩减步骤的当前列表,以及生成的列表列表。

第一步

(a b c d)  =>  ((a)
                (b)
                (c)
                (d))

第二步

(0 1 2 3)  =>  ((0 a)
                (1 b)
                (2 c)
                (3 d))

第三步

(x y z t)  =>  ((x 0 a)
                (y 1 b)
                (z 2 c)
                (t 3 d))

请注意,元素会被推到每个列表的前面,这就解释了为什么每个结果列表都被反转为w.r.t.期望的结果。 因此,transpose函数应在执行简化后反转每个列表:

(defun transpose (lists)
  (mapcar #'reverse
          (reduce (lambda (result list)
                    (if result
                        (mapcar #'cons list result)
                        (mapcar #'list list)))
                  lists
                  :initial-value nil)))

与以前一样,最好避免分配太多内存。

(defun transpose (lists)
  (let ((reduced (reduce (lambda (result list)
                           (if result
                               (mapcar #'cons list result)
                               (mapcar #'list list)))
                         lists
                         :initial-value nil)))
    (map-into reduced #'nreverse reduced)))

实施例

(transpose '(("ab" "cd" "ef") ("01" "23" "45")))
=> (("ab" "01") ("cd" "23") ("ef" "45"))

(import 'alexandria:curry)

(zip (curry #'join-string "|")
     '(("ab" "cd" "ef") ("01" "23" "45")))
=> ("ab|01" "cd|23" "ef|45")

编辑:其他答案已经提供了join-string的示例定义。您还可以使用cl-strings中的join功能。

答案 3 :(得分:2)

最初,我尝试使用backquote并拼接,@并定义一个宏来摆脱apply。但是正如@coredump所指出的那样,这还有其他问题。

无论如何,如果要在连接元素之间添加分隔符,则需要Pythonic函数join()作为组合器。如果函数应按照您希望的方式运行,concatenate会使事情变得更复杂。

由于我使用了@Sylwester非常优雅的join()定义,我的答案与他的非常相似。也许我的回答最接近你原来的例子。

(defun join (l &key (sep ", "))
  (format nil (format nil "~a~a~a" "~{~a~^" sep "~}") l))

使用此功能,我们可以将您的func定义为:

(defun func (lists &key (sep "|") (combiner #'join))
  (apply #'mapcar
         #'(lambda (&rest args) (funcall combiner args :sep sep))
         lists))

或者没有apply - @Rainer Joswig如何指出 - 以及所有以前的回答者 - 因为它对参数的数量有限制(~50):

(defun func (lists &key (sep "|") (combiner #'join))
  (reduce #'(lambda (l1 l2)
              (mapcar #'(lambda (e1 e2) 
                          (funcall combiner (list e1 e2) :sep sep)) l1 l2))
          lists))

或略短:

(defun func (lists &key (sep "|") (combiner #'join))
  (reduce #'(lambda (l1 l2)
              (mapcar #'(lambda (&rest l) (funcall combiner l :sep sep)) l1 l2))
          lists))

测试:

(func '(("abc" "def" "ghi") ("012" "345" "678") ("jkl" "mno" "pqr"))) 
;; ("abc|012|jkl" "def|345|mno" "ghi|678|pqr")

请注意:sep ""使join等效于concatenate函数。

(func '(("abc" "def" "ghi") ("012" "345" "678") ("jkl" "mno" "pqr")) :sep "")
;; ("abc012jkl" "def345mno" "ghi678pqr")

感谢@Sylwester,@ coredump和@Rainer Joswig!

答案 4 :(得分:2)

怎么样:

(defun unzip (lists &key (combiner #'list))
  (apply #'mapcar combiner lists))

然后你应该join

(defun join (l &key (sep ", "))
  (format nil (format nil "~a~a~a" "~{~a~^" sep "~}") l))

然后为您的特定用途制作包装或lambda:

(unzip '(("abc" "def" "ghi") ("012" "345" "678") ("jkl" "mno" "pqr"))
       :combiner (lambda (&rest l) (join l :sep "|")))
; ==> ("abc|012|jkl" "def|345|mno" "ghi|678|pqr")

答案 5 :(得分:1)

为什么不在params中使用&amp; rest并在lambda函数上返回它。

CL-USER> (mapcar (lambda (&rest l) l) '(1 2 3) '(a b c) '("cat" "duck" "fish"))
((1 A "cat") (2 B "duck") (3 C "fish")) 

然后你可以使用像这样的应用:

(apply #'mapcar (lambda (&rest l) l) '(("ab" "cd" "ef") ("01" "23" "45"))) 
; => (("ab" "01") ("cd" "23") ("ef" "45"))

直到这里,是普通的zip会做什么,现在你想把这个子列表加入到一个字符串中,并带有一些自定义分隔符。

为此最好的答案是使用格式,也像这样answer 来自stackoverflow https://stackoverflow.com/a/41091118/1900722

(defun %d (stream &rest args)
  "internal function, writing the dynamic value of the variable
DELIM to the output STREAM. To be called from inside JOIN."
  (declare (ignore args)
           (special delim))
  (princ delim stream))

(defun join (list delim)
  "creates a string, with the elements of list printed and each
element separated by DELIM"
  (declare (special delim))
  (format nil "~{~a~^~/%d/~:*~}" list))

然后你只需要在这个函数和zip

的结果列表中再次使用mapcar
(mapcar (lambda (lst) (join lst "|")) '(("ab" "01") ("cd" "23") ("ef" "45")))
 ; => ("ab|01" "cd|23" "ef|45")