0

So I have 2 router functions for learning to work with clack in common lisp. One is written without macros, and the other is written with macros that expand to match the first function, but only the non-macro version is working. The code below has (:use :trivia) from (ql:quickload 'trivia).

This one is written without any macros, and works:

(defun router (env)
  (match env
    ((guard (property :path-info path)
        (equalp "/" path))
     (home env))
    ((guard (property :path-info path)
        (equalp "/live/clicked" path))
     (live-clicked env))
    ((property :path-info path)
     `(404 nil (,(format nil "404 page not found"))))))

I decided I didn't like those guard clauses taking up so much space in the function definition, so I rewrote the function:

(defun router (env)
  (match env
    (route "/" (home env))
    (route "/live/clicked" (live-clicked env))
    ((property :path-info path)
     `(404 nil (,(format nil "404 page not found"))))))

route is defined as so:

(defmacro route (valid-path &body body)
  (let ((path (gensym)))
    `((guard (property :path-info ,path)
         (equalp ,valid-path ,path))
      ,@body)))

With this new router function and macro, the function is always short-circuiting on the first clause. When macroexpanding the 2 (route ...) clauses, I receive this output, matching the function I wrote:

* `(defun router (env)
      (match env
        ,(macroexpand '(route "/" (home env))) 
        ,(macroexpand '(route "/live/clicked" (live-clicked env)))
        ((property :path-info path)
         `(404 nil (,(format nil "404")))))))
(DEFUN ROUTER (ENV)
  (MATCH ENV
   ((GUARD (PROPERTY :PATH-INFO #:G120) (EQUALP "/" #:G120)) (HOME ENV))
   ((GUARD (PROPERTY :PATH-INFO #:G121) (EQUALP "/live/clicked" #:G121)) (LIVE-CLICKED ENV))
   ((PROPERTY :PATH-INFO PATH) `(404 NIL (,(FORMAT NIL "404")))))

(home env) and (live-clicked env) are functions that return something similar to (backquote (200 nil (,(*form generating html*)))). env is the state of the web request, but in this instance it only needs to be (list :path-info "/live/clicked")

2 Answers 2

4

First of all, macroexpand should be used for interactive debugging rather than in production code. Its use inside defun (almost) never makes sense.

Second, to understand your second router, you should macroexpand its body rather than the defun, i.e.,

(macroexpand '(match env
        (route "/" (home env))
        (route "/live/clicked" (live-clicked env))
        ((property :path-info path)
         `(404 nil (,(format nil "404"))))))

You will see that route is not expanded because it is not in a "function position".

What you need to expand your route before match is expanded, i.e., you probably want a wrapper around match (I removed gensym because you indicated in the comments that it is really not necessary):

(defmacro my-match (what &body clauses)
  (labels ((handle-clause (clause)
             (if (eq (car clause) 'route)
                 (apply #'route (rest clause))
                 clause))
           (route (valid-path &rest body)
             `((guard (property :path-info path)
                      (equalp ,valid-path path))
               ,@body)))
    `(match ,what ,@(mapcar #'handle-clause clauses))))

and then

(defun router (env)
  (my-match env
    (route "/" (home env))
    (route "/live/clicked" (live-clicked env))
    ((property :path-info path)
     `(404 nil (,(format nil "404 page not found"))))))

Testing:

(macroexpand '(my-match env
        (route "/" (home env))
        (route "/live/clicked" (live-clicked env))
        ((property :path-info path)
         `(404 nil (,(format nil "404"))))))
==>
(MATCH ENV ((GUARD (PROPERTY :PATH-INFO PATH) (EQUALP "/" PATH)) (HOME ENV))
 ((GUARD (PROPERTY :PATH-INFO PATH) (EQUALP "/live/clicked" PATH))
  (LIVE-CLICKED ENV))
 ((PROPERTY :PATH-INFO PATH) `(404 NIL (,(FORMAT NIL "404")))))
T
Sign up to request clarification or add additional context in comments.

3 Comments

In my code, there's a * before that containing the macroexpand calls, signifying that it is in the repl (IE, debugging). I had added gensym in debugging attempts, as I was thinking that the path variable might be contributing to the issues. I think you could have used kinder phrasing with both of those notes. I appreciate your help in this :)
I am not sure where the you found unkindness, but I apologize nevertheless. I modified the related parts, I hope it's okay now.
All good - I didn't think you meant anything by it, just rubbed me the wrong way. I've definitely been on the other end of things :). context for future readers, It was noted that I didn't need gensym in my macro.
1

The pattern matcher you are using, Trivia, has a defpattern macro. That's what you must use to define a macro against the pattern language itself, rather than defmacro.

1 Comment

I wrote a much longer answer, but it was TL; DR. The gist of it is that embedded languages provided by macros are not traversed for macroexpansion. The macro-expander sees match, and calls a macro. That macro traverses the match form and spits out code. That code, though traversed by the expander, no longer has clause syntax for it to look at. Sub-languages for pattern matching, looping and whatever else must always provide their own macro definers. Wrapping the constructs is a stop-gap solution that will not scale to multiple developers who all want to extend the same construct.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.