3.2 Terms
4.2.6 The Avoidance Problem
The type system for ML modules that I have presented in this chapter does not give a complete account of the ML module system, nor is it intended to. Rather, the goal of this type system is to capture what I believe are the most important and interesting aspects of the ML module system—sealing, functors and translucency—in an elegant type-theoretic framework. In addition, the type system illustrates that it is easy to support both total and partial functors, and both the basic and impure forms of sealing, within a single unified language design.
There are several features of ML that are difficult to account for directly in type theory but which will be dealt with formally in the language design of Part III using elaboration techniques. Most of these features are syntactic conveniences, whose practical importance should not be discounted but which are not very interesting from a type-theoretic point of view. As I discussed in Section 4.1.2,
one particularly important one is ML’s notion of signature matching, which is considerably more permissive than the signature subtyping judgment of this type system.
Another one, which is the subject of this section, is not so much a feature of ML as an issue that arises in a number of different guises and can be seen as affecting the typing rules for several different constructs in my type system. Recall that, for both second projections π2M and functor
applications Fτ(M), the submodule M must be projectible in order for the whole module to be considered well-formed. One way to address this restriction is to support a variant of each of these constructs that permits the module M to be non-projectible, even impure, at the expense of requiring a signature annotation. In other words, consider extending the language with the constructs (π2M : S) and (Fτ(M) : S), for both of which the principal signature will be S. These
annotated constructs are in fact already expressible as derived forms: π2(M) : S
def
= letX = M in (π2X : S)
Fτ(M) : S def= letX1= F in letX2= M in (X1τ(X2) : S)
In practice, however, the signature annotation may constitute an unacceptable amount of syntactic overhead. We would like to have some way of inferring the signature S.
Unfortunately, it is not always possible to do so. As the above derived forms illustrate, the problem boils down to the desire for an unannotated let-module construct. Suppose that we had such a construct, written let X = M1 in M2, with exactly the same typing rule as annotated let’s
(Rule 101). Then clearly the above derived forms, minus the signature annotations, would give us a way of encoding second projections and functor applications in which the constituent module M need not be projectible.
The difficulty comes in computing the principal signature of let X = M1 in M2. Say that the
principal signature of Mi is Si. The signature S2 may refer to the variable X. To construct a
principal signature for thelet, we need to find a minimal supersignature of S2 that avoids reference
to X. The “avoidance problem” [22, 45] is that such a minimal supersignature does not always exist. The same problem arises at the level of constructors and kinds as well: given a kind K that refers to a constructor variableα, there is not necessarily any minimal superkind of K that avoids α. For example, consider the kind K = (T→
s
(α))×s
(α), which refers to a variableαbound withkindT. One obvious superkind of K that avoidsαis (T→T)×T, but there are more precise ones. Specifically, for any type C of kindTthat does not mentionα, the kind Σβ:(T→T).
s
(β(C)) is anα-avoiding superkind of K. For different choices of C, however, the superkinds are incomparable, and there is no minimal one.
Going back to the original problem of allowing second projections and functor applications involving non-projectible modules, there is an alternative solution employed by Harper and Lil- libridge [28] in their module type system. The idea is to allow π2M and Fτ(M), even when M is
not projectible, so long as in the first case M can be given a non-dependent pair signature S1×S2,
and in the second case F can be given a non-dependent functor signature S1−→τ S2. In both cases,
the signature of the result is merely S2, avoiding any need to substitute a non-projectible module
for a variable. Nevertheless, Harper and Lillibridge’s type system still runs afoul of the avoidance problem. For instance, take the π2M case. The principal signature of M may be a dependent pair
signature S = ΣX:S1.S2. Computing the principal signature ofπ2M will thus require finding a min-
imal non-dependent supersignature of S. This is tantamount to finding a minimal supersignature of S2 that avoids mentioning Xc, which is precisely the avoidance problem.
Different dialects of ML handle the avoidance problem—or do not handle it—in different ways. The type system of this chapter sidesteps the avoidance problem by requiring signature annotations on let-modules and restricting arguments to functors and second projections to be projectible.
4.2. MODULES 79
# module type S = sig type t end module F = functor (X : S) ->
struct type u = X.t type v = X.t end module G = functor (X : S) ->
struct type u = X.t type v = u end
module AppF = F((struct type t = int end : S)) module AppG = G((struct type t = int end : S));;
(* Output of the Objective Caml 3.07+2 compiler *) module type S = sig type t end
module F : functor (X : S) -> sig type u = X.t and v = X.t end module G : functor (X : S) ->
sig type u = X.t and v = u end module AppF : sig type u and v end module AppG : sig type u and v = u end
Figure 4.12: Encoding of the Avoidance Problem in O’Caml
Shao’s type system makes similar restrictions, and these enable his language to support principal signatures [69].
On the other hand, both the Harper-Lillibridge “translucent sums” calculus [28] and Leroy’s “manifest types” calculus [42] do not attempt to work around the avoidance problem at all, and thus lack principal signatures. Objective Caml, based on Leroy’s work, also lacks principal signatures. Most of the time this does not cause serious problems, but on occasion it leads to unpredictable typechecking, as illustrated in the O’Caml code shown in Figure 4.12. Two functors F and G are defined that have equivalent, transparent principal signatures. Yet when the functors are applied to the same sealed module expression, the signatures of the results AppF and AppG differ rather arbitrarily, based on some purely syntactic discrepancy between the signatures ofFand G.
The semantics of Standard ML, as described by Harper and Stone [32], addresses the avoidance problem differently, and in such a way that avoids the unpredictability of O’Caml typechecking. SML interprets the unannotated let X = M1 in M2 as if it were the pair module hX = M1,M2i,
and then ensures via elaboration techniques that the second component of this pair is the only one visible from outside the let. Applications of functors to non-projectible modules are handled by first rewriting them in terms of let-expressions (via the encoding given earlier in this section) and then interpreting the let’s as pairs. (Second projections may be rewritten similarly, but SML chooses to only permit projections from paths.)
Do modules in SML have principal signatures? Yes and no. Under the Harper-Stone interpreta- tion of SML, SML modules are translated to internal-language (IL) modules, and these IL modules do have principal signaturesin the IL. However, the principal IL signature of an IL module does not necessarily correspond to any SML signature, so one cannot always write the principal signature of an SML module in SML itself. (As Shao would say, SML lacks “fully syntactic” signatures.) Returning to the example in Figure 4.12, if we were to write this code in SML, then AppF would receive the IL signature sig type u = ?.t and v = ?.t end, where ?.tstands for the abstract t component of the unnamed argument module. There is no way to write this signature in SML itself, but at least we can count on the fact thatAppF.u= AppF.v, which is not true in O’Caml.
There is a simple, reasonable tradeoff here: SML modules written without the use of unanno- tatedlet’s (and the other features encoded in terms of them) will have principal signaturesin SML, whereas modules that employ the more flexible let construct may not. The language I define in Part III follows the SML/Harper-Stone approach to dealing with the avoidance problem.