Your function works, but relies on APPLY
.
The number of lists you can pass to apply
is limited by CALL-ARGUMENTS-LIMIT
, which may be large enough in a given implementation. Your function, however, is supposed to accept an arbitrary number of lists, and so, if you want to code portably, you shouldn't use apply
in your case.
Zip
The zip
function combines all first elements, then all second elements, etc. from a list of lists.
If your input list of lists is the following:
'((a b c d)
(0 1 2 3)
(x y z t))
Then zip will build the following list:
(list (combine (list a 0 x))
(combine (list b 1 y))
(combine (list c 2 z))
(combine (list d 3 t)))
If you look closely, you can see that you can split the work being done as mapping of the combine
function over the transpose of your initial list of lists (viewed as a matrix):
((a b c d) ((a 0 x)
(0 1 2 3) ===> (b 1 y)
(x y z t)) (c 2 z)
(d 3 t))
And so, zip
can be defined as:
(defun zip (combine lists)
(mapcar #'combine (transpose lists)))
Alternatively, you could use MAP-INTO
, if you care about reusing the memory that is allocated when calling transpose
:
(defun zip (combiner lists &aux (transposed (transpose lists)))
(map-into transposed combiner transposed))
Transpose
There are different ways to transpose a list of lists. Here I am going to use REDUCE
, which is known as fold in some other functional languages. At each step of reduction, the intermediate reducing function will take a partial result and a list, and produce a new result. Our result is also a list of lists.
Below, I show the current list at each step of reduction, and the resulting list of lists.
First step
(a b c d) => ((a)
(b)
(c)
(d))
Second step
(0 1 2 3) => ((0 a)
(1 b)
(2 c)
(3 d))
Third step
(x y z t) => ((x 0 a)
(y 1 b)
(z 2 c)
(t 3 d))
Note that elements are pushed in front of each lists, which explains why each resulting list is reversed w.r.t. the expect result.
The transpose
function should thus reverse each list after performing reduction:
(defun transpose (lists)
(mapcar #'reverse
(reduce (lambda (result list)
(if result
(mapcar #'cons list result)
(mapcar #'list list)))
lists
:initial-value nil)))
Like previously, it might be preferable to avoid allocating too much memory.
(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)))
Example
(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")
Edit: the other answers already provide example definitions for join-string
. You could also use the join
function from cl-strings.