18.2 Structures
18.2.2 Long and Short Identifiers
Once a structure has been bound to a structure identifier, we may access its components usingpaths, orlong identifiers, orqualified names. A path has the form strid.id.2 It stands for theid component of the structure bound tostrid. For example,Queue.emptyrefers to theemptycomponent of the structureQueue. It has type’a Queue.queue(note well the syntax!), stating that it is a polymorphic value whose type is built up from the unary type constructor Queue.queue. Similarly, the function Queue.insert has type ’a * ’a Queue.queue -> ’a Queue.queueandQueue.removehas type’a Queue.queue -> ’a * ’a Queue.queue.
Type definitions permeate structure boundaries. For example, the type ’a Queue.queueis equivalent to the type’a listbecause it is defined to be so in the structureQ. We will shortly introduce the means for limiting the visibility of type definitions. Unless special steps are taken, the def- initions of types within a structure determine the definitions of the long identifiers that refer to those types within a structure. Consequently, it is correct to write an expression such as
2Inchapter 21, we will generalize this to admit an arbitrary sequence ofstrid’s sepa-
rated by a dot.
18.2 Structures 149
val q = Queue.insert (1, ([6,5,4],[1,2,3]))
even though the list [6,5,4] was not obtained by using the operations from the structureQ. This is because the typeint Queue.queue is equiva- lent to the typeint list, and hence the call toinsertis well-typed.
The use of long identifiers can get out of hand, cluttering the pro- gram, rather than clarifying it. Suppose that we are frequently using the Queue operations in a program, so that the code is cluttered with calls to Queue.empty, Queue.insert, and Queue.remove. One way to reduce clut- ter is to introduce a structure abbreviation of the form structure strid =
strid0 that introduces one structure identifier as an abbreviation for an- other. For example, after declaring
structure Q = Queue
we may writeQ.empty,Q.insert, andQ.remove, rather than the more ver- bose forms mentioned above.
Another way to reduce clutter is toopenthe structureQueueto incorpo- rate its bindings directly into the current environment. Anopendeclaration has the form
open strid1 ... stridn
which incorporates the bindings from the given structures in left-to-right order (later structures override earlier ones when there is overlap). For example, the declaration
open Queue
incorporates the body of the structureQueueinto the current environment so that we may write justempty,insert, andremove, without qualification, to refer to the corresponding components of the structureQueue.
Although this is surely convenient, using openhas its disadvantages. One is that we cannot simultaneously open two structures that have a component with the same name. For example, if we write
open Queue Stack
where the structureStackalso has a component empty, then uses ofempty (without qualification) will stand forStack.empty, not forQueue.empty.
18.3 Sample Code 150
Another problem with open is that it is hard to control its behavior, since it incorporates theentirebody of a structure, and hence may inadver- tently shadow identifiers that happen to be also used in the structure. For example, if the structure Queue happened to define an auxiliary function helper, that function would also be incorporated into the current environ- ment by the declarationopen Queue, which may not have been intended. This turns out to be a source of many bugs; it is best to useopensparingly, and only then in aletorlocaldeclaration (to limit the damage).
18.3
Sample Code
Hereis the code for this chapter.
Chapter 19
Signature Matching
When does a structure implement a signature? The structure must pro- vide all of the components and satisfy all of the type definitionsrequired by the signature. Type components must be provided with the same number of arguments and with an equivalent definition (if any) to that given in the signature. Value components must be present with the type specified in the signature, up to the definitions of any preceding types. Exception components must be present with the type of argument (if any) specified in the signature.
These simple principles have a number of important consequences.
• To minimize bureaucracy, a structure may providemore components than are strictly required by the signature. If a signature requires componentsx, y, andz, it is sufficient for the structure to providex, y,z, andw.
• To enhance reuse, a structure may provide values with more general
types than are required by the signature. If a signature demands a function of typeint->int, it is enough to provide a function of type ’a->’a.
• Toavoid over-specification, a datatype may be provided where a type is required, and a value constructor may be provided where a value is required.
• Toincrease flexibility, a structure may consist of declarations presented in any sensible order, not just the order specified in the signature, provided that the requirements of the specification are met.
19.1 Principal Signatures 152
19.1
Principal Signatures
There is amost stringent, ormost precise, signature for a structure, called its
principal signature. A structure may be considered to match a signature ex- actly when the specified signature is no more restrictive than the principal signature of the structure. To determine whether a structure matches a sig- nature, it is enough to check whether the principal signature of that struc- ture is no weaker than the specified signature. For the purposes of type checking, the principal signature is the official proxy for the structure. We need never examine the code of the structure durng type checking, once its principal signature has been determined.
A structure expression is assigned a principal signature by a component- by-component analysis of its constituent declarations. The principal sig- nature of a structure is obtained as follows:1
1. Corresponding to a declaration of the form type (tyvar1,...,tyvarn) tycon = typ,
the principal signature contains the specification type (tyvar1,...,tyvarn) tycon = typ
The principal signature includes the definition oftycon. 2. Corresponding to a declaration of the form
datatype (tyvar1,...,tyvarn) tycon = con1 of typ1 | ... | conk of typk
the principal signature contains the specification datatype (tyvar1,...,tyvarn) tycon =
con1 of typ1 | ... | conk of typk
The specification is identical to the declaration. 3. Corresponding to a declaration of the form
1These rules gloss over some technical complications that arise only in unusual cir-
cumstances. SeeThe Definition of Standard ML[3] for complete details.
19.2 Matching 153
exception id of typ
the principal signature contains the specification exception id of typ
4. Corresponding to a declaration of the form val id = exp
the principal signature contains the specification val id : typ
where typis the principal type of the expression exp(relative to the preceding declarations).
In brief, the principal signature contains all of the type definitions, datatype definitions, and exception bindings of the structure, plus the principal types of its value bindings.
19.2
Matching
A candidate signature sigexpc is said tomatcha target signaturesigexpt iff sigexpchas all of the components and all of the type equations specified by sigexpt. More precisely,
1. Every type constructor in the target must also be present in the can- didate, with the same arity (number of arguments) and an equivalent definition (if any).
2. Every datatype in the target must be present in the candidate, with equivalent types for the value constructors.
3. Every exception in the target must be present in the candidate, with an equivalent argument type.
4. Every value in the target must be present in the candidate, with at least as general a type.
19.2 Matching 154
The candidate may have additional components not mentioned in the tar- get, or satisfy additional type equations not required in the target, but it cannot have fewer of either. The target signature may therefore be seen as aweakeningof the candidate signature, since all of the properties of the latter are true of the former.
The matching relation is reflexive—every signature matches itself— and transitive— ifsigexp1matchessigexp2andsigexp2matchessigexp3, then sigexp1 matches sigexp3. Two signatures are equivalent (in the sense of
Chapter chapter 18) iff each matches the other, which is to say that they are equivalently restrictive.
It will be helpful to consider some examples. Recall the following sig- natures fromchapter 18.
signature QUEUE = sig
type ’a queue exception Empty val empty : ’a queue
val insert : ’a * ’a queue -> ’a queue val remove : ’a queue -> ’a * ’a queue end
signature QUEUE WITH EMPTY = sig
include QUEUE
val is empty : ’a queue -> bool end
signature QUEUE AS LISTS =
QUEUE where type ’a queue = ’a list * ’a list
The signatureQUEUE WITH EMPTYmatches the signatureQUEUE, because all of requirements of QUEUE are met by QUEUE WITH EMPTY. The converse does not hold, because QUEUElacks the componentis empty, which is re- quired byQUEUE WITH EMPTY.
The signatureQUEUE AS LISTSmatches the signatureQUEUE. It is identi- cal toQUEUE, apart from the additional specification of the type’a queue. The converse fails, because the signature QUEUE does not satisfy the re- quirement that’a queuebe equivalent to’a list * ’a list.
19.2 Matching 155
Matching does not distinguish between equivalent signatures. For ex- ample, consider the following signature:
signature QUEUE AS LIST = sig type ’a queue = ’a list exception Empty
val empty : ’a list
val insert : ’a * ’a list -> ’a list val remove : ’a list -> ’a * ’a list val is empty : ’a list -> bool
end
At first glance you might think that this signature does not match the sig- nature QUEUE, since the components of QUEUE AS LIST have superficially dissimilar types from those inQUEUE. However, the signatureQUEUE AS LIST isequivalentto the signatureQUEUE with type ’a queue = ’a list, which matchesQUEUEfor reasons noted earlier. Therefore,QUEUE AS LISTmatches QUEUEas well.
Signature matching may also involve instantiation of polymorphic types. The types of values in the candidate may be more general than required by the target. For example, the signature
signature MERGEABLE QUEUE = sig
include QUEUE
val merge : ’a queue * ’a queue -> ’a queue end
matches the signature
signature MERGEABLE INT QUEUE = sig
include QUEUE
val merge : int queue * int queue -> int queue end
because the polymorphic type ofmergeinMERGEABLE QUEUEinstantiates to its type inMERGEABLE INT QUEUE.
Finally, a datatype specification matches a signature that specifies a type with the same name and arity (but no definition), and zero or more
19.2 Matching 156
value components corresponding to some (or all) of the value constructors of the datatype. The types of the value components must match exactly the types of the corresponding value constructors; no specialization is allowed in this case. For example, the signature
signature RBT DT = sig
datatype ’a rbt = Empty |
Red of ’a rbt * ’a * ’a rbt | Black of ’a rbt * ’a * ’a rbt end
matches the signature signature RBT =
sig
type ’a rbt
val Empty : ’a rbt
val Red : ’a rbt * ’a * ’a rbt -> ’a rbt end
The signature RBTspecifies the type’a rbtas abstract, and includes two value specifications that are met by value constructors in the signature RBT DT.
One way to understand this is to mentally rewrite the signatureRBT DT in the (fanciful) form2
signature RBT DTS = sig
type ’a rbt
con Empty : ’a rbt
con Red : ’a rbt * ’a * ’a rbt -> ’a rbt con Black : ’a rbt * ’a * ’a rbt -> ’a rbt
The rule is simply that avalspecification may be matched by aconspeci- fication.
2Unfortunately, the “signature”RBT DTSisnota legal ML signature!
19.3 Satisfaction 157
19.3
Satisfaction
Returning to the motivating question of this chapter, a candidate structure
implements a target signature iff the principal signature of the candidate structure matches the target signature. By the reflexivity of the match- ing relation it is immediate that a structure satisfies its principal signa- ture. Therefore any signature implemented by a structure is weakerthan the principal signature of that structure. That is, the principal signature is thestrongestsignature implemented by a structure.
19.4
Sample Code
Chapter 20
Signature Ascription
Signature ascription imposes the requirement that a structure implement a signature and, in so doing, weakens the signature of that structure for all subsequent uses of it. There are two forms of ascription in ML. Both re- quire that a structure implement a signature; they differ in the extent to which the assigned signature of the structure is weakened by the ascrip- tion.
1. Transparent, or descriptive ascription. The structure is assigned the signature obtained bypropagatingtype definitions from the principal signature to the candidate signature.
2. Opaque, or restrictiveascription. The structure is assigned the target signatureas is, without propagating any type definitions.
In either case the components of the structure are cut down to those spec- ified in the signature; the only difference is whether type definitions are propagated from the principal signature or not.
20.1
Ascribed Structure Bindings
The most common form of signature ascription is in a structure binding. There are two forms, the transparent
structure strid : sigexp = strexp