Frank Jefferson
Frank Jefferson

Reputation: 87

Building functions with dynamic inputs with macros

My goal is to have a macro that builds functions automatically with an argument list generated elsewhere. I want the macro to return a list composed of the function and the argument list it used (list of symbols). I'm using SBCL.

Generating the argument list

Let's say the argument list is generated by :

(defun input-syms ()
  (list 'in1 'in2 'in3))
;;=> (IN1 IN2 IN3)

Building a function with this argument list

Following the useful answer in Nested `defun` produces a repeated warning in Allegro Common Lisp, I used labels like so (adding the elements of a list, just for the sake of example) :

(defmacro create-funtest ()
  (let ((input-list (input-syms)))
    `(labels ((fun-created ,input-list
                (reduce #'+ (list ,@input-list))))
       #'fun-created)))
(funcall (create-funtest) 2 2 3) ;=> 7

This seems to work, although I think there could be a simpler way of doing this. (list ,@input-list) seems unnecessary , but replacing it with just ,input-list does not work.

Returning the argument list along with the function

This is where I am at a loss and it seems related to the issue of what exactly ,input-list means. I think this has something to do with the fact that we're manipulating symbols so I tried throwing symbol-value in there but to no avail.

What I would like to obtain, expressed in not-working code would be :

(defmacro create-funtest2 ()
  (let ((input-list (input-syms)))
    `(labels ((fun-created ,input-list
                (reduce #'+ (list ,@input-list))))
       (list #'fun-created ,input-list))))

Which must return : (#<FUNCTION ...> (IN1 IN2 IN3)). But calling create-funtest2 gives a compilation error The variable IN2 is unbound.. I think it's trying to evaluate the symbol instead of giving me the symbol as is.

I need to be able to get the symbols that were used to build the function, I use them when calling the function afterwards to know which input is what. This list of symbols can also be modified by the create-funtest macro, so I really need to get it from inside the macro.

Edit 1

Thanks Rainer Joswig for the answer. The thing that is bothering me is actually returning the symbol list as symbols. I guess the expanded code for create-funtest2 (from the expansion you've given) should look like :

(LABELS ((FUN-CREATED (IN1 IN2 IN3)
          (REDUCE #'+ (LIST IN1 IN2 IN3))))
  (LIST #'FUN-CREATED (LIST 'IN1 'IN2 'IN3)))

So that the output of the macro is (#<FUNCTION ...> (IN1 IN2 IN3)). The issue is that I want to evaluate input-list, but keep its elements as symbols (not sure I'm wording that right sorry).

Edit 2

Thank you coredump. The working version of create-funtest2 is :

(defmacro create-funtest2 ()
  (let ((input-list (test-input-syms)))
    `(labels ((fun-created ,input-list
                (reduce #'+ (list ,@input-list))))
       (list #'fun-created (quote ,input-list)))))

Which gives the expansion (thank you Rainer for the code snippet):

(let ((*print-circle* t)
          (*PRINT-RIGHT-MARGIN* 50))
      (pprint (copy-tree (macroexpand-1 '(create-funtest2)))))
=>
(LABELS ((FUN-CREATED (IN1 IN2 IN3)
           (REDUCE #'+ (LIST IN1 IN2 IN3))))
  (LIST #'FUN-CREATED '(IN1 IN2 IN3)))

And is called in the following way :

(defparameter *fun-created2* (create-funtest2))
(funcall (car *fun-created2*) 1 2 3) ; => 6, OK
(second *fun-created2*) ; => (IN1 IN2 IN3), OK

Upvotes: 3

Views: 100

Answers (2)

coredump
coredump

Reputation: 38809

Simpler version

The reason you might want to use reduce when given an arbitrary long list of inputs is that function calls are limited by CALL-ARGUMENT-LIMIT. Here, however, you are using your list of symbols as function parameters, which is limited by LAMBDA-PARAMETERS-LIMIT. Also, the second must be greater or equal to the first. So if the list of symbols is short enough to serve as a list of parameters, it is also short enough to be used as arguments in the call of +.

(defun input-symbols ()
  '(in1 in2 in3))

(defmacro create-funtest ()
  (let ((args (input-symbols)))
    `(lambda ,args (+ ,@args))))

Here above, I also used an anonymous function, but this is not important.

Returning symbols for evaluation

Your second version, rewritten using the same approach as above, is:

(defmacro bad-create-funtest2 ()
  (let ((args (input-symbols)))
    `(list (lambda ,args (+ ,@args))
           ,args)))

What does macroexpand say about this?

(macroexpand '(bad-create-funtest2))
=> (LIST (LAMBDA (IN1 IN2 IN3) (+ IN1 IN2 IN3))
         (IN1 IN2 IN3))

Here, you can see that you are trying to call in1 with arguments in2 and in3. You do not want to evalutate the list of symbols, just pass it unevaluated.

Returning symbols unevaluated

(defmacro create-funtest2 ()
  (let ((args (input-symbols)))
    `(list (lambda ,args (+ ,@args))
           (quote ,args))))

By quoting the value, you can ensure the value will not be evaluated.

(macroexpand '(create-funtest2))
=> (LIST (LAMBDA (IN1 IN2 IN3) (+ IN1 IN2 IN3))
         '(IN1 IN2 IN3))

Upvotes: 2

Rainer Joswig
Rainer Joswig

Reputation: 139261

Inputlist is (IN1 IN2 IN3).

This works:

(reduce #'+ (list IN1 IN2 IN3))

This does not work:

(reduce #'+ (IN1 IN2 IN3))

Reason: There is no function IN1.

Macroexpand is your friend:

CL-USER 58 > (let ((*print-circle* t)
                   (*PRINT-RIGHT-MARGIN* 50))
               (pprint (copy-tree (macroexpand-1 '(create-funtest2)))))

(LABELS ((FUN-CREATED (IN1 IN2 IN3)
           (REDUCE #'+ (LIST IN1 IN2 IN3))))
  (LIST #'FUN-CREATED (IN1 IN2 IN3)))

The expanded code has two problems:

  1. the function IN1 in the last line does not exist
  2. the variables IN2 and IN3 in the last line does not exist

Maybe the macroexpand will help you to solve your problem.

For more help you'd need to come up with slightly better explanation what you want to do.

Upvotes: 2

Related Questions