Search code examples
timecommon-lisplisp-macros

Using a macro to define functions computing the 10 Easter related based dates


I'm currently learning lisp with Graham's book “ANSI Common Lisp” and as an exercise I am writing Julian-day based calendar calculations. As you know, the Easter Sunday changes from year to year and there is about 10 other special days whose actual date depends on the Easter Sunday date.

I want to define a function for each of these days, all following the following pattern:

(defun carnaval (year)
  "Carnaval Monday of YEAR.

This is 48 days before Easter Sunday."
  (- (easter year) 48))

Instead of repeating 10 times similar declarations, it seems appropriate to use a macro:

(defmacro %defeasterday (function-name screen-name offset)
  `(defun ,function-name (year)
     ,(format nil "~A of YEAR.~%~%This is ~A day~:p ~A Easter Sunday"
              screen-name
              (abs offset)
              (if (< 0 offset) "after" "before"))
     (+ (easter year) ,offset)))

(The starting % marks my intent to not export the macro in the package where it is defined.)

The macro can be used to define a function for each of the day whose date is based on Eastern Sunday date:

(%defeasterday carnaval   "Carnaval Monday" -48)
(%defeasterday mardi-gras "Mardi gras"      -47)
(%defeasterday ash        "Ash Wednesday"   -46)
…

Now, for the sake of the exercise, I would like to pack all the data on a list and use the %defeasterday macro on its items. My attempt was

(mapc #'(lambda (args) (apply #'%defeasterday args))
      '((carneval   "Carneval Monday" -48)
        (mardi-gras "Mardi gras"      -47)
        (ash        "Ash Wednesday"   -46)))

This fails with

Execution of a form compiled with errors.
Form:
  #'%DEFEASTERDAY
Compile-time error:
  The :macro name %DEFEASTERDAY was found as the argument to FUNCTION.
   [Condition of type SB-INT:COMPILED-PROGRAM-ERROR]

which teaches me that a macro is not “just a function mapping code to code” since apply is picky about running them.

How can I use the %defeasterday macro above to iterate on a list?

(If you need a ad-hoc easter function for the purpose of testing, please (defun easter () 2457860) which gives the expected answer for 2017.)


Solution

  • Applying a macro does not work

    You can't apply a macro:

    (apply #'defsomething '(foo bar))
    

    But you can evaluate a form:

    (eval (let ((args '(foo bar)))
             `(defsomething ,@args)))
    

    or

    (let ((args '(foo bar)))
      (eval `(defsomething ,@args)))
    

    Also see the function COMPILE, if you want to make sure that the code is compiled.

    Using a defining macro

    The correct way to use a defining (!) macro is this:

    (%defeasterdays
      (carnaval   "Carnaval Monday" -48)
      (mardi-gras "Mardi gras"      -47)
      (ash        "Ash Wednesday"   -46))
    

    The %defeasterdays macro should then expand above into:

    (progn
     (%defeasterday carnaval   "Carnaval Monday" -48)
     (%defeasterday mardi-gras "Mardi gras"      -47)
     (%defeasterday ash        "Ash Wednesday"   -46))
    

    DEFUN is a top-level macro. Once usually want to keep it this way. If you use EVAL of a DEFUN form, then it is not at the top-level for a file compiler. Thus you need to do the transformation in a macro, such that the defining forms are still at 'top-level'. PROGN subforms are still at top-level for a file compiler.

    Making the file compiler happy

    You can use the file compiler to compile the following code:

    ; we need the file compiler to know the value of *DAYS*
    ;  thus the eval-when. By default `DEFVAR` would not have
    ;  been executed
    (eval-when (:compile-toplevel :load-toplevel :execute)
      (defvar *days*
        '((carnaval   "Carnaval Monday" -48)
          (mardi-gras "Mardi gras"      -47)
          (ash        "Ash Wednesday"   -46))))
    
    ; a file compiler sees the following macro
    :  and its definition is automatically available at compile time
    (defmacro %defeasterday (function-name screen-name offset)
      `(defun ,function-name (year)
         ,(format nil "~A of YEAR.~%~%This is ~A day~:p ~A Easter Sunday"
                  screen-name
                  (abs offset)
                  (if (< 0 offset) "after" "before"))
         (+ (easter year) ,offset)))
    
    ; same here, the compiler learns about the next macro
    (defmacro %defeasterdays (list)
      `(progn ,@(loop for item in (symbol-value list)
                      collect `(%defeasterday ,@item))))
    
    ; now the file compiler sees the following form.
    ;  it will be macro expanded. The macros are known.
    ;  one of the macros now needs at expansion time the value
    ;  of *DAYS*. Since we have made *DAYS* known to the compiler,
    ;  this will work.
    (%defeasterdays *days*)
    

    The main advantage is that the file compiler will see the definition of all your generated functions at compile time. It will be able to generate efficient code for the functions and it also may be able to generate efficient code for forms calling these functions.

    You can also load this file, but it depends on the implementation whether the code will be compiled or if you end up with interpreted functions.