common-lisp

seal a generic function to prevent additional methods


I sometimes use defmethod to enforce types in functions since the syntax is nice.

Here is an utterly terrible plus function, for the sake of example.

(defgeneric plus (x y)
  :documentation "a terrible + function")

(defmethod plus ((x integer) (y integer))
  (+ x y))

Is there a way to seal my generic method so that other implementations can't be added to it?


Solution

  • Yes, you can

    You need to define a new type of generic functions, using the meta-object protocol:

    (ql:quickload :closer-mop)
    

    Here is the API of the small library:

    (defpackage :sealable-gf
      (:use :cl)
      (:export 
        #:sealable-gf        ;; class of generic functions that can be sealed
        #:seal               ;; sealing function
        #:sealed-function    ;; serious-error signaled on modification
        #:function-of        ;; accessor for the function stored in above error
    ))
    

    And the implementation:

    (in-package :sealable-gf)
    

    Define the class of function, be sure to use the appropriate metaclass:

    (defclass sealable-gf (c2mop:standard-generic-function)
      ((sealed :initform nil))
      (:metaclass c2mop:funcallable-standard-class))
    

    The function that seals a generic function:

    (defun seal (g)
      (check-type g sealable-gf)
      (with-slots (sealed) g
        (setf sealed t)))
    

    The error:

    (define-condition sealed-function (serious-condition)
      ((function :initarg :function :reader function-of)))
    

    Plug our code before the add-method function for our class:

    (defmethod c2mop:add-method :before ((g sealable-gf) m)
      (with-slots (sealed) g
        (when sealed
          (error 'sealed-function :function g))))
    

    For example:

    (defpackage :seal-user (:use :cl :sealable-gf))
    (in-package :seal-user)
    
    (defgeneric plus (a b)
      (:documentation "add stuff generically")
      (:method ((a integer) (b integer)) (+ a b))
      (:generic-function-class sealable-gf))
    
    (plus 3 2)
    => 5
    
    (seal #'plus)
    => T
    
    (defmethod plus ((a string) (b string))
      (concatenate 'string a b))
    ;; Condition SEALABLE-GF:SEALED-FUNCTION was signalled.
    ;;    [Condition of type SEALED-FUNCTION]
    

    But should you do this?

    You can use check-type or declare to manipulate types, there is no need to use generic functions. Also, sealing goes against the open nature of generic functions, at least in my opinion CLOS is about leaving some space in your design for subclasses and new methods (the fact that we can answer positively here is because no one sealed add-method, for example).