Emulating Python's zip function with variadic input

Hello, first post here, and I'm quite new to Racket. I searched the above question in previous answers but didn't find anything.

What I'm trying to achieve: basically a for/list with a variadic input.

Example:

(define (a '(1 2 3)))
(define (b '("x" "y")))
(define (zip . rest)
    ...)
> ((1 "x") (2 "y"))

But with a variable amount of inputs.

I've tried rolling my own recursive functions that emulate for/list and seeing if map or apply could help, but none seem to work.

I'm sure there's a package out there that can solve my problem, but I'd like to figure this out.

Feel free to correct me on any posting mistakes I may have made

#lang racket

#; {∀ (X ...) [Listof X] ... -> [Listof [Listof X]]}
(define (zip . rest)
  (cond
    [(ormap empty? rest) '()]
    [else (cons (map first rest) (apply zip (map cdr rest)))]))

(module+ test
  (require rackunit)

  (define a '(1 2 3))
  (define b '("x" "y"))
  (define expected '((1 "x") (2 "y")))
  (check-equal? (zip a b) expected))
1 Like

It would be easier to give suggestions if you could share some of your non-working attempts, so we could suggest changes for you to consider.

Since you mentioned apply, one thing I noticed when working out a solution for myself was that the somewhat “magic” way multiple arguments are transformed into a list adds an extra dimension to think about. It might simplify things to break down the problem this way:

(define (zip . rest)
  (zip-list-of-lists rest))
(define (zip-list-of-lists list-of-lists)
  ...)
SPOILER: Here is one solution—but do try working it out yourself. ``` #lang racket (define (zip . lsts) (if (ormap null? lsts) null (cons (map car lsts) (apply zip (map cdr lsts))))) ```
1 Like

It would be easier to give suggestions if you could share some of your non-working attempts, so we could suggest changes for you to consider.

Having come from imperative langugages I started using the for loops when I couldn't figure out the apply with recursion. Here's the non working imperative version. for/list was really close to what I wanted so I figured I could somehow inject what I needed into it.

(define (variadic2 . in)
  (define out empty)
  (for ([z (in-range (length (car in)))])
    (for ([x (in-range (length in))])
      (append (list (list-ref (list-ref in x) z)) out )))
  out)

I deleted my recursive attempts, but will try again.

Thank you for the lead-up bite to think about.

One problem with this code is that Racket's append is very different from Python's append. In Python, x.append(y) mutates list x by inserting the contents of list y at the end, returning None. In Racket, lists are immutable, and (append x y) returns a new list containing the contents of x followed by the contents of y: it is a pure function, with no side-effects. That means your variadic2 will always return the initial out, i.e. empty: the for loops are just dead code.

Everyone in this thread has stumbled over this irritating difference! From an old mailing-list thread, Python's append vs Racket's append and helping novices understand the implications:

Another thing to note is that list-ref is often a sign of poorly structured code, because lists are not random-access: list-ref must traverse from the beginning, taking O(n) time. That's all the more true when used in a pattern like:

(for ([x (in-range (length in))])
  ... (list-ref in x) ...)

which could be written instead as:

(for ([v (in-list in)])
  ... v ...)

It makes your code clearer to directly iterate through the elements of a sequence, rather than accessing them indirectly through an index, and in many cases it will perform better, too.

If you want a data structure that works more like Python lists, take a look at racket/treelist.

1 Like