Student
Student

Reputation: 719

Generate TYPECASE with macro in Common Lisp

I have a list of two element sublists which will change and grow in the course of the program. I want to write a macro which takes a key and generates a case dynamically like:

;; This is the List for saving CASE clauses
(setf l '((number 2) (symbol 3)))

;; and i want to have the following expansion
(typecase 'y
  (number 2)
  (symbol 3)) 

I could have a macro which only refers to the global l:

(defmacro m (x)
  `(typecase ,x ,@l))

which would expand correctly

(m 'y)  ;expands to (TYPECASE 'Y (number 2) (symbol 3))

But how can i write the macro with a parameter for the list l so that it would work with other lists as well?

;; A macro which should generate the case based on the above list
(defmacro m (x l)
  `(typecase ,x ,@l))

This doesn't work since l in the arguments list i a symbol and a call to (m 'y l) will expand to (TYPECASE 'Y . L).

Wanting to adhere to typecase mechanism, my workaround was as follows:

(setf types-x '(((integer 0 *) 38)
                ((eql neli) "Neli in X")
                (symbol 39))
            )
(setf types-y '(((eql neli) "Neli in Y")
                ((array bit *) "A Bit Vector")))

(defmacro m (x types-id)
  (case types-id
    (:x `(typecase ,x ,@types-x))
    (:y `(etypecase ,x ,@types-y))))

(m 'neli :x)                ;"Neli in X"
(m 'neli :y)                ;"Neli in Y"
(m 'foo :x)             ;39

Any hints and comments is appreciated.

Upvotes: 1

Views: 411

Answers (3)

Gwang-Jin Kim
Gwang-Jin Kim

Reputation: 9865

The actual problem that you face is that if you do

(setf l '((number 2) (symbol 3)))

already on toplevel, if you evaluate l, you don't come further than

((number 2) (symbol 3))

So if you use l in a macro as an argument, you can't come further than this. But what you need is to evaluate this form (modified after adding a typecase and an evaluated x upfront) once more within the macro.

This is, why @tfb suggested to write a function which actually evaluates the matching of the types specified in l. So, we could regard his type-match function as a mini-interpreter for the type specifications given in l.

If you do a simple (defmacro m (x l) `(typecase ,x ,@l)) you face exactly that problem:

(macroexpand-1 '(m 1 l)) 
;; (typecase 1 . l)

but what we need is that l once more evaluated.

(defmacro m (x l)
  `(typecase ,x ,@(eval l)))

Which would give the actually desired result:

(macroexpand-1 '(m 1 l))
;; (TYPECASE 1 (NUMBER 2) (SYMBOL 3)) ;
;; T

;; and thus:
(m 1 l) ;; 2

So far, it seems to work. But somewhere in the backhead it becomes itchy, because we know from books and community: "Don't use eval!! Eval in the code is evil!"

Trying around, you will find out when it will bite you very soon:

# try this in a new session:

(defmacro m (x l) `(typecase ,x ,@(eval l)))
;; m

;; define `l` after definition of the macro works:
(setf l '((number 2) (symbol 3)))
;; ((NUMBER 2) (SYMBOL 3))

(m 1 l)
;; 2  ;; so our `eval` can handle definitions of `l` after macro was stated

(m '(1 2) l)
;; NIL

;; even redefining `l` works!
(setf l '((number 2) (symbol 3) (list 4)))
;; ((NUMBER 2) (SYMBOL 3) (LIST 4))

(m 1 l)
;; 2

(m '(1 2) l)
;; 4 ;; and it can handle re-definitions of `l` correctly.

;; however:
(let ((l '((number 2) (symbol 3)))) (m '(1 2) l))
;; 4 !!! this is clearly wrong! Expected is NIL!
;; so our `eval` in the macro cannot handle scoping correctly
;; which is a no-go for usage!

;; but after re-defining `l` globally to:
(setf l '((number 2) (symbol 3)))
;; ((NUMBER 2) (SYMBOL 3))

(m '(1 2) l)
;; NIL ;; it behaves correctly

(let ((lst '((number 2) (symbol 3) (list 4)))) (m '(1 2) lst))

;; *** - EVAL: variable LST has no value
;; so it becomes clear: `m` is looking in the scoping
;; where it was defined - the global scope (the parent scope of `m` when `m` was defined or within the scope of `m`).

So the conclusion is:

The given macro with eval is NOT working correctly!! Since it cannot handle local scoping.

So @tfb's answer - writing a mini-evaluator-function for l is the probably only way to handle this in a proper, safe, correct way.

Update

It seems to me that doing:

(defmacro m (x l)
  `(typecase ,x ,@l))

(defun m-fun (x l)
  (eval `(m ,x ,l)))

(m-fun ''y l) ;; 3
(m-fun 'y l)  ;; error since y unknown

(let ((l '((number 2) (symbol 3) (list 4))))
  (m-fun ''(1 2) l)) ;; => 4 since it is a list

(let ((l '((number 2) (symbol 3))))
  (m-fun ''(1 2) l)) ;; => NIL since it is a list

(let ((l '((number 2) (symbol 3))))
  (m-fun ''y l)) ;; => 3 since it is a symbol

(let ((n 12))
  (m-fun n l))  ;; => 2 since it is a number

;; to improve `m-fun`, one could define
(defun m-fun (x l)
  (eval `(m ',x ,l)))
;; then, one has not to do the strangely looking double quote 
;; ''y but just one quote 'y.
(let ((l '((number 2) (symbol 3) (list 4))))
  (m-fun '(1 2) l)) ;; => 4 since it is a list
;; etc.

at least hides the eval within a function. And one does not have to use backquote in the main code.

Upvotes: 2

user5920214
user5920214

Reputation:

You don't need a macro for what you're trying to do: use a function.

For instance, given

(defvar *type-matches*
  '((float 0)
    (number 1)
    (t 3)))

Then

(defun type-match (thing &optional (against *type-matches*))
  (loop for (type val) in against
        when (typep thing type)
        return (values val type)
        finally (return (values nil nil))))

Will match a thing against a type:

> (type-match 1.0)
0
float

> (type-match 1)
1
number

You want to keep the variables sorted by type, which you can do by, for instance:

(setf *type-matches* (sort *type-matches* #'subtypep :key #'car))

You want to keep the matches sorted of course.

If you want to delay the execution of the forms then you can do something like this (this also deals with sorting the types):

(defvar *type-matches*
  '())

(defmacro define-type-match (type/spec &body forms)
  ;; define a type match, optionally in a specified list
  (multiple-value-bind (type var)
      (etypecase type/spec
        (symbol (values type/spec '*type-matches*))
        (cons (values (first type/spec) (second type/spec))))
    (let ((foundn (gensym "FOUND")))
      `(let ((,foundn (assoc ',type ,var :test #'equal)))
         (if ,foundn
             (setf (cdr ,foundn) (lambda () ,@forms))
           (setf ,var (sort (acons ',type (lambda () ,@forms) ,var)
                            #'subtypep :key #'car)))
         ',type/spec))))

(defun type-match (thing &optional (against *type-matches*))
  (loop for (type . f) in against
        when (typep thing type)
        return (values (funcall f) type)
        finally (return (values nil nil))))

Upvotes: 3

RowPJ
RowPJ

Reputation: 529

Macro expansion happens at compile time, not run time, thus if the case clause list changes over the course of the program, the macro expansion will not change to reflect it. If you want to dynamically select an unevaluated but changeable value, you can use assoc in the expansion instead of case:

(defmacro m (x l)
  `(second (assoc ,x ,l)))

Sample expansion:

(m x l)
->
(SECOND (ASSOC X L))

Output of (assoc x l) with the value of l in your question and x = 'x:

(let ((x 'x))
  (m x l))
->
2

However if you did decide to do it this way, you could simplify things and replace the macro with a function:

(defun m (x l)
  (second (assoc x l)))

UPDATE FOR QUESTION EDIT:

Replace assoc as follows:

(defun m (x l)
  (second (assoc-if (lambda (type)
                     (typep x type))
                    l)))

Upvotes: 0

Related Questions