Search code examples
lispcommon-lispsbcl

Unbound Variable in Lisp Macro using loop-for-collect


I am a beginner lisp programmer, and I'm following the Practical Common Lisp book, specifically chapter 9.

After finishing the chapter, I've tried to expand the unit-testing environment. Specifically, I wanted a macro that gets a function name and a list of inputs and outputs instead of manually comparing them. i.e.

(check-i-o 1+ (2 3) (3 4)) => (check (= (1+ 2) 3) (= (1+ 3) 4))

I will admit that I am still a bit confused about the syntax of backtiks, and this probably is the problem, but this is the following code I wrote trying to program this

(defmacro check-i-o (function &body inputs-outputs)
  `(check
    ,@(loop for i-o in inputs-outputs
            collect `(= (,function (first i-o)) (second i-o)))))

For some reason, whenever I try to run this macro on an example (for instance, (check-i-o 1+ (2 3) (3 4))) I encounter the error The variable I-O is unbound.

If it is relevant: I am using slime-repl sbcl in portacle emacs on windows.

Thank you so much for your help!

I have tried multiple variations on the code provided (mainly changing backticks, using #'1+ instead of 1+ when calling the macro, and trying to remove the &body from the decleration of the macro...) but nothing helped and I am at a loss...


Solution

  • You can macroexpand your example code to debug your problem:

    > (macroexpand '(check-i-o 1+ (2 3) (3 4)))
    
    (CHECK 
      (= (1+ (FIRST I-O)) (SECOND I-O)) 
      (= (1+ (FIRST I-O)) (SECOND I-O)))
    

    The generated code contains references to I-O, a variable that is not bound when you evaluate the expression. It was a variable bound inside your macro, but you directly inserted the I-O symbol in the resulting expression. That's why you have an error.

    The backquote/unquote mechanism is a way to produce a form where some parts are evaluated and other parts aren't: what is backquoted is not evaluated, except for the subterms which are unquoted.

    In your code, you write:

    `(= (,function (first i-o)) (second i-o)))
    

    Only the content of the function variable is inserted in the resulting form. The rest is left unevaluated. You need to write:

    `(= (,function ,(first i-o)) ,(second i-o)))
    

    Another improvement would be to use the fact that LOOP allows you to pattern-match (aka. destructure) lists for you:

    ,@(loop
        for (i o) in inputs-outputs
        collect `(= (,function ,i) ,o))))