Python enumerate() analog in Common Lisp
Asked Answered
V

4

7

I want to map over a list, but keeping track of the element index in the list.

In Python I can do something along the lines of:

map(lambda (idx, elt): "Elt {0}: {1}".format(idx, elt), enumerate(mylist))

I was trying to translate it to something along the lines of:

(mapcar-something (lambda (elt idx) (format nil "Elt ~D: ~S" idx elt))
                  '(a b c d))

Expected result:

("Elt 0: A" "Elt 1: B" "Elt 2: C" "Elt 3: D")

But I can't find the mapcar-something function that I should use. Do I need to implement that myself (via loop, maybe)?

Virus answered 10/5, 2016 at 12:59 Comment(1)
You can wrap it in a let block and capture an index variable from the lambda, then increment that each time the lambda is called. I don't think there's a builtin.Mirepoix
V
5

Common Lisp's LOOP macro can automatically keep track of the index.

(loop for elt in '(a b c d) and idx from 0
      collect (operation-on idx elt))

LOOP automatically increases by 1 a variable initialized with from; and if they are introduced by and then both assignations (the element of the array and the index) happen at once, not nested.

Thus, an enumerate function would be along the lines of:

(defun enumerate (list &optional (first-index 0))
  (loop for elt in list and idx from first-index
    collect (cons idx elt)))

In the expressiveness of Common Lisp, it would possibly be useful to define a macro along the lines of:

(defmacro with-enumerated-list ((list elt idx &key (first-index 0)) &body body)
  `(loop for ,elt in ,list and ,idx from ,first-index
     collect (progn ,@body)))

In which case, the enumerate function could be reduced to:

(defun enumerate (list &optional (first-index 0))
  (with-enumerated-list (list elt idx :first-index first-index)
    (cons idx elt)))
Virus answered 18/5, 2016 at 13:24 Comment(5)
Typical problem: you haven't tried your own code. The second ENUMERATE function does not work. There is no need for a macro, your macro's interface is ugly, it does no error checking and it provides no additional value over the plain LOOP. Worse: the way you define the body opens up lots of problems (can you see why)? By introducing the macro, you have introduced one bug and another source for random bugs...Luciferase
@RainerJoswig is it so bad an answer that it's more worth to downvote than to try to improve it? i did try my own code, but it seems I had copied a bad iteration of it. The value it provides is the language-specific idiom, which, arguably, may not mean much. If your reasoning on why the macro is ugly relies on the lack of hygiene on it, that can be both good and bad. Why is your so strongly negative opinion?Virus
The LOOP syntax leaks into the WITH-ENUMERATED-LIST. Do you see that? If not, I'll show you how in my next comment.Luciferase
So, can you explain why this does not work: (with-enumerated-list ('(1 2 3 4 5) it count) (print it) (print count) count)Luciferase
@RainerJoswig I see, thank you for the "bug report", I have just edited the answer. And arguably we might as well want to parametrize collect (so that we may just want to do things) to even be a keyword argument, as first-index, but I think that's a more valid follow-up as a gist at GitHub than as an addition here. Is there anything else that is wrong with the answer?Virus
L
9
CL-USER 25 > (defun iota (n)
               (loop for i below n collect i))
IOTA

CL-USER 26 > (iota 4)
(0 1 2 3)

CL-USER 27 > (mapcar (lambda (elt idx)
                       (format nil "Elt ~D: ~S" idx elt))
                     '(a b c d)
                     (iota 4))
("Elt 0: A" "Elt 1: B" "Elt 2: C" "Elt 3: D")

or

CL-USER 28 > (loop for elt in '(a b c d) and idx from 0
                   collect (format nil "Elt ~D: ~S" idx elt))
("Elt 0: A" "Elt 1: B" "Elt 2: C" "Elt 3: D")
Luciferase answered 10/5, 2016 at 13:18 Comment(1)
I had no idea that and idx from 0 would automatically increment idx by one each time.Virus
V
5

Common Lisp's LOOP macro can automatically keep track of the index.

(loop for elt in '(a b c d) and idx from 0
      collect (operation-on idx elt))

LOOP automatically increases by 1 a variable initialized with from; and if they are introduced by and then both assignations (the element of the array and the index) happen at once, not nested.

Thus, an enumerate function would be along the lines of:

(defun enumerate (list &optional (first-index 0))
  (loop for elt in list and idx from first-index
    collect (cons idx elt)))

In the expressiveness of Common Lisp, it would possibly be useful to define a macro along the lines of:

(defmacro with-enumerated-list ((list elt idx &key (first-index 0)) &body body)
  `(loop for ,elt in ,list and ,idx from ,first-index
     collect (progn ,@body)))

In which case, the enumerate function could be reduced to:

(defun enumerate (list &optional (first-index 0))
  (with-enumerated-list (list elt idx :first-index first-index)
    (cons idx elt)))
Virus answered 18/5, 2016 at 13:24 Comment(5)
Typical problem: you haven't tried your own code. The second ENUMERATE function does not work. There is no need for a macro, your macro's interface is ugly, it does no error checking and it provides no additional value over the plain LOOP. Worse: the way you define the body opens up lots of problems (can you see why)? By introducing the macro, you have introduced one bug and another source for random bugs...Luciferase
@RainerJoswig is it so bad an answer that it's more worth to downvote than to try to improve it? i did try my own code, but it seems I had copied a bad iteration of it. The value it provides is the language-specific idiom, which, arguably, may not mean much. If your reasoning on why the macro is ugly relies on the lack of hygiene on it, that can be both good and bad. Why is your so strongly negative opinion?Virus
The LOOP syntax leaks into the WITH-ENUMERATED-LIST. Do you see that? If not, I'll show you how in my next comment.Luciferase
So, can you explain why this does not work: (with-enumerated-list ('(1 2 3 4 5) it count) (print it) (print count) count)Luciferase
@RainerJoswig I see, thank you for the "bug report", I have just edited the answer. And arguably we might as well want to parametrize collect (so that we may just want to do things) to even be a keyword argument, as first-index, but I think that's a more valid follow-up as a gist at GitHub than as an addition here. Is there anything else that is wrong with the answer?Virus
B
2

Here's another one:

(defun enumerate (collection &key (as 'list))
  (let ((index -1))
    (map as
      (lambda (element)
        (cons (incf index) element))
      collection)))

This one has the advantage that it works for and can result in both lists and vectors:

CL-USER[2]: (enumerate '(1 2 3))
((0 . 1) (1 . 2) (2 . 3))

CL-USER[3]: (enumerate "abc" :as 'vector)
#((0 . #\a) (1 . #\b) (2 . #\c))
Banner answered 26/8, 2018 at 17:49 Comment(0)
P
1

If you want something that looks like your original example:

(defun enumerate (function list)
  (let ((idx 0))
    (loop for elt in list
      collect (funcall function elt idx)
      do (incf idx))))

Your example:

(enumerate (lambda (elt idx) (format nil "Elt ~D: ~S" idx elt))
           '(a b c d))
=> ("Elt 0: A" "Elt 1: B" "Elt 2: C" "Elt 3: D")
Parrett answered 18/5, 2016 at 2:36 Comment(1)
It's fine, but Rainer's second code example improves upon yours by handling the incf to the loop implementation, thus hiding it from the programmer, which is closer to the use in Python.Virus

© 2022 - 2024 — McMap. All rights reserved.