Implementations can store values in the slots of structure objects (and possibly standard objects) in a packed/unboxed format for space and sometimes speed efficiency, similarly to how they can be stored in arrays and complexes. But unlike those cases, there is no defined interface for the programmer to get information about this storage.
Add a function: upgraded-slot-type
. This works analogously to upgraded-array-element-type
or upgraded-complex-part-type
, in that it takes a type specifier and environment and returns an upgraded type specifier, where the input is a recognizable (fixing the UAET spec bug) subtype of the output. There are no other requirements, and in particular implementations are permitted to always return T
. upgraded-slot-type
has no semantic effects and only serves as an optimization hint for the programmer.
upgraded-slot-type
is essentially only relevant for structure objects. Vector and list structures have "slot types" of the vector element type and T
, respectively.
The standard explicitly leaves the consequences of redefining a structure undefined. This is one reason that defstruct
is inconvenient for development: implementations are allowed to do essentially anything in the event of defstruct
definition, even technically when it's the same form being evaluated (though all implementations I know of do something reasonable in this case). But I think it's possible to make redefinition semantics clearer and safer, while retaining defstruct
's efficiency.
X3J13 decided against more specific redefinition semantics in issue DEFSTRUCT-REDEFINITION, on the theory that it was only really relevant for interactive programming. This is probably true, but interactive programming is a lot of programming and I feel that leaving it totally undefined is pretty unfortunate. At the very least, merely changing initforms ought to be fine.
Redefining a structure, i.e. evaluating a defstruct
form where the name has previously been used to define a structure, works as follows.
Any functions or setf expansions defined by the new definition that were also defined by the old definition are redefined if necessary. For example, given (defstruct foo bar)
followed by (defstruct foo bar baz)
, make-foo
is redefined as if by defun
.
It is implementation-dependent whether any functions or setf expansions defined by the previous definition will still be available if the new structure definition does not define them. For example, given (defstruct foo bar)
then (defstruct foo (bar 0 :read-only t))
it is implementation-dependent whether there is a setf expansion for foo-bar
. Portable code must not rely on these definitions persisting.
If the new definition is compatible with the old definition, the structure class (if there is one) is perhaps reinitialized but not replaced, and existing instances continue to be instances of the same structure class. If the new definition is not compatible with the old, any structure class is replaced in the global environment rather than reinitialized (as by (setf find-class)
), but the old class and any methods specialized on it, etc., are retained. Implementations are encouraged to issue a warning upon incompatible structure redefinition.
In case of compatible redefinition, any new slots in an old object are uninitialized even if the new definition has an initform, so reading these slots of an old object before they are written has undefined consequences. Reading the value of any slots corresponding to an :initial-offset
is undefined behavior. The value of any slots corresponding to a :named
option is the name, and the consequences are undefined if the new slot definition's :type
would not permit this. [NOTE: The X3J13 rationale for uninitialized element access being undefined behavior rather than simply returning an indetermine value is to allow implementations to issue warnings or errors, and I think that applies here. See issue UNINITIALIZED-ELEMENTS.]
Two structure definitions are compatible if:
- They have the same
:type
option, i.e. they are both structure objects, both lists, or both vectors of the same element type. - They both have no
:include
or both have the same:include
. - They either both have
:named
or neither does. - They either both have the same
:initial-offset
or neither has an:initial-offset
. - They have the same number of slots, in the same order, with the same names, and the same upgraded types.
Implementations may define additional situations in which structure definitions are compatible. Compatibility is defined essentially so that old instances can be used as new instances without any reinitialization process.
Note that for the purpose of determining the upgraded type of a slot that is :included
in another structure, types declared for the slot in an overriding slot description are irrelevant. That is, the type is upgraded from its original type declaration, not from that declared in a subtype of the structure, even in that subtype of the structure.
(defstruct foo)
(defparameter *old-foo-class* (find-class 'foo))
(defparameter *old-foo-constructor* #'make-foo)
(defmethod testfun ((x foo)) :old)
(defstruct foo (bar nil))
(defmethod testfun ((x foo)) :new)
;; Assuming the structure definitions are incompatible in this implementation:
(eq *old-foo-class* (find-class 'foo)) ; => NIL
(typep (funcall *old-foo-constructor*) 'foo) ; => NIL
(eq *old-foo-class* (class-of (funcall *old-foo-constructor*))) ; => true
(testfun (funcall *old-foo-constructor*)) ; => :OLD
(typep (make-foo) 'foo) ; => true
(testfun (make-foo)) ; => :NEW
(foo-bar (funcall *old-foo-constructor*)) ; => undefined behavior, as the type restriction on foo-bar was violated
;; If they are compatible:
(eq *old-foo-class* (find-class 'foo)) ; => T
(typep (funcall *old-foo-constructor*) 'foo) ; => T
(testfun (funcall *old-foo-constructor*)) ; => :NEW
(foo-bar (funcall *old-foo-constructor*)) ; => undefined behavior, as the slot is uninitialized (despite the initform)
(defstruct foo (bar 0 :type fixnum))
(defstruct (foo2 (:include foo)) (baz 'baz :type symbol))
(defstruct (foo (:constructor)) (bar 13 :read-only t :type fixnum)) ; compatible
(defstruct (foo2 (:include foo (bar 15 :type (unsigned-byte 8))) (baz 'foo2:type symbol)) ; compatible, regardless of how ub8 upgrades
Structures can be made to work with make-instance
and related functions in some respects in what is I think a reasonable way.
make-instance
is permitted on structure classes. The initargs and initforms are as they are for a non-BOA structure constructor. Structure constructors are not required to use make-instance
, i.e. methods on make-instance
may not be invoked by a structure constructor.
allocate-instance
is permitted on structure classes. A structure constructed in this way is not initialized, so reading any of its slots before they are written has undefined consequences. Structure constructors and make-instance
are not required to use allocate-instance
, i.e. methods on it may not be invoked by a structure constructor.
Methods on shared-initialize
, initialize-instance
etc. specialized on structure classes may be defined by the programmer, but are not guaranteed to be run at any time unless directly invoked, even in the case of using make-instance
.
The obstacle to storing structures unboxed in arrays is the semantics of eql
. It is generally understood, though not explicitly stated by the standard that I can tell, that arrays preserve EQLity. That is, (let ((a (make-array ...))) (setf (aref a 0) some-object) (eql some-object (aref a 0)))
must be true, regardless of the specialization of the array. Storing objects in arrays unboxed essentially means aref
must box a new object at times, and this new object will not be eq
to the original object. This is okay for numbers or characters, since eql
is defined for them anyway. But structures are only eql
if they are eq
, so boxing a structure in aref
would mean the array did not preserve EQLity. Therefore we need an extension.
New unboxable structure classes can be defined through defstruct
. A new :unboxable
option is provided. It takes no parameters, like :named
. An error is signaled if :unboxable
and :type
are both supplied.
Implementations are permitted to not support unboxable structure objects in some or all circumstances. If an implementation cannot make an unboxable structure object from a defstruct
definition in which that was requested, it is encouraged to issue a note.
If :unboxable
is provided to defstruct
, all slots of the defined class, including inherited slots, are read only, as if :read-only t
was supplied in their definitions. An error is signaled if :read-only nil
is specified in the definition of a slot in an :unboxable
structure.
Unboxability is not inherited: structure classes that are not unboxable may :include
unboxable structure classes and vice versa. [NOTE: This presents a problem with respect to UAET's requirement to move "up" in the type lattice.]
Implementations are encouraged but not required to store unboxable structure objects unboxed in specialized arrays and to make unboxable structure classes their own upgraded-array-element-type
and upgraded-slot-type
.
If an unboxable struct class is the upgraded element type of an array, the consequences are undefined if an attempt is made to store an instance of a subclass of that class in that array. [Also presents some UAET weirdness. Also, maybe "should signal an error" instead of just UB?]
Unboxable structure objects behave differently when passed to eql
than other structure objects. Two unboxable structure objects are eql
if and only if they have the same class and all of their fields are eql
to each other. equal
is similarly defined recursively.
eq
on unboxable structure objects is defined as it is for numbers and characters. That is, the implementation is permitted to "copy" unboxable structure objects at any time, and there is no guarantee that eq
on unboxable structure objects is ever true. eql
should be used instead.
(defstruct bpoint (x 0.0 :type single-float) (y 0.0 :type single-float) (z 0.0 :type single-float))
(defstruct (point :unboxable) (x 0.0 :type single-float) (y 0.0 :type single-float) (z 0.0 :type single-float))
(let* ((bpoint (make-bpoint :x 1.0 :y 39.4 :z -7.6))
(array (make-array 23 :element-type 'bpoint :initial-element bpoint)))
(print (array-element-type array)) ; probably T
(print (eq (aref array 0) bpoint)) ; => T
(print (eql (aref array 0) bpoint)) ; => T
(+ (bpoint-x (aref array 0)) (bpoint-y (aref array 0)) (bpoint-z (aref array 0))))
(let* ((point (make-point :x 1.0 :y 39.4 :z -7.6))
(array (make-array 23 :element-type 'point :initial-element point)))
(print (array-element-type array)) ; can be POINT or #<STRUCTURE-CLASS POINT> or T or some other superclass
(print (eq (aref array 0) point)) ; can be NIL
(print (eql (aref array 0) point)) ; => T
;; In an implementation that has unboxable POINT but doesn't optimize very well, this could incur three boxings,
;; but a better compiler could avoid boxing entirely (except maybe the resulting float).
(+ (point-x (aref array 0)) (point-y (aref array 0)) (point-z (aref array 0))))
(eql (make-bpoint) (make-bpoint)) ; => NIL
(eql (make-point) (make-point)) ; => T