Search code examples
typesruntime-errorcommon-lispclos

How to force slot's type to be checked during make-instance?


Let's say I have the following class declaration:

(defclass foo-class ()
  ((bar :initarg :bar
        :type list)))

When I create an instance of this class, make-instance won't check whether passed arguments satisfy types of slots. So, I can create "invalid" objects this way:

> (make-instance 'foo-class :bar 'some-symb)
#<FOO-CLASS {102BEC5E83}>

However, what I'd like to see is the behavior similar to the creation of an instance of a struct, where the types are checked:

(defstruct foo-struct
  (bar nil :type list))

> (make-foo-struct :bar 'some-symb)
;; raises contition:
;;
;; The value
;; SOME-SYMB
;; is not of type
;; LIST
;; when setting slot BAR of structure FOO-STRUCT

Is there any way to achieve this?


Solution

  • Whether slot types are being checked or not is undefined for both structures and CLOS instances.

    Many implementations will do it for structures - but not all.

    Few implementations will do it for CLOS instances - Clozure CL actually does it for example.

    SBCL also can check CLOS slot types - when safety is high:

    * (declaim (optimize safety))
    
    NIL
    * (progn
    (defclass foo-class ()
      ((bar :initarg :bar
            :type list)))
    (make-instance 'foo-class :bar 'some-symb))
    
    debugger invoked on a TYPE-ERROR: The value SOME-SYMB is not of type LIST.
    
    Type HELP for debugger help, or (SB-EXT:EXIT) to exit from SBCL.
    
    restarts (invokable by number or by possibly-abbreviated name):
      0: [ABORT] Exit debugger, returning to top level.
    
    ((SB-PCL::SLOT-TYPECHECK LIST) SOME-SYMB)
    0] 
    

    How to do it otherwise?

    This is kind of an advanced subject which probably needs some CLOS meta-object-protocol hackery. Two variants:

    • define a method for SHARED-INITALIZE which checks the init arguments.

    • define a metaclass for your class and a method on SET-SLOT-VALUE-USING-CLASS . But then you need to be sure that your implementation actually provides AND uses SET-SLOT-VALUE-USING-CLASS. This is a generic function, which is part of the MOP. Some implementations provide it, but some are only using it when requested (otherwise setting a slot may get a speed penalty).

    For the latter here is self-built SBCL version to check types for writing slots:

    First the metaclass:

    ; first a metaclass for classes which checks slot writes
    (defclass checked-class (standard-class)
      ())
    
    ; this is a MOP method, probably use CLOSER-MOP for a portable version
    (defmethod sb-mop:validate-superclass
               ((class checked-class)
                (superclass standard-class))
       t)
    

    Now we check all slot writes for that metaclass:

    ; this is a MOP method, probably use CLOSER-MOP for a portable version    
    (defmethod (setf sb-mop:slot-value-using-class) :before
                  (new-value (class checked-class) object slot)
      (assert (typep new-value (sb-mop:slot-definition-type slot))
          ()
        "new value ~a is not of type ~a in object ~a slot ~a"
        new-value (sb-mop:slot-definition-type slot) object slot))
    

    Our example class uses that metaclass:

    (defclass foo-class ()
      ((bar :initarg :bar :type list))
      (:metaclass checked-class))
    

    Using it:

    * (make-instance 'foo-class :bar 42)
    
    debugger invoked on a SIMPLE-ERROR in thread
    #<THREAD "main thread" RUNNING {10005605B3}>:
      new value 42 is not of type LIST
      in object #<FOO-CLASS {1004883143}>
      slot #<STANDARD-EFFECTIVE-SLOT-DEFINITION COMMON-LISP-USER::BAR>
    
    Type HELP for debugger help, or (SB-EXT:EXIT) to exit from SBCL.
    
    restarts (invokable by number or by possibly-abbreviated name):
      0: [CONTINUE] Retry assertion.
      1: [ABORT   ] Exit debugger, returning to top level.