Search code examples
macroscommon-lisp

common lisp macro doesn't work if argument is generated by the function


I am new to the common lisp language. I wrote a program that executes addition using "fourcalc" macro reading "test.txt" file. When it run the list returned by the "cat-file" function, nil is returned, but when the list is defined directly as the macro argument, 5 is returned. Why is there such a difference?

## test result

% sbcl --script newlang.lisp 
  0: (CAT-FILE "test.txt")
  0: CAT-FILE returned (A PLUS 2 3)
  0: (CAT-FILE "test.txt")
  0: CAT-FILE returned (A PLUS 2 3)
T
  0: ((MACRO-FUNCTION FOURCALC) (FOURCALC (A PLUS 2 3)) #<unused argument>)
  0: FOURCALC returned (+ 2 3)
5
  0: ((MACRO-FUNCTION FOURCALC) (FOURCALC (CAT-FILE "test.txt")) #<unused argument>)
  0: FOURCALC returned NIL
NIL


## newlang.lisp

(defmacro fourcalc (args-list)
    (let ((who (first args-list))
        (f (second args-list))
        (x (third args-list))
        (y (fourth args-list)))
    (cond
        ((eql who 'a)
            (cond
                ((eql f 'plus) (list '+ x y))
                ((eql f 'minus) (list '- x y))
                ((eql f 'mul) (list '* x y))
                ((eql f 'div) (list '/ x y))
                (t nil))))))

(defun cat-file (file)
    (with-open-file (in file :direction :input)
        (loop for str = (read in nil)
            while str collect str)))

(trace cat-file)
(trace fourcalc)
(cat-file "test.txt")
(format t "~A~%" (listp (cat-file "test.txt")))
(format t "~A~%" (fourcalc (a plus 2 3)))
(format t "~A~%" (fourcalc (cat-file "test.txt")))

## test.txt
% cat test.txt
a plus 2 3

Try:

  • confirm type check
  • trace the macro and function

Expecting: return 5 when (fourcalc (cat-file "test.txt") is done


Solution

  • Macros are syntax extension and they take static code as input and result in transformed code as output.

    The code (fourcalc (a plus 2 3)) NEVER evaluates the form (a plus 2 3) before the macro has done it's thing. The macro expands it to (+ 2 3) and that is the code that is replaced by the macro form and the code that gets compiled and executed. You'll see this by evaluating macroexpand:

    (macroexpand-1 '(fourcalc (a plus 2 3)))
    (+ 2 3) ;
    t
    

    In the second version it never evaluates (cat-file "test.txt") for the exact same reason. The macro evaluates to nil since the first part is cat-file and not a:

    (macroexpand-1 '(fourcalc (cat-file "test.txt")))
    nil ;
    t 
    

    The code will never fetch anything from text file since the macro expansions replaces that code. Arguments are never generated by functions since macros work on the code as the data and happening long before anything else gets run. It will continue to expand until the expansion does not change the result, then it will expand the sub forms of the code that didn't expand not more and continue until there is no more expansions. If you make a function this can happen when the function gets created once and then the function gets stored with the replacement only.