defgeneric with optional and keyword arguments

221 views Asked by At

I want to define a generic function in CL that takes an optional and a keyword argument both of which have a default value. I tried

(defgeneric read-one (buffer &optional (sz 1) &key (signed '()))

but this throws Invalid &OPTIONAL argument specifier #1=(SZ 1)

So what is the proper way to do this sort of thing?

2

There are 2 answers

1
leetwinski On BEST ANSWER

afaik you can't provide defaults in defgeneric. You would have to do this in the concrete implementation (defmethod)

(defgeneric read-one (buffer &optional sz &key signed))

(defmethod read-one (buffer &optional (sz 1) &key (signed '()))
  (format t "~a, ~a, ~a~%" buffer sz signed))

CL-USER> (read-one (list 1 2 3) )
;; (1 2 3), 1, NIL
;; NIL

;; CL-USER> (read-one (list 1 2 3) 101 :signed t)
;; (1 2 3), 101, T
;; NIL
0
ignis volens On

You can't provide defaults or supplied-p options in generic function lambda lists (or things like &aux).

So to provide default values you need to to do this in a method. This can be painful if you have a lot of primary methods, so a good way to do this, if you want the default to be common between primary methods, is in a method which wraps around the primary method or methods.

In standard method combination it's tempting to use around methods for this:

(defgeneric foo (x &optional y &key z)
  (:method :around (x &optional (y 1) &key (z 0))
   (call-next-method x y :z z)))   

(defmethod foo (x &optional y &key z)
  ;; This method can assume things have been defaulted as can any
  ;; other principal method
  (values x y z))

Now

> (foo nil)
nil
1
0

> (foo nil 4 :z 12)
nil
4
12

But that can be problematic, because it is always possible for another around method to be wrapped around any given around method, since around methods run in most-specific-first order:

(defmethod foo :around ((x number) &optional (y 3) &key (z 8))
   (call-next-method x y :z z))

And now

> (foo nil)
nil
1
0

> (foo 1)
1
3
8

Sometimes that is what you want: you might want the default values to depend on the classes of the positional arguments. But sometimes what you want is to be able to say 'these are the defaults, nothing can override them'. You can't do that with standard method combination.

To do this you need wrapping-standard method combination or something like it. Using that you can do this:

(defgeneric foo (x &optional y &key z)
  (:method-combination wrapping-standard)
  (:method :wrapping (x &optional (y 1) &key (z 0))
   (call-next-method x y :z z)))

(defmethod foo (x &optional y &key z)
  (values x y z))

And this wrapping method can't be overridden:

(defmethod foo :around ((x number) &optional (y 3) &key (z 8))
   (call-next-method x y :z z))

But still:

> (foo nil)
nil
1
0

> (foo 1)
1
1
0

Note I did not write wrapping-standard, but it's useful I think.