Claire Nielsen
Claire Nielsen

Reputation: 2161

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?

Upvotes: 2

Views: 88

Answers (2)

Rainer Joswig
Rainer Joswig

Reputation: 139261

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))))

Upvotes: 4

blihp
blihp

Reputation: 675

You absolutely can do what you want as a function. It won't be the prettiest code, but it will work. The key thing that you took away from macros is correct: they are evaluated at compile time[1] I took a look at the other question you referenced and while they were giving you some good optimization/refactoring advice, sometimes you just want what you want. So I've tried to minimally change your code to get it working with one change: rather than using a macro which is eval'd at compile time, I've made it a function which generates the code which is then eval'd at run time:

(defun make-canned-format (template field-names)
    (eval `(lambda ,field-names
        (apply #'format `(nil ,,template ,,@field-names)))))

Now you should be able to do whatever makes sense for the mass definition of your functions (i.e. wrap in macros, loops, whatever) Something to keep in mind about this approach is that the performance of repeated calls with the same template/field names will be dismal (since it is blindly regenerating the same source code and eval it every call at run time vs. a macro whose definition would just eval once at compile time.) But since you appear to be calling it once per pair of parameters and storing the result of the generation, this won't be an issue.

[1] unless you use the approach used here to generate the macro and eval it at run time. This can be confusing and even harder to debug than macros already can be, but it can be done.

Upvotes: 1

Related Questions