19.13 Primary classes

Classes have one additional variation that you can use to optimize performance. A class that is defined as primary allows the compiler to generate the most efficient code for accessing the slots defined in the primary class (whether the accessor is applied to the primary class or to one of that class's subclasses). However, a primary class cannot be combined with any other primary class (unless one is a subclass of the other). This restriction implies that you should delay declaring a class to be primary until you are sure of your inheritance design. Also, because sealed classes are already highly optimized, the primary declaration is of most use for open classes.

As an example, consider the class <sixty-unit>, and its slot total-seconds, as used in this method for decode-total-seconds:

define method decode-total-seconds 
    (sixty-unit :: <sixty-unit>)
 => (hours :: <integer>, minutes :: <integer>, seconds :: <integer>)
  decode-total-seconds(sixty-unit.total-seconds);
end method decode-total-seconds; 

Although the generic function for the slot accessor total-seconds is sealed, and it is trivial for the compiler to infer that its argument is a <sixty-unit> in the call sixty-unit.total-seconds, because <sixty-unit> is declared open, the compiler cannot emit the most efficient code for that call. Because an open class could be mixed with any number of other classes, there is no guarantee that the slots of every object that is a <sixty-unit> will always be stored in the same order — there is no guarantee that total-seconds will always be the first slot in an object that is an indirect instance of <sixty-unit>, for instance.

Declaring a class primary is essentially making a guarantee that the compiler can always put the primary class's slots in the same place in an instance, and that any other superclasses will have to adjust:

define abstract open primary class <sixty-unit> (<object>)
  constant slot total-seconds :: <integer>,
    required-init-keyword: total-seconds:;
end class <sixty-unit>;

By adding the primary declaration to the definition, any library that subclasses <sixty-unit> is guaranteed to put total-seconds at the same offset. Hence, the compiler can turn the call sixty-unit.total-seconds into a single machine instruction (load with constant offset), without concern over which subclass of <sixty-unit> was passed as an argument.

Comparison with C++: A primary class is like an ordinary base class in C++. Because only one primary class is allowed as a base class, its data members can be assigned the same fixed offset for all derived classes. See Appendix B.2, The concept of classes, for a more detailed analogy.

It is permissible to make subclasses of a primary class also primary, essentially freezing the assignment of all the slots in the subclass too. What is not permissible is to multiply inherit from more than one primary class; as you can see, such behavior would lead to a conflict between the fixed slot assignments.

Because primary classes restrict extension in this way, you should use them sparingly in libraries intended to be software components. Primary classes are of most benefit in large, modular programs, where all the clients of each component are known, and the need for extensibility is bounded; typically that occurs toward the end of a project, when you are tuning for performance.