Warm tip: This article is reproduced from serverfault.com, please click

Dynamic type specification in Common Lisp?

发布于 2020-12-07 00:26:29

I know that you can, at programming time, declare a type for variable arguments:

(defun foo (a b) (declare (type integer a b))

However; I'd like to be able to specify the types dynamically. I've got a macro which lets me create a-lists (list of cons cells) by passing an args-list ... for example:

(defalist foo (x y z))  
--> returns function whose body ~ (list (cons 'x x) (cons 'y y) (cons 'z z))

I would also like to be able to dynamically specify the types of the args, the way you would within a defun, but programmatically generated with a macro. I've tried using declare/declaim but I keep running into issues -- it seems like SBCL needs the actual type specified at compile time... I'd like to be able to, within a defmacro, be able to do something like:

 (mapcan #'(lambda (arg typ) (declare (type typ arg))) args-list types-list)

What would be the best way to do this?

Thanks for your help!

Questioner
Flywheel
Viewed
0
tfb 2020-12-08 02:40:30

I'm not sure quite what you mean by 'dynamic' here.

If what you mean is that you want to provide type specifications dynamically at run-time (which would be the standard meaning pf 'dynamic' I think), then you can't do this with declare (or you can't without eval or compile or some equivalent and let's not go there). Rather you need to use check-type, typep or some related thing to check types dynamically. The job of declare as used in function definitions is to allow the compiler to learn some things about types in the function, which may allow it to create code which is some combination of more correct, more careful, less careful, faster (which of these applies depends on the compiler and compilation options). For that to work these types necessarily need to be known at compile time.

However, if what you really want is to be able to declare types in the definition of a macro, then macroexpansion happens at (or just before) compile time, so these types are in fact static type declarations: the defalist is going to expand info some defun form and you can simply add a suitable declare to that form.

Here is such a macro (note, I don't know what your defalist is meant to do: this is just what I invented), which allows you to specify types:

(defmacro defalist (name (&rest args/types) &body junk &key
                         (default-type t)
                         (checked-instead
                          ;; use explicit type checks if we're not
                          ;; using a Python-derived compiler.
                          #-(or SBCL CMUCL) t
                          #+(or SBCL CMUCL) nil))
  ;; Each argument should either be a name or (name type).
  (declare (ignore junk))               ;just to get indentation
  (assert (every (lambda (a/t)
                   (or (symbolp a/t)
                       (and (consp a/t)
                            (symbolp (first a/t))
                            (= (list-length a/t) 2))))
                 args/types)
      (args/types)
    "bad arguments ~A" args/types)
  (multiple-value-bind (args types)
      (loop for a/t in args/types
            collect (typecase a/t
                      (symbol a/t)
                      (cons (first a/t)))
            into the-args
            collect (typecase a/t
                      (symbol default-type)
                      (cons (second a/t)))
            into the-types
            finally (return (values the-args the-types)))
    `(defun ,name (,@args)
       ,(if checked-instead
            `(progn
               ,@(loop for a in args and tp in types
                       collect `(check-type ,a ,tp)))
          `(declare ,@(loop for a in args and tp in types
                            collect `(type ,tp ,a))))
       (list ,@(loop for a in args
                   collect `(cons ',a ,a))))))

And now

(defalist foo (a b c))

expands to

(defun foo (a b c)
  (declare (type t a) (type t b) (type t c))
  (list (cons 'a a) (cons 'b b) (cons 'c c)))
(defalist foo (a b c)
  :default-type integer)

expands to

(defun foo (a b c)
  (declare (type integer a) (type integer b) (type integer c))
  (list (cons 'a a) (cons 'b b) (cons 'c c)))

and finally

(defalist foo ((a fixnum) (b float) c)
  :default-type integer)

expands to

(defun foo (a b c)
  (declare (type fixnum a) (type float b) (type integer c))
  (list (cons 'a a) (cons 'b b) (cons 'c c)))

An important note: in this particular macro, the declarations in the functions it defines will certainly help the SBCL or CMUCL compilers check argument types for you, and may even help them infer the return types of the function (this does not seem to be the case in practice). But they won't, for instance, result in the list it returns being represented any differently. However it's easy to imagine macros a bit like this one whose corresponding function does something where the type declarations are more useful. Even for this function, you could perhaps add a declaration of the return type which might help (certainly it might help SBCL / CMUCL do more type inference).

If you are not using a compiler derived on the CMUCL compiler, then declarations like this are likely to make code less safe. The macro therefore tries to detect this, and in those compilers it replaces the type declarations by explicit checks. This can be manually controlled with the checked-instead keyword.