19.11 Sealed domains

When you define a protocol that is meant to be extended by many libraries, both the base classes and the generic functions that make up the protocol must be open. This simple exigency might make it seem that there is no hope of optimizing such a protocol — however, there is hope. You use the define sealed domain form to seal selectively subsets or branches of the protocol, permitting the compiler to make all the optimizations that would be possible if the classes and generic functions were sealed, but only for the particular subset or branch in question.

Advanced topic: Sealed domains are one of the most difficult concepts of the Dylan language to understand fully. It is reasonable to defer careful reading of this section until you are faced with a situation similar to the example — an imported open class and generic function that will be specialized by your library.

As an example, consider the say protocol as used in the time library. Because the say generic function is defined to be open, even if the compiler can infer that the argument to say is a <time> or <time-offset>, it must insert code to choose the appropriate method to call at run time on the off chance that some other library has added or removed methods for say. The solution is to add the following definition to the time library:

// Declare the say generic function sealed, for all time classes
define sealed domain say (<time>);

This statement is essentially a guarantee to the compiler that the only methods on say that are applicable to <time> objects (and also to <time-of-day> and <time-offset> objects, because <time-of-day> and <time-offset> are subclasses of <time>) are those that are defined explicitly in the time library (and in any libraries from which that one imports). Thus, when the compiler can prove that the argument to say is a <time-offset>, it can call the correct method directly, without any run-time dispatch overhead.

Another way to get the same effect as a sealed domain, which is also self-documenting, is to use define sealed method when defining individual methods on the protocol. So, for instance, in the case of the time library, we might have defined the two methods on say as follows:

define sealed method say (time :: <time>)
  let (hours, minutes) = decode-total-seconds (time); 
  format-out("%d:%s%d", hours, if (minutes < 10) "0" else " " end, minutes);
end method say;
define sealed method say (time :: <time-offset>) => ()
  format-out("%s ", if (time.past?) "minus" else "plus" end);
  next-method();
end method say;

Defining a sealed method is the same as defining the generic function to be sealed over the domain of the method's specializers. In effect, this technique says that you do not intend anyone to add more specific methods in that domain, or to create classes that would change the applicability of the sealed methods.

With either the define sealed domain form or the sealed methods, the use of say on <time> objects will be as efficient as it would be were say not an open generic function after all. At the same time, other libraries that create new classes can still extend the say protocol to cover those classes.

Sealed domains impose restrictions on the ability of other libraries to create new methods, to remove new methods, and to create new classes:

You cannot add methods to an open generic function imported from another library that would fall into the sealed domain of any other library. You can avoid this restriction by ensuring that at least one of the specializers of your method is a subtype of a type defined in your library.

Comparison with C++: A C++ compiler could optimize out the dispatching of a virtual function by analyzing the entire scope of the argument on which the virtual function dispatches, and proving that argument's exact class. Unfortunately, that scope is often the entire program, so this optimization often can be performed only by a linker. Even a linker cannot make this optimization when a library is compiled, because the classes of a library can be subclassed by a client. The complexity is compounded for dynamic-link libraries, where there may be multiple clients at once. As a result, this optimization is rarely achieved in C++.

In Dylan, sealed classes, sealed generic functions, and sealed domains explicitly state which generic functions and classes may be extended, and, more important, which cannot. The library designer plans in advance exactly what extensibility the library will have. The Dylan compiler can then optimize dispatching on sealed generic functions and classes and within sealed domains with the assurance that no client will violate the assumptions of the optimization. The sealing restrictions against subclassing or changing method applicability are automatically enforced on each client of a Dylan library.

When you seal a domain of a generic function imported from another library, you will not cause conflicts with other libraries, as long as both of the following conditions hold:

1. At least one of the types in the sealed domain is a subtype of a class defined in your library

2. No additional subtypes can be defined for any of the types in the sealed domain

In the case of a type that is a class, the first condition means that you must have defined either the class or one of its superclasses in your library. The second condition means that the classes in the domain must not have any open subclasses (a degenerate case of which is a leaf class — a class with no subclasses at all).

If you need to seal a domain over a class that has open subclasses, you will need a thorough understanding of the sealing constraints detailed in The Dylan Reference Manual, but these two simple rules should handle many common cases.

In our example, we obeyed both rules of thumb: our methods for say are on classes we defined, and our sealing was over classes that will not be further subclassed. The rules of thumb not only keep you from violating sealing constraints, they make for good protocol design: a library that extends a protocol really should extend it only for classes it fully understands, which usually means classes it creates.

As an example of the restriction on subclassing open classes involved in a sealed domain, if the <time> class were an open class, we still could not add the following class in a library that used the time library:

define class <place-and-time> (<position>, <time>)
end class <place-and-time>;

As far as the compiler is concerned, it "knows" that the only say method applicable to a <time> is the one in the time library. (That is what we have told it with our sealed domain definition.) It would be valid to pass a <place-and-time> object as an argument to a function that accepted <time> objects, but within that function the compiler might have already optimized a call to say to the method for <time> objects (based on <time> being in the sealed domain of say). But there is also a method for say on <position>, and, more important, we probably will want to define a method specifically for <place-and-time>. Because of this ambiguity, the class <place-and-time> cannot be defined in a separate library, and the compiler will signal an error.

Note that the class <place-and-time> could be defined in the time library. The compiler can deal correctly with classes that may straddle a sealed domain, if they are known in the library where the sealed domain is defined. It would also be valid to subclass <time> in any way that did not change the applicability of methods in any sealed generic-function domains that include <time>. The actual rule involved depends on an analysis of the exact methods of the generic function, and the rule is complicated enough that you should just rely on your compiler to detect illegal situations.