• No results found

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