From: Derek D. <dr...@cs...> - 2002-09-17 14:41:25
|
The higher-order module extension to Standard ML provided in the Moscow ML 2.00 compiler is unsound. In particular, devious use of a combination of generative and applicative functors can result, as in the example given below, in the casting of an integer to a function, and a segmentation fault when that function is called. In this note we proceed to explain what the problem is, how it arises, how it might be overcome in Moscow ML, and how it can be avoided in the first place by a stronger theory of type generativity. The theory of higher-order modules developed in Chapter 5 of Claudio Russo's thesis, on which the Moscow ML extension is based, supports applicative functors *in place of* generative functors. Applicative functors return equivalent abstract types when applied to modules with equivalent type components, whereas generative functors return new abstract types at each application. The theory naturally includes a form of applicative functor signature as well, which is necessary to specify the argument signature of a higher-order functor. The Moscow ML extension, however, adds applicative functors and their signatures on top of SML and its generative functors. In our paper "A Type System for Higher-Order Modules" (available in expanded form at http://www.cs.cmu.edu/~dreyer/papers/thoms/full.pdf), we pointed out that the way the two forms of functors interact in Moscow ML is undesirable. Specifically, there is no restriction on the bodies of applicative functors, which means one can defeat the generativity of a generative functor by eta-expanding it into an applicative one. As a consequence, the invariants one can assume regarding type abstraction in generative functors are considerably weaker than in SML proper. By itself, the weakness of Moscow ML's generative functors in the presence of applicative functors is not unsound. The unsoundness arises (in a variant of the eta-expansion problem) because Moscow ML also includes a form of generative functor *signature*, which is not present in SML and has no formal basis in Russo's thesis. We now describe our example of the unsoundness step-by-step (the full code is given in one piece at the bottom of this note). First, we define two generative functors F1 and F2, both of signature UNIT -> S but with different implementations of the abstract type t in S: signature S = sig type t val x : t val f : t -> unit end signature UNIT = sig end structure Unit = struct end functor F1 (X:UNIT) :> S = struct type t = int val x = 12 fun f x = () end functor F2 (X:UNIT) :> S = struct type t = unit -> unit fun x () = () fun f g = g () end We now define an *applicative* functor Apply, which takes a *generative* functor F (of signature UNIT -> S) as its argument and applies it to Unit. In Moscow ML, applicative functors and their signatures are differentiated from generative ones by omitting the parentheses around the argument: signature ARG = sig functor F : functor (X:UNIT) -> S end functor Apply Arg:ARG = Arg.F(Unit) Moscow ML returns the following signature for Apply: > New type names: t functor Apply : {functor F : {}->?t/1.{type t = t/1, val x : t/1, val f : t/1 -> unit}} -> {type t = t, val f : t -> unit, val x : t} The unsoundness is already foreseeable in this signature: every instantiation of Apply will yield a module whose type component is equal to the same abstract type "t", regardless of what the functor argument F is. In particular, if we instantiate Apply with F1 and F2, the resulting type components are both deemed equal to "t", but underneath the abstraction the types are really different: structure Res1 = Apply(struct functor F = F1 end) structure Res2 = Apply(struct functor F = F2 end) val breakit = Res2.f(Res1.x) (* core dump ensues *) To understand why Apply is given an unsound signature, it is necessary to understand where type abstraction occurs in generative vs. applicative functors. Generative functors generate new abstract types every time they are applied; however, the definition of a generative functor itself does not create any new abstract types. Applicative functors are the opposite: new abstract types are created at the *definition* of an applicative functor, corresponding to the abstract types in the result. These types must be parameterized (or "skolemized") over the abstract types in the functor argument (if there are any), so that the applicative functor will return equivalent abstract types in the result only for equivalent arguments. Once these abstract type *constructors* have been created, the functor can be given a fully transparent signature that refers to the constructors. The problem, then, with the Apply functor above is that its argument is a generative functor, which has no abstract type components itself, and so the abstract type t in the result of Apply is not skolemized over anything. Another way of saying this is that all generative functors of a given signature (e.g. UNIT -> S) are considered trivially to have equivalent type components, since they have none. That's why every application of Apply produces the same type "t". One way to fix the unsoundness would be to drop generative functor *signatures* from Moscow ML and to restrict generative functor definitions to top-level (as in SML, where functors are first-order). Apart from limiting the sorts of higher-order modules one can write, this modification would still suffer from the problem (discussed above) that generativity is weak in the presence of applicative functors, but we conjecture that the language would be sound. There may be other solutions as well. A better approach would be to strengthen the notion of generativity. In particular, as we have argued in our paper (cited above), generative functors should be treated as if they actually generate new and possibly different types at each run-time instantiation, even if such run-time type generation is not actually possible in the language. Now it is clearly only sound for a functor to be "applicative" if every application of the functor to the same argument produces the same type components in the result. Thus, under our interpretation of generativity, an applicative functor may not contain generative functor applications because they produce types whose definitions depend (in principle) on run-time conditions. In our type system, this semantics is achieved by treating generativity as a "dynamic effect" that is encapsulated by generative functors and released at their applications. Applicative functors are those whose bodies are free of dynamic effects. In particular, the Apply functor in the example above could not be considered applicative in our system because its body contains a generative functor application, which is effectful. (See our paper for further details and comparison with other related work.) As for the immediate problem of restoring the soundness of Moscow ML, however, it is not clear to us how this stronger account of generativity could be incorporated directly into Russo's formalism for higher-order modules, which underlies the Moscow ML implementation. -- Derek Dreyer, Karl Crary and Bob Harper -------------------------------------------------------------- Here's the unsoundness example in full: signature S = sig type t val x : t val f : t -> unit end signature UNIT = sig end structure Unit = struct end functor F1 (X:UNIT) :> S = struct type t = int val x = 12 fun f x = () end functor F2 (X:UNIT) :> S = struct type t = unit -> unit fun x () = () fun f g = g () end signature ARG = sig functor F : functor (X:UNIT) -> S end functor Apply Arg:ARG = Arg.F(Unit) structure Res1 = Apply(struct functor F = F1 end) structure Res2 = Apply(struct functor F = F2 end) val breakit = Res2.f(Res1.x) (* core dump ensues *) |