Recursive Structures for Standard ML

Claudio V. Russo Microsoft Research Ltd., St George House, 1 Guildhall Street, Cambridge CB2 3NH [email protected]

ABSTRACT both small and large programs. SML’s general-purpose Core Standard ML is a statically typed language supports “programming in the small” with a rich that is suited for the construction of both small and large range of types and computational constructs that includes programs. “Programming in the small” is captured by Stan- mutually recursive datatypes and functions, control con- dard ML’s Core language. “Programming in the large” structs, exceptions and references. is captured by Standard ML’s Modules language that pro- SML’s special-purpose Modules language supports “pro- vides constructs for organising related Core language defini- gramming in the large”. Constructed on top of the Core, tions into self-contained modules with descriptive interfaces. the Modules language allows sequential definitions of identi- While the Core is used to express details of algorithms and fiers denoting Core language types and terms to be packaged data structures, Modules is used to express the overall archi- together into possibly nested structures, whose components tecture of a software system. In Standard ML, modular pro- are accessed by the dot notation. Structures are transpar- grams must have a strictly hierarchical structure: the depen- ent:bydefault,therealisation (i.e. implementation) of a dency between modules can never be cyclic. In particular, type component within a structure is evident outside the definitions of mutually recursive Core types and values, that structure. Signatures are used to specify the types of struc- arise frequently in practice, can never span module bound- tures, by specifying their individual components. A type aries. This limitation compromises , component may be specified opaquely, permitting a variety forcing the programmer to merge conceptually (i.e. archi- of realisations, or transparently, by equating it with a partic- tecturally) distinct modules. We propose a practical and ular Core type ([10] uses the terminology abstract and man- simple extension of the Modules language that caters for ifest instead; we follow [8]). A structure matches asigna- cyclic dependencies between both types and terms defined ture if it provides an implementation for all of the specified in separate modules. Our design leverages existing features components, and, thanks to the subtyping relation called of the language, supports separate compilation of mutually enrichment, possibly more. A signature may be used to recursive modules and is easy to implement. opaquely constrain a matching structure. This existentially quantifies over the actual realisation of type components that have opaque specifications in the signature, effectively Categories and Subject Descriptors hiding their implementation. A functor definition defines a tag">D.3.3 [Programming Languages]: Language Constructs polymorphic function mapping structures to structures. A and Features—Modules,packages functor may be applied to any structure that realises a sub- type of the formal argument’s type, resulting in a concrete General Terms implementation of the functor body. Despite the flexibility of the Modules type system, it does LANGUAGES, THEORY suffer from an awkward limitation. Unlike the definitions of the Core language, module bindings must have a strictly Keywords hierarchical structure: the dependency between modules can recursive modules, datatypes, Standard ML, type theory never be cyclic. In particular, although definable within the confines of a single module, definitions of mutually recursive 1. INTRODUCTION types and values can never span module boundaries. While this does not affect the expressiveness of the Core language, Standard ML [12] (henceforth SML) is a high-level pro- the restriction does compromise modular programming. The gramming language that is suited for the construction of programmer is typically left with two choices. She can either merge conceptually distinct modules into a single module, just to satisfy the type checker. Or she can resort to tricky Permission to make digital or hard copies of all or part of this work for encodings in the Core language to break the cycles between personal or classroom use is granted without fee provided that copies are modules. The first approach obscures the architecture of the not made or distributed for pro£t or commercial advantage and that copies program; the second obscures its implementation. Neither bear this notice and the full citation on the £rst page. To copy otherwise, to solution is satisfactory. republish, to post on servers or to redistribute to lists, requires prior speci£ In this article, we relax the hierarchical structure of Mod- permission and/or a fee. ICFP’01, September 3-5, 2001, Florence, Italy. ules, allowing structures to be recursive. Our extension sup- Copyright 2001 ACM 1-58113-415-0/01/0009 ...$5.00.

50

ports mutual recursion at the level of Core datatypes and components, and possibly more. A structure matches asig- (independently) values. Although different in detail, our nature containing opaque type or datatype specifications if proposal may be seen as adapting Crary, Harper and Puri’s it enriches a complete realisation of that signature. theoretical analysis of module recursion to Standard ML [2]. Core expressions e describe a simple functional language Our main contribution is to provide a practical type system extended with the projection of a value identifier from a for recursive modules in Standard ML that avoids some of structure . Constructor applications and case expres- the typing challenges of the formalism in [2], and exploits sions are tagged with the name of the datatype (tp) that Standard ML’s subtyping on modules to provide slightly they introduce or eliminate. A constructor application takes more expressive and convenient constructs. Our extension the values of its n arguments and builds a tuple tagged with preserves the existing features of Standard ML. the constructor c, introducing a value of type tp. The typ- For presentation purposes, we formulate our extension, ing rules ensure that the constructor is fully applied. A case not for Standard ML, but for a representative toy language expression evaluates e to a constructed value of type tp and called Mini-SML. The static semantics of Mini-SML is based choosesacontinuationbasedonthetagofthisvalue.Kisa directly on that of Standard ML. Mini-SML includes the es- finite set of constructors used to index the set of alternative sential features of Standard ML Modules but, for brevity, continuations. Each alternative binds nc constructor argu- only has a simple Core language of explicitly typed, mono- ments xc,0 ···xc,nc−1 in the continuation body ec (typing morphic functions and non-parametric type and datatype will ensure that nc is the arity of c in the datatype). A case definitions. [14] treats a more realistic Standard ML-like expression need not be exhaustive, in which case evaluation Core with implicitly typed, polymorphic functions and pa- aborts by raising the built-in exception match. rameterised type definitions. Our proposal has been adapted A structure path sp is a reference to a bound structure to full Standard ML and is available in MoscowML [17]. identifier, or the projection of one of its substructures. A Section 2 introduces the syntax of Mini-SML. Sections 3 structure body b is a dependent sequence of definitions: sub- gives a motivating example to illustrate the limitations of sequent definitions may refer to previous ones. A type def- acyclic Modules. Section 4 reviews the static semantics of inition abbreviates a type. A datatype definition is like a Mini-SML. Section 5 defines our extension to recursive mod- datatype specification but generates a new(and thus dis- ules and its static and dynamic semantics. Section 6 recasts tinct) type with the corresponding set of constructors. As the first example so that its mutually dependent components in signatures, datatype replication declares a datatype that may be separately compiled. Section 7 presents a real-world is equivalent to, and thus compatible with, the datatype tp. example that uses recursive modules to implement an ad- Datatype replication is used to copy a datatype into an- vanced data structure. Section 8 assesses our contribution. other whilst preserving compatibility with that type. Value, recursive function and structure definitions bind term 2. THE SYNTAX OF MINI-SML identifiers to the values of expressions. A functor definition introduces a named function on structures: X is the func- Figure 1 defines the abstract syntax of Mini-SML. A type tor’s formal argument, S specifies the argument’s type, and s path tp is a projection t or sp.t of a (Core) type component is the functor’s body that may refer to X. The functor may from the context or a structure path. A core type umaybe be applied to any argument that matches S. A signature used to define a type identifier or to specify the type of a definition abbreviates a signature. A structure expression s Core value. These are just the types of a simple functional evaluates to a structure. It may be a path or an encapsulated language, extended with type paths. A signature body Bisa structure body, whose type, value and structure definitions sequential specification of a structure’s components. A type become the components of the structure. The application of component may be specified transparently, by equating it a functor evaluates its body against the value of the actual with a type, or opaquely, permitting a variety of realisations argument. A transparent constraint (s : S) restricts the (ie. implementations). The implementation of a transpar- visibility of the structure’s components to those specified in ent component is fixed (up to the realisation of any opaque the signature, which the structure must match, and reveals types that it mentions). An opaque datatype specification the actual realisations of all type components in the sig- describes an arbitrary (recursive) datatype with finite set of nature (even those with opaque specifications). An opaque constructors K. Each constructor c ∈ Kisspecifiedtotake constraint (s :> S) is similar, but hides the actual realisation nc (≥ 0) arguments of type uc,0 ···uc,n −1; nc is the con- c of type components with opaque specifications, introducing structor’s arity. In short, a datatype is a recursive, named newabstract types. sum of anonymous, possibly empty products The specifi- cation may be realised by any datatype with compatible constructors. Transparent datatype replication specifies a 3. MOTIVATING EXAMPLE datatype that is equivalent to, and thus compatible with, We can illustrate the limitations of the acyclic Modules the type tp (which must itself be bound to a datatype). language of Mini-SML (and Standard ML) by attempting Transparent types and datatype replication may be used to to implement mutually recursive functions over mutually re- express type sharing constraints in the usual way. Value and cursive datatypes. For more good examples, see [2]. structure components are specified by their type and signa- Suppose we wish to define evaluation functions for two ture. The specifications in a body are dependent in that mutually recursive types of natural number and boolean ex- subsequent specifications may refer to previous ones. A sig- pressions. Natural expressions include a conditional expres- nature expression S encapsulates a body, or is a reference sion, If, that tests a boolean condition; boolean expressions to a bound signature identifier. Informally, a structure en- include a predicate, Null, on naturals. For each sort of ex- riches (has a subtype of) a completely transparent signature pression, the evaluation function eval reduces an expression (ie. one containing no opaque type or datatype specifica- to normal form. tions) if it provides an implementation for all of its specified Figure 2 is what we would like to write in Mini-SML, but

51

Meta-variables t ∈ TypId, c ∈ ConId, x, f ∈ ValId, X ∈ StrId, F ∈ FunId and T ∈ SigId range over disjoint sets of type, constructor, value, structure, functor and signature identifiers. The meta-variable K ranges over finite sets of constructor identifiers. The meta-notation c∈K pc and i

Structure Paths sp ::= X | sp.X structure identifier, structure projection Structure Bodies

b ::= type t = u; b type definition | c,i datatype t=( c∈K c of i S opaque constraint

(Mini-SML supports local functor and signature definitions so that structure bodies can play the role of Standard ML’s separate top-level syntax. In SML, constructors can have a most one argument, with multiple arguments encoded as tuples — in Mini- SML, we support multiple arguments simply to avoid introducing separate syntax and rules for tuples. In SML, constructors are referenced by (long) value identifiers, with the constructor status (and associated datatype) recorded in the type of the identifier. To avoid formalizing this machinery, we simply treat constructors as uninterpreted tags, relying on explicit type annotations at constructor applications and case expressions to indicate membership of a particular datatype. Since we are only interested in cross-module recursion, datatype and function declarations are singly-recursive; support for mutual recursion is a by-product of our extension.)

Figure 1: Syntax of Mini-SML

52

structure Nat = struct 3 type. Finally, the type reference Bool.t againreferstoa datatype t = Zero | Succ of t 1 type defined in Bool, but, unlike the first reference, the fact |Ifof Bool.t *t*t that Bool.t is actually a datatype is crucial in this context, fun eval(n:t):t = case n : t of since we are eliminating a value of the datatype and need Zero => n to knowits constructors and their types. If Bool.t was | Succ m => Succ(eval m):t 2 3 an ordinary type abbreviation or an abstract type, the case |Ifbte=>case Bool.eval b: Bool.t of expression would fail to typecheck. True => eval t Suppose we now try to typecheck these forward references | False => eval e by throwing in some forward declarations. The first ref- end erence typechecks if we make a forward declaration that structure Bool = struct Bool.t is a type; the second typechecks if we make a forward datatype t = True | False | Null of Nat.t declaration that Bool.eval has type Bool.t -> Bool.t;the fun eval(b:t):t = case b : t of third (and its enclosing case statement) typechecks if we True => b make a forward declaration that Bool.t is the datatype | False => b datatype t = True | False | Null of Nat.t. | Null n => case Nat.eval n : Nat.t of Inspired by the similar constructs in [2], we propose to Zero => True:t extend the Modules language with two new constructs for | Succ m => False:t expressing such forward declarations. The first construct is end a newsignature expression, rec(X:S1)S2, called a recursive signature (similar to the recursively dependent signatures of [2]). It constructs a signature from the body signature S2, Figure 2: mutually recursive structures under the forward declaration of a structure X matching the signature S1. Recursive signatures are useful for spec- ifying mutually recursive types that span module bound- cannot. The code is rejected because of the three forward 1 aries within the signature. Formally, we will require that references to Bool (shown boxed) in the definition of Nat. the forward signature is enriched by (ie. a supertype of) the Permuting the structure definitions does not help. body, under some contractive (ie. non-circular) realisation In Standard ML, the easiest way to define Bool and Nat of the type components in the forward specification. The is to use two global, simultaneous definitions (joined with realisation identifies datatypes in the body with any of their and) of the datatypes and functions: forward declarations as opaque types; it ties the recursive datatype tNat = ... | If of tBool * tNat * tNat knot. The contractiveness condition forces each recursive and tBool = ... | Null of tNat type to be mediated by a datatype, which is consistent with fun evalNat(n:tNat):tNat = ... evalBool ... ordinary SML. Allowing for enrichment lets us get away with and evalBool(n:tBool):tBool = ... evalNat ... only giving forward specifications for those components that structure Nat = struct datatype t = datatype tNat are actually required in the body. val eval = evalNat end signature EVAL = structure Bool = struct datatype t = datatype tBool rec(X: sig structure Bool: sig type t end end) val eval = evalBool sig structure Nat: sig end datatype t = Zero | Succ of t | If of X.Bool.t *t*t In larger examples, this solution is unsatisfactory. It re- end quires the programmer to mangle the names of the types and structure Bool: sig functions and define them in a scope that encompasses both datatype t = True | False | Null of Nat.t modules, obscuring the program’s architecture. The solu- val eval: t -> t tion also impedes separate compilation, since most end do not allowsimultaneous definitions to be split across com- end; pilation units. Another solution is to give a forward declara- tion, ForwardNat, whose individual components are param- eterised by their forward references to Bool. Bool can then Figure 3: type recursion using a recursive signature be defined in terms of ForwardNat. We subsequently define Nat so that its components are the fixed-points of apply- With a recursive signature, we can specify the mutually ing ForwardNat’s components to the appropriate members recursive datatypes declared in Bool and Nat (Figure 3). of Bool. This solution is tedious, error-prone and inefficient, Notice that we only need a forward declaration of X.Bool.t, since the only way to represent Bool.t in ForwardNat.eval since the signature of Bool canmakeanordinary(backward) is as an abstract type, not a datatype. reference to Nat. The specification of Bool.eval in the body, It is instructive to analyse the forward references in Figure although superfluous here, will be used below. 1 2. The type reference Bool.t merely refers to a type The second construct, rec(X:S)s, is a newstructure ex- defined in Bool — the fact that Bool.t is a datatype with pression, called a recursive structure, that lets us construct a set of constructors is irrelevant since we are neither trying 1 to eliminate or introduce a value of the type. The term The term recursive signature is perhaps a misnomer, since 2 the forward declaration declares a structure, not a signature, reference Bool.eval refers to a value defined in Bool:the and allows us to take the a fixed-point of the datatypes only information we need to typecheck this reference is its specified in the body, not a fixed-point of the body itself.

53

structure Eval = rec(X:EVAL) Kinds, Type Variables, Variable Sets and Types struct structure Nat = struct datatype t = datatype X.Nat.t κ ∈ Kind ::= ∗ ranging over types fun eval(n:t):t = casen:tof |◦ ranging over datatypes def Zero => n ακ ∈ Var κ = {ακ,βκ,...} kinded type variables def | Succ m => Succ(eval m):t α ∈ Var = Var ∗ ∪ Var ◦ type variables |Ifbte=> ∈ def case X.Bool.eval b : X.Bool.t of P,Q VarSet =Fin(Var ) type variable sets True => eval t u ∈ Type ::= α type variable | False => eval e | u → u function space end structure Bool = struct Realisations datatype t = datatype X.Bool.t def fin fin ϕ ∈ Real = Var ∗ → Type ∪ Var ◦ → Var ◦ fun eval(b:t):t = caseb:tof True => b Constructor Environments and Type Structures | False => b def fin n | Null n => case Nat.eval n : Nat.t of K∈ConEnv =ConId→ n≥0 Type Zero => True:t ∈ | ◦ K | Succ m => False:t Φ TyStr ::= u (α , ) end Structures, Signatures, and Existential Structures end      fin   St ∪  St ∈ TypId → TyStr,  def  fin S∈Str = Sx ∪  S ∈ → Figure 4: term recursion using a recursive structure   x ValId Type ,   SX  fin  SX ∈ StrId → Str L∈Sig ::= ΛP.S a cyclic value for s, given a forward declaration of this value as the structure X, and a partial specification of its type S. X∈ExStr ::= ∃P.S Recursive structures are used to define mutually recursive values whose definitions span module boundaries within s. Functors and Contexts Formally, we will require that the forward declaration of a F∈Fun ::= ∀P.S→X recursive structure is enriched by the body. Allowing for     fin enrichment lets us get away with only giving forward dec-   Ct ∈ TypId → TyStr,   Ct ∪   larations for those components that are actually required   C ∈ →fin   CT ∪  T SigId Sig,  in the body. Opaque datatypes and types in the forward def  fin C∈Context = Cx ∪  Cx ∈ ValId → Type , declaration must be implemented in the body by datatype     CX ∪  C ∈ →fin  replication or an equivalent type abbreviation (respectively),  C  X StrId Str,   F  fin  and simply introduce newabstract types. CF ∈ FunId → Fun Nowwecan define the structures Bool and Nat as sub- structures of the recursive structure bound to Eval in Figure 3. The recursive structure is typechecked under the assump- Figure 5: semantic objects of Mini-SML tion that its body enriches the forward declaration of X (ie. that it matches the signature EVAL). The datatypes declared in EVAL are implemented by replicating them in the body. 4. STATIC SEMANTICS OF MINI-SML Since the function X.Bool.eval is declared in EVAL and the Before we can propose our extension, we need to review type X.Bool.t is declared as an appropriate datatype in the static semantics, or typing judgements, of Mini-SML. EVAL, the boolean case expression in Nat.eval typechecks. Following [12], our static semantics distinguishes syntac- Notice that the forward declaration only declares a subset tic types of the language from their semantic counterparts, of the body’s components: we omitted Nat.eval,sinceno called semantic objects. The semantic objects, defined in forward reference to it is required. Eval.Nat.eval will still Figure5,playtheroleoftypesinthesemantics.WeletO be accessible, since the type of the body, not the forward range over all semantic objects. declaration, determines the type of a recursive structure. Notation 1. For sets A and B,Fin(A) denotes the set of Operationally, a recursive structure is evaluated by eval- fin uating its body under the initial assumption that X is un- finite subsets of A,andA → B denotes the set of finite defined. If evaluation of the body attempts to evaluate X, maps from A to B.Letf and g be finite maps. D(f) execution aborts by raising the (new) exception ⊥ (an al- denotes the domain of definition of f.Thefinitemapf + g def ternative design is to enter a loop). If not, and evaluation has domain D(f) ∪D(g) and values (f + g)(a) =ifa ∈ of the body produces a value, we update the binding of X D(g)theng(a)elsef(a). Finally, if p ≡ i

54

over arbitrary semantic types, but variables of kind ◦ are Definition 1. Enrichment Relation restricted to range only over other (datatype) variables. Se- • Given two type structures Φ and Φ,Φenriches Φ, ∈  mantic types u Type are the semantic counterparts of syn- written Φ  Φ , if and only if tactic Core types, and are used to record the denotations of  type identifiers and the types of value identifiers. The sym- 1. Φ = Φ ;or bols Λ, ∃ and ∀ bind finite sets, P, of type variables. 2. Φ ≡ (α◦, K)andΦ ≡ α◦. A realisation ϕ ∈ Real maps type variables to semantic • S S S S types and datatype variables to datatype variables. It de- Given two structures and , enriches , written SS D S ⊇D S fines a substitution on type variables (and a renaming on , if and only if ( ) ( )and datatype variables) in the usual way. The operation of ap- 1. for all t ∈D(S), S(t) S(t); and plying a realisation ϕ to an object O is written ϕ (O). Re-   2. for all x ∈D(S ), S(x) = S (x); and alisations of datatype variables are restricted to renamings   to ensure that the set of type structures (below) is closed 3. for all X ∈D(S ), S(X) S(X). under realisation. Enrichment is a pre-order that defines a subtyping relation ∈ Type structures Φ TyStr record the denotations of type on semantic structures (SS means S is a subtype of S). components, and may either be a simple type u, arising from The relation allows entire components to be forgotten in the ◦ K an ordinary type specification or definition, or a pair (α , ) supertype, but also allows a datatype to be equated with an of a datatype variable and its constructor environment,aris- ordinary type component, by hiding its constructors. ing from a datatype specification or definition. K∈ConEnv is a finite map from constructors to tuples of argument types. Definition 2. Functor Instantiation Semantic structures S∈Str are used as the types of A semantic functor ∀P.S→Xinstantiates to a functor structure identifiers and paths. A semantic structure maps instance S →X, written ∀P.S→X > S →X,ifand type components to the type structures they denote, and only if ϕ (S)=S and ϕ (X )=X , for some realisation ϕ value and structure components to the types they inhabit. with D(ϕ)=P. S def For clarity, we define the extension functions t  Φ, = Definition 3. Signature Matching { → } S S def { → } S S S def t Φ + ,x:u, = x u + ,andX: , = A semantic structure S matches asignatureΛP.S if and { →S} S ∅ X + , and let S denote the empty structure . only if S  ϕ (S) for some realisation ϕ with D(ϕ)=P. A semantic signature ΛP.S is a parameterised type: it describes the family of structures ϕ (S), for ϕ a realisation of The static semantics of Mini-SML is defined by the judge- the parameters in P. Λ-bound type variables are introduced ments in Figures 6 and 7. Denotation judgements (C by datatype specifications and opaque specifications. p  O) relate type phrases to their denotations; classifi- The existential structure ∃P.S, on the other hand, is a cation judgements (C p:O) relate term phrases to their quantified type: variables in P are existentially quantified semantic types. We deviate from the presentation in the in S and thus abstract. Existential structures describe the Definition [12] by classifying structure expressions and bod- types of structure bodies and expression. Existentially quan- ies using existentially quantified semantic structures. The tified type variables are explicitly introduced by datatype Definition classifies structure expressions using bare seman- definitions and opaque constraints s :> S, and implicitly tic structures, but, to capture generativity of type defini- eliminated at various points in the static semantics. tions, uses a state from which to generate new type variables A semantic functor ∀P.S→Xdescribes the type of a . We prefer our presentation because it is stateless and thus functor identifier: the universally quantified variables in P more declarative ([6] uses a similar presentation). The pro- are bound simultaneously in the functor’s domain, S,andits cedural and declarative formalisations of the semantics are range, X . These variables capture the type components of explained in detail, and proved equivalent, in [14, 15]. We the domain on which the functor behaves polymorphically; only explain the additional rules concerning datatypes here. their possible occurrence in the range caters for the propa- Note that a constructor application or case expression is gation of type identities from the functor’s actual argument: well-formed only if the explicit type path tp denotes a type functors are polymorphic functions on structures. The range structure with a constructor environment, ie. a datatype. X c,i is the type of the functor body, that may introduce new (datatype t=( c∈K c of i

55

t ∈D(C) C sp : S t ∈D(S) X ∈D(C) C tp  Φ C t  C(t) C sp.t  S(t) C sp : S C X:C(X) ◦ C tp  u C tp  (α , K) C sp : S X ∈D(S) ◦ C u  u C tp  u C tp α C sp.X:S(X) C u  u C u  u C sp : S   C u → u  u → u C s:X C sp : ∃∅.S C u  u C, t  u B  ΛP.S C b:∃P.S t ∈D(S) P ∩V(u)=∅ C struct b end : ∃P.S C L C S B  (type t=u;B) ΛP.(t  u, ) C s:∃P.S P ∩V(C(F)) = ∅    α∗ ∈V(C) C, t α∗ B  ΛP.S t ∈D(S) α∗ ∈ P C(F) > S →∃Q.S SS Q ∩ P = ∅  C (type t; B)  Λ{α∗}∪P.(t α∗, S) C F(s) : ∃P ∪ Q.S  α◦ ∈V(C) C s:∃P.SC S  ΛQ.S ◦ ∩V S ∅SS D ∀c ∈ K. ∀i S) : Q. (datatype t=( c∈K c of i

56

5. THE EXTENSION hypothetical types cannot capture any types in the forward Formally, our extension requires two new syntactic con- declaration. The newtypes returned by the phrase are the structs, both additions to the Modules language: union of the newtypes in the forwarddeclaration and the newtypes of the body.  S ::= . . . | rec(X:S)S recursive signature Although not fully illustrated by our examples, Rule 1 allows a mixture of opaque and transparent type specifica- s ::= . . . | rec(X:S)s recursive structure tions in both the forward declaration and the body. Opaque Two new rules extend the Modules judgements C S  L type components in the forward declaration must be speci- and C s:X (the semantic objects are unchanged): fied in the body as either a non-circular transparent type, an (inherently non-circular) opaque type, or a potentially cir- cular datatype; transparent type components must simply C S ∩V C ∅ S  ΛP. P ( )= be implemented by equivalent types in the body (modulo C S  S ∩ ∪V S ∅ , X: S  ΛQ. Q (P ( )) = realisation by ϕ). (The side condition on the range of ϕ S  S D ∩ V ∅ ϕ ( ) ϕ ( ) (ϕ)=PP α∈P (ϕ (α)) = rules out the declaration of equi-recursive types - we only C (rec(X:S)S)  ΛQ.ϕ (S) support recursion through named, iso-recursive types. At (1) higher kinds (type constructors), this restriction appears to Rule 1 relates a recursive signature expression to its de- avoid the typing difficulties associated with equi-recursive notation, a semantic signature. The parameters P of the se- types, whose equivalence at higher kinds is not known to be mantic signature ΛP.S stem from opaque type and datatype decidable [2].) Unlike Rule 1, Rule 2 merely requires that the specifications in the forward declaration’s syntactic signa- type of the body enriches the forward declaration, without ture S. The denotation of the signature body, S,isdeter- allowing the further realisation of any opaque types in the mined in the current context, extended with the assumption forward signature. Omitting the realisation step is a design that X has type S. The side-condition P ∩V(C)=∅ pre- decision but is motivated by the principle that type equiv- vents the capture of free variables in C by the bound vari- alences valid outside the body should also be valid inside ables in P and ensures that these variables are treated as it: in particular, a forward reference to a type component parameters for the classification of S. The forward param- should be compatible with a backward reference to its cor- eters P, which may occur in S andthusinΛQ.S,must responding declaration in the body. be realised in a way that ensures the body enriches the for- Given a semantic signature ΛP.S, and a semantic struc-  ward specification. The realisation ϕ identifies type vari- ture S , the algorithm for matching the structure against ables stemming from opaque and datatype specifications in the signature is a straightforward adaptation of the folk- the forward signature with types specified in the body. Note lore two-pass algorithm (a simpler, one-pass variant, suitable that the variables in P may occur in S, so we apply the re- in the absence of recursive types, is described and proved alisation to both structures when checking enrichment; the correct in [14]). The first-pass of the algorithm is used to simultaneous realisation ties the recursive knot. The first construct a candidate realisation ϕ of the variables in P.  side condition on the domain of ϕ ensures that all forward The algorithm traverses the structure of S keeping S fixed. specifications are realised. The second side condition on the Each type variable α in P (and only those in P) is realised  range of ϕ ensures that the realisation is not circular, hence incrementally (by the corresponding type component in S ), contractive and unique. The parameters Q ofthebodyde- as we encounter its first occurrence in a type structure of termine the parameters of the entire signature ΛQ.ϕ (S). the form α or (α,K)inS (corresponding to the opaque or datatype specification that introduced α). The incremental realisation is applied to the current realisation of S before C S  ΛP.S P ∩V(C)=∅  resuming the traversal. After the construction of ϕ, the folk- C, X:S s:∃Q.S  lore algorithm conducts a second traversal of ϕ (S), to check Q ∩ (P ∪V(S)) = ∅SS  that the original S enriches ϕ (S). Our algorithm, however,  C (rec(X:S)s) : ∃P ∪ Q.S (2) must also be able to check the enrichment condition in Rule 1, namely ϕ (S)  ϕ (S)forsomeϕ with D(ϕ)=P where Rule 2 relates a recursive structure expression to its type. ϕ is not circular (P ∩ V(ϕ (α)) = ∅). Observe that If the forward declaration’s signature S denotes ΛP.S,the α∈P enrichment must hold under a simultaneous realisation of body s of the structure expression is classified in the ex-  not only S, but also S , which is different from the ordi- tended context C, X:S. The side-condition P ∩V(C)=∅  nary signature matching problem where S is assumed not prevents the capture of free variables in C by the bound to mention any of the variables in P. Our solution is to variables in P and ensures that these variables from the use a modified signature matching algorithm, that, during forward declaration are treated as new types for the clas- the first traversal of S, performs an occur check at each re- sification of s. During this classification, the constructors alisation step, preventing variables in P from appearing in of any datatypes and the types of any values specified in S the range of ϕ. In addition, before resuming the traversal, are known. Since S is intended as a forward declaration of the incremental realisation is applied not only to the cur- the types and values in s, the rule requires that the type   rent realisation of S, but also of S . The second pass of of s, ∃Q.S , enriches the forward declaration. In particular,  the algorithm simply checks that ϕ (S )  ϕ (S). Note that this checks that any forward declared values that might be the algorithm is very similar to unification. The algorithm referenced in s are actually defined with the correct type in and its correctness proof will appear in a future technical s (when the value of s is defined). Note that s may itself report. It easily scales to cope with Standard ML’s type declare some newtypes Q. Before checking that the type constructors (parameterised types). of the body enriches the forward declaration, we eliminate We can present the dynamic semantics of recursive struc- the existential quantification over Q, ensuring that these

57

∈D E E tures as a simple extension of a sketched call-by-value se- X ( ) (X) = V  (1) mantics for Mini-SML. The Mini-SML semantics is intended E, H sp ↓ R, H E, H X ↓ V, H to model the semantics of Standard ML. Our presentation X ∈D(E) E(X) = ll∈D(H) H(l)=V follows the style of the Definition [12]. The dynamic objects (2) used during evaluation consist of: E, H X ↓ V, H • ∈ X ∈D(E) E(X) = ll∈D(H) H(l)=⊥ asetofexceptions ex Exn (distinct from values) that (3) includes the existing exception match raised by the at- E, H X ↓⊥, H tempted evaluation of a missing case alternative, and E H ↓  H ∈D   ⊥ , sp V , X (V )V(X) = V a newexception , raised by a premature reference to  (4) an undefined recursive structure identifier. E, H sp.X ↓ V, H E H ↓ H • asetofcore values v ∈ CorVal that includes recursive , sp ex,  (5) function closures and applications of n-ary construc- E, H sp.X ↓ ex, H tors to values (whose form we shall leave unspecified). . E, H s ↓ R, H . • asetofstructure values, mapping value and structure identifiers to values: E H ⊥ ↓ H ∈D H  ( , X=l), ( ,l = ) s V, l ( )  fin  (6) def Vx ∪  Vx ∈ ValId → CorVal E, H rec(X:S)s ↓ V, (H ,l =V) V ∈ StrVal =  fin VX   VX ∈ StrId → StrVal (E, X=l), (H,l = ⊥ s ↓ ex, H ) l ∈D(H) (7) E, H rec(X:S)s ↓ ex, H • asetofcore results r ∈ CorRes ::= v | ex to cap- ture the values returned or exceptions raised by Core F ∈D(E) E(F) = evaluation. E, H s ↓ V, H (E, X=V), H s ↓ R, H  (8) • asetofstructure results R ∈ StrRes ::= V | ex E, H F(s) ↓ R, H to capture the values returned or exceptions raised by F ∈D(E) E(F) = E, H s ↓ ex, H Module evaluation. (9) E, H F(s) ↓ ex, H • asetoffunctor closures ∈ FunVal. Figure 8: evaluation judgements • a infinite set of heap locations l ∈ Loc, distinct from StrVal. • a set of finite heaps mapping locations to structure bind structure identifiers to values, not locations. Evaluat- values or the distinguished undefined element ⊥: ing a structure identifier that refers to a functor argument or structure declaration returns the value bound to that iden- def fin H∈Heap =Loc→ StrVal ∪{⊥} tifier in the environment (Rule (1)). If the identifier refers to a recursively bound identifier, which is necessarily bound • a set of environments E∈Env mapping value iden- to a location, we dereference that location in the heap, and tifiers to core values, structure identifiers to structure return the stored value (Rule (2)), or raise the exception ⊥ if values or locations and functor identifiers to closures: the location contains ⊥ (Rule (3)). Evaluating a projection      fin  either projects a value from the path’s value, or propagates  Ex ∪  Ex ∈ ValId → CorVal  def  fin an exception (Rules (5) and (6)). In an implementation, E∈ EX ∪  E ∈ → ∪ Env =   X StrId StrVal Loc we can distinguish references to recursive and non-recursive  EF  fin  EF ∈ FunId → FunVal bindings statically, so there is no need for a run-time test to distinguish applications of Rules (2) or (3) from Rule The dynamic semantics is defined by environment based (1). This is important because it ensures that the compi- evaluation judgements (Figure 8). Each judgement form re- lation of references to non-recursive bindings, that occur in lates an environment, initial heap and expression to a pair, ordinary Standard ML programs, is not penalised by our consisting of a result (a value or an exception) and a possi- extension. Since we evaluate the bodies of recursive struc- bly updated heap. The omitted judgements E, H e ↓ r, H tures eagerly, and only once, the execution order of their E, H b ↓ R, H for Core expressions and structure bodies side-effects (if any) is completely deterministic: this means are a straightforward adaptation of the existing Standard that it is straightforward to extend Mini-SML with the im- ML rules, modified to propagate a heap in the same way pure features of ordinary SML (references, exceptions and that Standard ML’s evaluation rules already propagate a I/O) that rely on a fixed evaluation order. Note that functor store (in an implementation, the store can simply be re-used application remains call-by-value (Rules (8) and (9)). for the heap). Recursive structure expressions are evaluated Although we do not attempt it here, we should be able by choosing a newlocation in the heap, initialized to ⊥.If to prove type soundness for (an instrumented version of) evaluation of the recursive structure’s body yields a value, this semantics using the technique of Tofte [18] and its re- we update the heap and return that value (Rule (6)). If it finement by Elsman [6]. The main technical challenge in yields an exception, perhaps because of a premature eval- proving the type soundness result has less to do with the uation of X, we simply propagate the exception (Rule(7)). presence of the heap, which is similar to ML’s store and Although we have omitted many of the rules, as in Standard should succumb to known proof techniques. The difficulty ML, structure declarations and functor applications always lies with ML’s treatment of datatypes as named recursive

58

types. Although type generativity ensures that each new Ideally, we would like to separately compile the defini- datatype is assigned a unique type variable, a priori, there tions of Bool and Nat as functors, each parameterised by is nothing in the static semantics that prevents an existing the implementation of the other (Figure 9), and then take datatype variable from being associated with more than one their fixed point in a separate compilation unit. The func- constructor environment in the context, making it impos- tors BoolFun and NatFun are completely independent and sible to deduce the structure of a constructed value from can be compiled separately (the common signature EVAL is its type. Type soundness can still be proved modulo an in- a convenient abbreviation only, and could be in-lined, and terpretation of datatype variables as recursive types that is indeed made smaller, at each occurrence). consistent with each type structure in the context (cf. [6]). The difficulty arises when we try to take the naive fixed The proof will appear in a future technical report. point of the two functor applications: structure Eval = rec(X:EVAL) struct 6. SEPARATE COMPILATION structure Nat = NatFun(X) It is a simple exercise to showthat the definition of the structure Bool = BoolFun(X) recursive signature and structure in Figures 3 and 4 type- end check. Although the definition of Eval contains two mutu- ⊥ ally recursive structures, it is not clear that we have gained This definition of Eval typechecks, but raises ,sincethe very much. We have obtained the desired modular struc- body of Eval attempts to evaluate its forward declaration ture, but the substructures are still defined simultaneously, before converging to a value (functor application is strict). and, in their present form, cannot be typechecked or com- structure Eval = rec(X:EVAL) struct piled in isolation. Support for separate compilation is one structure EtaX = struct (*an eta-expansion of X*) of the main motivations for introducing a module system. structure Nat = struct If the cost of programming with recursive modules is that datatype t = datatype X.Nat.t their substructures must be defined in a single compilation val eval = λn:t. X.Nat.eval n unit, then, aside from better control of the , we end have not progressed much beyond the original restriction structure Bool = struct that confines recursive definitions to a single module. datatype t = datatype X.Bool.t (* unit EVAL.sig *) val eval = λb:t. X.Bool.eval b signature EVAL = end rec(X: sig structure Nat: sig type t end end structure Bool: sig type t end structure Nat = NatFun(EtaX) end) structure Bool = BoolFun(EtaX) sig structure Nat: sig end datatype t = Zero | Succ of t | If of X.Bool.t *t*t Figure 10: using eta-expansion to reach a fixed-point val eval: t -> t end Fortunately, we can still obtain the correct fixed point if structure Bool: sig we apply the functors to an eta-expansion of the forward datatype t = True | False | Null of X.Nat.t declaration (Figure 10). While awkward, and slightly less val eval: t -> t efficient than a direct definition (Figure 4), this solution end applies whenever the forward declared values have function end; types. Note that our primary goal was to support cross- (* unit NatFun.sml *) module recursive functions, which satisfy this criterion. functor NatFun(X:EVAL) = struct Another alternative, that lets us get away with the naive datatype t = datatype X.Nat.t definition and avoid eta-expansion, would be to change our fun eval(n:t):t = case n : t of dynamic semantics and bind all structure identifiers to lo- Zero => n | Succ m => Succ(eval m):t cations into the heap; this modification has the effect of |Ifbte=>case X.Bool.eval b : X.Bool.t of penalising all structure projections, not just those from re- True => eval t cursively bound structures, and reduces the performance of | False => eval e ordinary Standard ML programs. end; (* unit BoolFun.sml *) functor BoolFun(X:EVAL) = struct 7. ADVANCED EXAMPLE datatype t = datatype X.Bool.t Aside from modular programming, recursive modules also fun eval(b:t):t = case b : t of have applications in the implementation of advanced algo- True =>b|False => b rithms and data structures. Chris Okasaki’s excellent book | Null n => case X.Nat.eval n : X.Nat.t of [13] gives a pseudo code implementation of “bootstrapped Zero => True:t heaps” that requires recursive structures. Figure 11 con- | Succ m => False:t tains a simplified version of his construction. The example end compiles in MoscowML but relies on some features not for- malised in Mini-SML (pattern matching and higher-order, applicative functors [11, 14, 16]). The Bootstrap functor Figure 9: separately compiled functors takes an arbitrary primitive heap functor, F, and an ordered

59

signature ORD = sig type t val leq:t*t->bool end running time of both findMin and merge to 0(1) worst-case signature HEAP = sig time, assuming that the original heap F supports insert in structure Elem: ORD 0(1) and merge and findMin in 0(log n) worst-case time [13]. type heap val empty: heap 8. RELATED WORK AND CONCLUSION val insert: Elem.t * heap ->heap For presentation purposes, we restricted our attention to val merge: heap * heap -> heap an explicitly typed, monomorphic Core language, but the val findMin: heap -> Elem.t option extension scales to full Standard ML [15], whose Core lan- end guage supports parameterised types and polymorphic val- ues. Our extension is available in the current release of functor Bootstrap(F:functor O:ORD -> MoscowML [17]. Adding recursive structures to Standard HEAP where type Elem.t = O.t) ML immediately extends the language with (explicit) poly- (O:ORD): HEAP = struct morphic recursion, since a recursive function may call it- signature BOOT= rec(X:sig structure Elem:ORD end) self through a forward reference, that can be specified to sig structure Heap: sig have the required polymorphic type. In combination with type heap = App.heap where App = F(X.Elem) MoscowML’s first-class modules [16], recursive structures end may be used to define (syntactically heavy-weight) encod- structure Elem: sig ings of class-based objects with virtual methods, using func- datatypet=E|HofO.t*Heap.heap tors to capture code inheritance (but not subtyping). Much valleq:t*t->bool more compelling is the useful combination of MoscowML’s end higher-order functors with recursive structures, that we il- end lustratedinSection7. structure Boot = rec(X:BOOT) struct MoscowML’s implementation of recursive structures is structure Elem = struct unsophisticated. We currently make no attempt to avoid datatype t = datatype X.Elem.t dynamic checks, and the cost of accessing a forward decla- fun leq (H (x, _), H (y, _))= O.leq (x, y) ration is proportional to its depth in the forward signature. end We do, however, depart from the naive dynamic semantics structure Heap = F(Elem) by compiling enrichment as a coercion to a pruned structure end value: in particular, the heap allocated value for a forward structure Elem = O declaration is a pruned version of the structure body’s value, datatype heap = datatype Boot.Elem.t reducing the potential for space leaks. Another obvious opti- val empty = E mization would be to flatten the representation of this value fun merge(E, h)=h|merge(h, E) = h to support depth-independent, constant-time access for each | merge(h1 as H(x,p1), h2 as H(y,p2)) = component in the forward declaration. Although it is dif- if O.leq(x,y) then H(x,Boot.Heap.insert(h2,p1)) ficult to construct realistic benchmarks, the current cost of else H(y,Boot.Heap.insert(h1, p2)) calling a recursive function through a forward declaration fun insert(x,h) = merge (H(x,Boot.Heap.empty),h) at depth 0 appears to be approximately 60% more expen- fun findMin E = NONE | findMin (H(x,_)) = SOME x sive than an ordinary recursive call. Removing the dynamic end check for definedness (when safe) reduces the figure to 40%. Another possible optimisation is to represent the forward Figure 11: Okasaki’s “bootstrapped” heaps reference as a cell storing, not a tagged value, but a function that initially returns ⊥ and is updated with a function that returns the value of the forward declaration, thus caching type, O, and returns an improved implementation of heaps the result of the check. Replacing the dynamic checks by over O. The datatype Boot.Elem.t is the type of boot- calls to these functions may be less expensive. strapped heaps. A bootstrapped heap is either empty, E, The important observation that recursive signatures can or a node, H(x,p), consisting of a root element x of type be used to specify mutually recursive types that span mod- O.t and an F-constructed primitive heap p of bootstrapped ule boundaries, while recursive structures should be used to heaps. The root x caches the minimum element amongst all define mutually recursive values, appears in the paper by those bootstrapped heaps contained in the primitive heap Crary, Harper and Puri [2] (recently revisited in [3]). That p. Bootstrapped heaps are ordered by Boot.Elem.leq with work is more theoretical than ours, and presents recursive respect to their root values. Note the use of signature re- signatures and modules as an extension of the phase dis- cursion to construct the type Heap.heap from the recursive tinction calculus of [9]. The authors use pseudo Standard application F(X.Elem) (which relies on the assumption that ML syntax for their examples but leave the integration with F is applicative). With these data structures in place, the Standard ML to future work. Our work may be seen as a construction uses bootstrapped heaps to represent heaps of concrete design based on this proposal, but it does differ O.t elements as follows. To merge two heaps we insert the in some aspects. In [2], a recursive signature does not con- underlying bootstrapped heap with the larger root into the tain a forward signature: all types in the signature body underlying bootstrapped heap with the smaller root, using are considered to be mutually recursive and must be fully the insert operation on bootstrapped heaps (not heaps). transparent. We allowfiner control of the recursion in sig- To insert an element into a heap, we create a singleton heap natures and support opaque types in the signature body. from the element and merge it. The minimum element of a More importantly, in [2] the forward declaration in a recur- (non-empty) heap is just the root at the node of the under- sive module determines the type of the entire phrase, while lying bootstrapped heap. This construction improves the in our proposal the body of a recursive structure must only

60

enrich the forward declaration, but the full complement of SIGPLAN’99 Conf. on Programming Language its components will still be accessible from the expression as Design and Implementation, pages 50-63. a whole. Our construct is more practical for the program- [3] Dreyer, D., and Harper, R., and Crary, K. 2001. mer, who must only make a forward declaration for those Toward a Practical Type Theory for Recursive components that require forward references in the body; the Modules. TR CMU-CS-01-112, Computer Science, remaining components in the body will not be hidden by the Carnegie-Mellon University, March.. forward declaration, and need not be specified. Allowing for [4] Duggan, D., and Sourelis, C. 1996. Mixin the addition of ordinary subtyping to [2] does not remove modules. In Proc. ACM SIGPLAN International the distinction between our construct and theirs. Another Conference on ,pages difference is that the system in [2] statically rejects recursive 262-273, Philadelphia. modules that may be undefined, while we allow such mod- [5] Duggan, D., and Sourelis, C. 1998. Parameterized ules at the cost of an inexpensive run-time check, that must modules, recursive modules, and mixin modules. be performed only when recursion crosses a module bound- In Proc. ACM SIGPLAN Workshop on ML, pages ary. To allowfixed-points of module expressions other than 87-96, Baltimore. values, but at the same time guarantee definedness, [2] uses [6] Elsman, M. 1999. Program Modules, Separate a refined type system that draws a distinction between valu- Compilation, and Intermodule Optimisation. PhD able and possibly undefined expressions. This is a cleaner thesis. University of Copenhagen. solution, but we prefer to pay the cost of the run-time check [7] Flatt, M., and Felleisen, M. 1998. Units: cool rather than burden the Standard ML programmer with sup- modules for HOT languages. In Proc. ACM plying more accurate type information in signatures. An SIGPLAN’98 Conf. on Programming Language optimizing can still track valuability internally to Design and Implementation, pages 236-248. remove unnecessary heap indirections and dynamic checks. Duggan and Sourelis’s [4] “mixin modules”, although re- [8] Harper, R., Lillibridge, M. 1994. A type-theoretic approach to higher-order modules with sharing. In lated to this work, solve a more general problem: to allow the definition of individual ML functions and datatypes to 21st ACM Symp. Principles of Programming span module boundaries. Their system is an extension of Languages. SML-style Modules that allows datatypes, functions and ini- [9] Harper, R, and Mitchell, J. C., and Moggi, E. tialisation code defined in one mixin module to be extended 1990. Higher-order modules and the phase by constructors, match rules and further initialisation code distinction. In 17th ACM Symp. Principles of defined in another, using a newoperator called mixin com- Programming Languages position. Composition merges two mixins to produce a new [10] Leroy, X., 1994. Manifest types, modules, and mixin; a separate construct closes a mixin module to take separate compilation. In 21st ACM Symp. the fixed-point of its components. In [5], the authors propose Principles of Programming Languages, pages an additional construct that links a simultaneous declara- 109–122. ACM Press. tion of mixins containing unrelated, but mutually recursive, [11] Leroy, X., 1995. Applicative functors and fully components, supporting cross-module recursion of the kind transparent higher-order modules. In 22nd ACM considered here. In combination, these extensions are more Symp. Principles of Programming expressive than our own, but they also require more sig- Languages.ACM Press. nificant changes to Standard ML and its implementations. [12] Milner, R., and Tofte, M., and Harper, R., and The semantics of the linking construct are not spelled out MacQueen, D. 1997. The Definition of Standard in detail, making a comparison with our approach difficult. ML (Revised). MIT Press. More distantly related to our work are Flatt and Felleisen’s [13] Okasaki, C. 1998. Purely Functional Data “units” [7], a module language for Scheme that caters for Structures. Cambridge University Press. cyclicly dependent and possibly dynamically linked “units”: [14] Russo, C. V. 1998. Types For Modules. PhD although the authors present a typed version of the lan- Thesis, LFCS, University of Edinburgh. guage, they point out that it does not give a satisfactory [15] Russo, C. V. 1999. Non-Dependent Types For treatment of ML style type sharing, which we do support. Standard ML Modules. In 1999 Int’l Conf. on Closely related to that proposal are Ancona and Zucca’s Principles and Practice of Declarative [1] mixin modules, and Wells and Vestergaard’s [19] simi- Programming. lar module calculus. In those systems, a module may only [16] Russo, C. V. 2000. First-Class Structures for contain terms, not types. The goals of these systems are Standard ML. In Nordic Journal of , different from ours, but this simplification avoids the main 7(4):348, Winter 2000. difficulty with type checking recursive modules, namely ac- [17] Sestoft, P., and Romanenko, S., and Russo, C. V. commodating mutually recursive, cross-module type defini- 2000. MoscowML V2.00. tions, which is the focus of this paper. [18] Tofte, M. 1988. Operational Semantics and Polymorphic Type Inference. PhD thesis, 9. REFERENCES Computer Science, University of Edinburgh. [1] Ancona, D., and Zucca, E. 1999. A primitive [19] Wells, M., and Vestergaard, R. 2000. Equational calculus of mixin modules. In Proc. Principles and reasoning for linking with first-class primitive Practice of Declarative Programming,LNCSvol. modules. In Programming Languages & Systems, 1702 , pages 69-72, Berlin. 9th European Symp. Programming,LNCSvol. [2] Crary, K., and Harper, R., and Puri, S. 1999. 1782 , pages 412-428, Berlin. What is a recursive module? In Proc. ACM

61