Mapeach

Many times when using mapcar, I find myself using a complex lambda expression for the function argument. This makes the code difficult to read since it breaks apart the flow. My code winds up looking like the following:

(mapcar (lambda (x)
          ...)
        list)

First you have to read the possibly massive lambda expression, then you finally find out what you are mapping over. As the lambda expression increases in length, it becomes harder and harder to read. A way to fix this is with the macro mapeach. Mapeach is a macro which is meant to be used when the lambda expression that would be passed to mapcar is much longer than the expression for the list. Mapeach works just like mapcar, but instead provides an alternative syntax which makes it easier to read when the lambda expression is complicated. Here is one possible implementation of mapeach:1

(defmacro mapeach (var list &body body)
  `(mapcar (lambda (,var) ,@body) ,list))

Mapeach, does two things to fix the problem. First it hides the lambda, making it easier to find the important parts of the code. Second, it inverts the order of the arguments, putting the simple list expression first and the complex body second. As a simple example of mapeach, here is how one could square each element in a list using it:

(mapeach x '(1 2 3)
  (* x x))

If one wanted to write the above code by using mapcar, it would look something like the following:

(mapcar (lambda (x)
          (* x x))
        '(1 2 3))

Although it doesn’t shine for this simple example, you can tell that mapeach makes the code a bit clearer. As the body for the lambda expression gets longer and longer, mapeach begins to make the code much easier to understand. I find that mapcar is nice to use only when the expression for the function is short. This happens either when you are either using a named function or you are using some sort of reader macro. Mapeach is another one of those macros that makes what seems like an insignificant difference. Even so, I find that it aids a lot in readability since it puts all of the simple parts in one place.

  1. An alternative implementation can expand into loop. That way it will automatically support destructuring over the list elements.

4 thoughts on “Mapeach

  1. I am an absolute beginner with Lisp’s Macros. Could you offer a few more details on the “alternative implementation that can expand into loop”? Which form could that be? Thanks!

    1. It would look like this:

      (defmacro mapeach (var list &body body)
        `(loop for ,var in ,list collect (progn ,@body)))
      

      When using for..in, destructuring is done over the variable. For example

        (loop for (x y) in '((1 2) (3 4)) collect (+ x y))
      
        => (3 7)
      

      So now with the loop implementation of mapeach, we could do the following:

        (mapeach (x y) '((1 2) (3 4))
          (+ x y))
      
        => (3 7)
      
  2. I liked the idea of mapeach and got to playing with the idea. I wanted a mapeach that would work with multiple lists like mapcar, that would intern anaphoric parameters in the caller’s package, and that would work with a single, or multiple forms in the body. Its late but it seems to work ok, its just not as simple as I’d like:

    (defmacro imapeach (body &rest lists)
      "Succinct anaphoric mapcar."
      (let ((parameters (loop for i from 1 to (length lists)
                              collect (intern (format nil "IT~A" i))))
            (it-sym (intern "IT")))
        `(symbol-macrolet ((,it-sym ,(first parameters)))
           (mapcar (lambda (,@parameters)
                     (declare (ignorable ,@parameters))
                     ,@(if (listp (car body))
                           body
                           (list body)))
                   ,@lists))))
    
    ;; (mapeach (+ it 5) '(1 2 3)) => (6 7 8)
    ;; (mapeach (+ it1 it2) '(1 2 3) '(7 8 9)) => (8 10 12)
    ;; (mapeach ((setf it1 (+ it2 1)) (- it2 it1)) '(1 2 3) '(7 8 9)) => (-1 -1 -1)
    
  3. Although I like this solution, I typically use flet in these cases. But I could adapt mapeach to be more like Ruby’s each which would be nice too. I'll have to try it out. Thank you!

Leave a Reply

Your email address will not be published. Required fields are marked *