Hofeach

Last time I talked about mapeach, a macro which is a simple wrapper around mapcar. After using mapeach a couple times, I found that I wanted ‘each’ version of many other other functions, removefind, and count to name a few. One option I had was to write a macro for every single one of these functions. If I were to have done this, I would have wound up with ‘remove-each’, ‘find-each’, and so on. Instead I took door number two, creating a general macro which I call ‘hofeach’Hofeach, is just like mapeach, except it takes an extra argument for the HOF (higher order function), that you want to use. Below is one possible implementation of hofeach.

(defmacro hofeach (hof var list &body body)
  `(funcall ,hof (lambda (,var) ,@body) ,list))

Here is what code that uses hofeach as a fill in for mapeach looks like:

(hofeach #'mapcar x '(1 2 3)
  (* x x))

=> (1 4 9)

Now we get to specify which HOF we want to use! If we want to keep all of the numbers in a list that are even, here is how we could do that:1 2

(hofeach #'remove-if-not x '(1.2 5 7 2 3.5 6 9)
  (and (integerp x) (evenp x)))

=> (2 6)

So now that I have hofeach, I generally will use it instead of passing a complex lambda expression to a HOF. Most of the time I use hofeach with remove-if-not, but I have also used it with count-if as well. It gives code a nice down and to the right look, which I find pretty easy to read. You get to read the forms in the order that they appear. If you were to use a lambda expression instead, it becomes much more difficult to read since you have to jump around in order to read the code.

  1. Remove-if-not is a pretty bad name. Removing all of the elements that do not satisfy some property can be thought of as keeping all of the ones that do. So instead of being called ‘remove-if-not’, it should be called ‘keep-if’.
  2. We need to use integerp since evenp signals an error when called with a non-integer value.

10 thoughts on “Hofeach

  1. I used to love writing macros like this. It does clarify some code snippets, but it comes at the cost of being harder to read for other programmers. This hurts when others have to read your code and look up your macro to understand exactly what you are doing. You are creating a personal dialect, which has disadvantages as well as advantages.

    My personal way of dealing with this issue is to create a local function immediately around the mapping function with flet. It allows me to use a self-documenting name instead of lambda, and is completely standard lisp.

    e.g.

    (flet ((meaningful-name (ele) big-complicated-form))
       (mapcar #'meaningful-name list))
    
    1. I think this is mostly a stylistic issue. If you are working with a team where everyone is familiar with them, I believe utility macros such as hofeach become a win all around. If you are working with programmers who are unfamiliar with the macros you are using (i.e. open source software) they can wind up being a net negative. Of course, with macros as simple as hofeach, it only takes several seconds to macroexpand it or read the source code to figure out what they are doing.

  2. An alternative approach would be to write a macro that expands to an -each macro, rather than being a general macro for any high-order function. That way, you can give the -each macro any name you want.

    In my view, syntax extension like this is a powerful tool that can easily be abused, but Ruku’s general argument against them would also be an argument against creating named functions: you don’t know what they do until you look (at least at the documentation if not the code).

    As for remove-if-not, it’s a good function with a bad name: a double negative, really. In Scheme and Haskell it’s called filter: (filter evenp? '(1 2 3 4 5)) evaluates to (2 4)., which is clear and clean.

  3. Improving on the macro I posted for mapeach, but of course, yet more complex. Nice posts, good discussion.

    (defmacro ahofeach (hof body &rest lists)
      "Succinct anaphoric HOF iteration."
      (flet ((wrap-functor-body (it-sym parameters body)
               `(lambda (,@parameters)
                  (declare (ignorable ,@parameters))
                  (symbol-macrolet ((,it-sym ,(first parameters)))
                    ,@body))))
        (let* ((parameters (loop for i from 1 to (length lists)
                                 collect (intern (format nil "IT~A" i))))
               (it-sym (intern "IT"))
               (functor (cond
                          ((and (listp body)
                                (or (and (= (length body) 2) ;; #'func, 'func
                                         (member (first body) '(function quote)))
                                    (and (>= (length body) 2) ;; (lambda nil …)
                                         (eq (first body) 'lambda))))
                           body)
                          ((and (listp body) (listp (first body))) ;; multiple forms
                           (wrap-functor-body it-sym parameters body))
                          (t ;; assume single form
                           (wrap-functor-body it-sym parameters (list body))))))
          `(funcall ,hof ,functor ,@lists))))
    
    ;; (let ((list '(1 2 3 4)))
    ;;   (list (ahofeach #'remove-if-not #'evenp list)
    ;;         (ahofeach 'remove-if-not 'evenp list)
    ;;         (ahofeach #'remove-if-not (evenp it) list)
    ;;         (ahofeach #'remove-if-not ((format t "…~A" it)
    ;;                                    (let ((x (multiple-value-list (floor it 2))))
    ;;                                      (zerop (second x))))
    ;;                   list)))
    ;; output => …1…2…3…4
    ;; value  => ((2 4) (2 4) (2 4) (2 4))
    
  4. Why the funcall?

    (defmacro hofeach (hof var list &body body)
      `(,hof (lambda (,var) ,@body) ,list))
    

    This allows to skip the #’ before the function name and lambda expressions can be used, too, as long as they’re not prefixed with #’.

    1. With macros that wind up calling a function, it is a style issue as whether to call it directly or to use funcall. I personally always use funcall because it allows for the possibility of an expression to determine which function to use. For example:

      (hofeach (if (...) 
                   #'remove-if-not 
                   #'remove-if)
        ...)
      
        1. That works except in cases where you want to pass in the value of a variable. It implicitly assumes that a symbol corresponds to the function of the same name and not to the variable.

Leave a Reply

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