Search code examples
macroslispcommon-lispcode-generation

Generating arbitrarily-parameterized functions in a loop


I'm trying to create a bunch of cookie-cutter functions and stick 'em in a hash. So far, I've got a macro that expands into such a function:

(defmacro make-canned-format-macro (template field-names)
  `(function (lambda ,field-names
               (apply #'format `(nil ,,template ,,@field-names)))))

(defparameter *cookie-cutter-functions* (make-hash-table))

(setf (gethash 'zoom-zoom *cookie-cutter-functions*)
      (make-canned-format-macro "~A-powered ~A" (fuel device)))
(setf (gethash 'delicious *cookie-cutter-functions*)
      (make-canned-format-macro "~A ice cream" (flavor)))
(setf (gethash 'movie-ad *cookie-cutter-functions*)
      (make-canned-format-macro "~A: A ~A and ~A film" (title star co-star)))

That repetitive setf, gethash, make-canned-format-macro pattern is awfully boilerplate-y, so I tried converting it to a loop:

(loop
  for template in '(('zoom-zoom "~A-powered ~A" (fuel device))
                    ('delicious "~A ice cream" (flavor))
                    ('thematic "~A: A ~A and ~A film" (title star co-star)))
  do (let ((func-name (car template))
           (format-string (cadr template))
           (params (caddr template)))
        (setf (gethash func-name *cookie-cutter-functions*)
              (make-canned-format-macro format-string params))))

Unfortunately, this blows up because make-canned-format-macro is operating on the value PARAMS instead of the value OF params, because it's getting macroexpanded at compile time, not evaluated at runtime. But as I learned when I asked this question, make-canned-format-macro won't work as a function, because it needs to construct the lambda form at compile time. (At least, I think that's what I learned from that? Please tell me I'm wrong about this point! I'd love to have my function-factory be a function, not a macro!)

My current thought is to write a turn-this-list-of-templates-into-make-canned-format-macro-forms macro instead of a loop. Is that the right thing to do (or at least a non-insane thing to do), or is there a better way?


Solution

  • Since you know the arguments at compile/macro-expansion time, you won't need apply:

    CL-USER 35 > (defmacro make-canned-format-macro (template field-names)
                   `(function (lambda ,field-names
                                (format nil ,template ,@field-names))))
    MAKE-CANNED-FORMAT-MACRO
    
    CL-USER 36 > (macroexpand-1 '(make-canned-format-macro "~A-powered ~A" (fuel device)))
    (FUNCTION (LAMBDA (FUEL DEVICE) (FORMAT NIL "~A-powered ~A" FUEL DEVICE)))
    T
    

    There is also no need to double quote things in a list:

    '('(a))
    

    Code like that is very unusual.

    Code generation at runtime

    The name -macro makes no sense, since it makes a function. The function needs to generate executable code: either use EVAL or use COMPILE.

    CL-USER 56 > (defun make-canned-format-function (template field-names)
                   (compile nil `(lambda ,field-names
                                   (format nil ,template ,@field-names))))
    MAKE-CANNED-FORMAT-FUNCTION
    
    
    CL-USER 57 > (loop
                  for (func-name format-string params)
                  in '((zoom-zoom "~A-powered ~A"        (fuel device))
                       (delicious "~A ice cream"         (flavor))
                       (thematic  "~A: A ~A and ~A film" (title star co-star)))
                  do (setf (gethash func-name *cookie-cutter-functions*)
                           (make-canned-format-function format-string params)))
    NIL
    

    Construction via macros

    CL-USER 77 > (defun make-canned-format-function-code (template fields)
                   `(lambda ,fields
                      (format nil ,template ,@fields)))
    MAKE-CANNED-FORMAT-FUNCTION-CODE
    
    CL-USER 78 > (defmacro def-canned-format-functions (ht description)
                   `(progn ,@(loop
                              for (func-name format-string params) in description
                              collect `(setf (gethash ',func-name ,ht)
                                             ,(make-canned-format-function-code format-string params)))))
    DEF-CANNED-FORMAT-FUNCTIONS
    
    CL-USER 79 > (pprint
                  (macroexpand-1
                   '(def-canned-format-functions
                     *foo*
                     ((zoom-zoom "~A-powered ~A"        (fuel device))
                      (delicious "~A ice cream"         (flavor))
                      (thematic  "~A: A ~A and ~A film" (title star co-star))))))
    
    (PROGN
      (SETF (GETHASH 'ZOOM-ZOOM *FOO*)
            (LAMBDA (FUEL DEVICE)
              (FORMAT NIL "~A-powered ~A" FUEL DEVICE)))
      (SETF (GETHASH 'DELICIOUS *FOO*)
            (LAMBDA (FLAVOR)
              (FORMAT NIL "~A ice cream" FLAVOR)))
      (SETF (GETHASH 'THEMATIC *FOO*)
            (LAMBDA (TITLE STAR CO-STAR)
              (FORMAT NIL "~A: A ~A and ~A film" TITLE STAR CO-STAR))))
    

    In your code you would write at top-level:

    (def-canned-format-functions
       *foo*
       ((zoom-zoom "~A-powered ~A"        (fuel device))
        (delicious "~A ice cream"         (flavor))
        (thematic  "~A: A ~A and ~A film" (title star co-star))))