Search code examples
common-lispclos

How to specialize generic function for subclasses of given class


How can i specialize a generic function to take symbols designating subclasses of given class. For example:

(defclass a () ())
(defclass b (a) ())
(defclass c (b) ())
(defclass d () ())

(defgeneric fun (param))
(defmethod fun ((param (<subclass of> a)))
  (format t "~a is a subclass of A~%" param))

(fun 'c) ;-> "C is a subclass of A"
(fun 'd) ;-> Error: not found method for generic function call (fun 'd)

Is such dispatching possible with CLOS? And if it is, what should I write instead of "subclass of"?


Solution

  • You won't be able to easily perform this exact task using only CLOS dispatching.

    Before I continue, I think some brief notes on terminology is important.

    The Common Lisp HyperSpec glossary defines "subclass" in this way:

    a class that inherits from another class, called a superclass. (No class is a subclass of itself.)

    This definition, while intuitive, seems odd to me as I'd expect that to be the definition of a "proper subclass". However, all classes are types, and it defines "subtype" as:

    a type whose membership is the same as or a proper subset of the membership of another type, called a supertype. (Every type is a subtype of itself.)

    Note the parenthetical: "Every type is a subtype of itself."

    It also defines a "proper subtype":

    (of a type) a subtype of the type which is not the same type as the type (i.e., its elements are a ``proper subset'' of the type).

    So, in your example, B and C are subclasses of A, and also subtypes. On the other hand B, C, and A are subtypes of A.

    The thing one puts in defmethod is a "parameter specializer name". It can be a symbol, a class (which is a little hard to type), or a list starting with eql. If you provide a symbol, it specifies the class named by that symbol (which is, of course, a type). An eql list specifies a type consisting of objects which are eql to the thing in the list.

    The method will match any object which is a member of the type the specializer specifies. And of course, a member of a subtype of X is also a member of X.

    So your first problem is that you are passing symbol objects to your method; every symbol is of type SYMBOL. A symbol that happens to name a class is no different in this respect; it's only relationship to the class is that it is the class's name, which is not a subtype relation.

    There are class objects (returned by find-class), but they're no better than symbols for method specialization here because the type of a class object is usually the same as the type of its subclasses' class objects.

    So, you're left using instances or reading AMOP to learn how to create your own types of generic functions.

    Once you have an instance, you can write the method like this:

    (defmethod fun ((param a))
      (if (eq (type-of param) 'a)
        (call-next-method)
        (format t "~a is a subclass of A~%" (type-of param))))
    

    If you have an easy way to retrieve instances of your classes, you could write this wrapper:

    (defmethod fun ((param symbol))
      (fun (retrieve-instance param)))
    

    Then you'll be able to pass symbols to fun and get the results you want.

    If you want to use AMOP functions (which were not specified by the standard but are widely available, see Closer Project), you can define retrieve-instance like this:

    (defun retrieve-instance (name)
      (let ((class (find-class name)))
        (unless (class-finalized-p class)
          (finalize-inheritance class))
        (class-prototype class)))
    

    Note that method dispatch is just about the only thing the result of class-prototype is good for; don't try to modify it or anything like that.