typeslispcommon-lisp

Defmethod on Arbitrary Type Specifiers?


What I'm trying to do is this:

(defgeneric fn (x))

(defmethod fn ((x (integer 1 *)))
    "Positive integer")

(defmethod fn ((x (integer * -1)))
    "Negative integer")

I want a generic function that works with arbitrary type specifiers, including the list-based ones such as (and x y), (or x y), (satisfies p), etc. Now when I attempt to run the above code, I get an "Invalid Specializer" error. A little bit of research reveals that defgeneric is designed to work with CLOS, not with arbitrary type specifiers. Is there a defgeneric-like system in Common Lisp that would get me the behavior I want for arbitrary type specifiers, not just classes?


Solution

  • Common Lisp defines two hierarchies which are related but not identical: the type hierarchy and the class hierarchy. Every class is a type, but the converse is not true — there are types that are not classes. For example, integer and string are classes, and therefore also types. On the other hand, (integer 1 *) and (satisfies evenp) are types, but not classes.

    > (type-of "toto")
    (SIMPLE-BASE-STRING 4)
    > (class-of "toto")
    #<BUILT-IN-CLASS STRING>
    

    Parameter specialisers — the things that you put after parameters in defmethod — can only be class names (or of the form (eql value)). Since (integer 1 *) is not a class name, your code is not allowed by Common Lisp. There is an excellent reason for that: the compiler is always able to determine the class hierarchy, while the type language is way too powerful for that:

    (defun satisfies-the-collatz-conjecture (n)
      (cond
        ((<= n 1) t)
        ((evenp n) (satisfies-the-collatz-conjecture (/ n 2)))
        (t (satisfies-the-collatz-conjecture (+ 1 (* n 3))))))
    
    (subtypep 'integer '(satisfies satisfies-the-collatz-conjecture))
    NIL ;
    NIL
    

    If you really need your code to be modular, you will need to first classify your values into something that can be made into a specialiser, and then dispatch on that:

    (defmethod fn-generic (x (sign (eql 'positive)))
      "Positive integer")
    
    (defmethod fn-generic (x (sign (eql 'negative)))
      "Negative integer")
    
    (defun classify (x)
      (cond
        ((< x 0) 'negative)
        ((= x 0) 'null)
        ((> x 0) 'positive)))
    
    (defun fn (x)
      (fn-generic x (classify x)))