structure strid :> sigexp = strexp
Transparent ascription is written using a single colon, “:”, whereas opaque ascription is written using “:>”.
Ascribed structure bindings are type checked as follows. First, we check thatstrexpimplements sigexpaccording to the rules given in chap- ter 19. The principal signaturesigexp0ofstrexpis determined, and checked
to matchsigexp. The matching process determines an augmentationsigexp0
ofsigexpin which we propagate type equations from the principal signa- ture, sigexp0, to sigexp0. Second, the structure identifier is assigned a sig-
nature based on the form of ascription. For an opaque ascription, it is assigned the signature sigexp; for a transparent ascription, it is assigned the signaturesigexp0.
Ascribed signature bindings are evaluated by first evaluating strexp. Then aviewof the resulting value is formed by dropping all components that are not present in the target signature, sigexp. The structure variable
strid is bound to the view. The formation of the view ensures that the components of a structure may always be accessed in constant time, and that there are no “space leaks” because of components that are present in the structure, but not in the signature.
20.2
Opaque Ascription
The primary use of opaque ascription is to enforce data abstraction. A good example is provided by the implementation of queues as, say, pairs of lists.
structure Queue :> QUEUE = struct
type ’a queue = ’a list * ’a list val empty = (nil, nil)
fun insert (x, (bs, fs)) = (x::bs, fs) exception Empty
fun remove (nil, nil) = raise Empty | remove (bs, f::fs) = (f, (bs, fs)) | remove (bs, nil) = remove (nil, rev bs) end
20.2 Opaque Ascription 160
The use of opaque ascription ensures that the type’a Queue.queue is ab- stract. No definition is provided for it in the signature QUEUE, and there- fore it has no definition in terms of other types of the language; the type ’a Queue.queueisabstract.
For the type’a Queue.queueto be abstract means that theonlyopera- tions that may be performed on values of that type areempty,insert, and remove. Importantly, wemay notmake use of the fact that a queue is really a pair of lists on the grounds that it is implemented this way. We have obscured this fact by opaquely ascribing a signature that does not provide a definition for the type ’a queue. All clients of the structure Queue are insulated from the details of how queues are implemented. Consequently, the implementation of queues can be changed without breaking any client code, as long as the new implementation satisfies the same signature.
Hiding the representation of a type allows us to isolate the enforcement of representation invariants on a data structure. We may think of the type ’a Queue.queue as the type of states of an abstract machine whose sole instructions are empty (the initial state), insert, and remove. Internally to the structure Queue we may wish to impose invariants on the internal state of the machine. The beauty of data abstraction is that it provides an elegant means of enforcing such invariants, called the assume-ensure, or rely-guarantee, method. It reduces the enforcement of representation invariants to these two requirements:
1. All initialization instructions must ensure that the invariant holds true of the machine state after execution.
2. All state transition instructions mayassumethat the invariant holds of the inputs states, and mustensurethat it holds of the output state. By induction on the number of “instructions” executed, the invariant must hold for all states—it must really be invariant!
Suppose that we wish to implement an abstract type of priority queues for an arbitrary element type. The queue operations are no longer poly- morphic in the element type because they actually “touch” the elements to determine their relative priorities. Here is a possible signature for priority queues that expresses this dependency:1
1Inchapter 21we’ll introduce better means for structuring this module, but the central
points discussed here will not be affected.
20.2 Opaque Ascription 161
signature PQ = sig
type elt
val lt : elt * elt -> bool type queue
exception Empty val empty : queue
val insert : elt * queue -> queue val remove : queue -> elt * queue end
Now let us consider an implementation of priority queues in which the elements are taken to be strings. Since priority queues form an ab- stract type, we would expect to use opaque ascription to ensure that its representation is hidden. This suggests an implementation along these lines:
structure PrioQueue :> PQ = struct
type elt = string
val lt : string * string -> bool = (op <) type queue = ...
. . . end
But not only is the type PrioQueue.queue abstract, so is PrioQueue.elt! This leaves us no means of creating a value of type PrioQueue.elt, and hence we can never call PrioQueue.insert. The problem is that the in- terface is “too abstract” — it should only obscure the identity of the type queue, and not that of the typeelt.
The solution is to augment the signature PQ with a definition for the typeelt, then opaquely ascribe this toPrioQueue:
signature STRING PQ = PQ where type elt = string structure PrioQueue :> STRING PQ = ...
Now the type PrioQueue.elt is equivalent to string, and we may call PrioQueue.insertwith a string, as expected.
The moral is that there is always an element of judgement involved in deciding which types to hold abstract, and which to make opaque. In the
20.3 Transparent Ascription 162
case of priority queues, the determining factor is that we specified only the operations on elt that were required for the implementation of priority queues, and no others. This means that elt could not usefully be held abstract, but must instead be specified in the signature. On the other hand the operations on queues are intended to be complete, and so we hold the type abstract.
20.3
Transparent Ascription
Transparent ascription cuts down on the need for explicit specification of type definitions in signatures. As we remarked earlier, we can always re- place uses of transparent ascription by a use of opaque ascription with a hand-crafted augmented signature. This can become burdensome. On the other hand, excessive use of transparent ascription impedes modular pro- gramming by exposing type information that would better be left abstract. The prototypical use of transparent ascription is to form a view of a structure that eliminates the components that are not necessary in a given context without obscuring the identities of its type components. Consider the signatureORDEREDdefined as follows:
signature ORDERED = sig
type t
val lt : t * t -> bool end
This signature specifies a type t equipped with a comparison operation lt.
It should be clear that it would not make sense to opaquely ascribe this signature to a structure. Doing so would preclude ever calling the lt operation, for there would be no means of creating values of typet. Such a signature is only useful once it has been augmented with a definition for the typet. This is precisely what transparent ascription does for you.
For example, consider the following structure binding:
20.3 Transparent Ascription 163
structure String : ORDERED = struct
type t = string val clt = Char.<
fun lt (s, t) = ... clt ... end
This structure implements string comparison in terms of character com- parison (say, to implement the lexicographic ordering of strings). Ascrip- tion of the signatureORDEREDensures two things:
1. The auxiliary functioncltis pruned out of the structure. It was in- tended for internal use, and was not meant to be externally visible. 2. The type String.tis equivalent tostring, even though this fact is
not present in the signature ORDERED. Transparent ascription com- putes an augmentation ofORDEREDwith this definition exposed. The “true” signature ofStringis the signature
ORDERED where type t = string
which makes clear the underlying definition oft.
A related use of transparent ascription is to document an interpreta- tion of a type without rendering it abstract. For example, we may wish to consider the integers ordered in two different ways, one by the stan- dard arithmetic comparison, the other by divisibility. We might make the following declarations to express this:
structure IntLt : ORDERED = struct
type t = int val lt = (op <) end
structure IntDiv : ORDERED = struct
type t = int
fun lt (m, n) = (n mod m = 0) end
20.4 Sample Code 164
The ascription specifies the interpretation of int as partially ordered, in two senses, but does not hide the type of elements. In particular, IntLt.t andIntDiv.tare both equivalent toint.
20.4
Sample Code
Hereis the code for this chapter.
Chapter 21
Module Hierarchies
So far we have confined ourselves to considering “flat” modules consist- ing of a linear sequence of declarations of types, exceptions, and values. As programs grow in size and complexity, it becomes important to intro- duce further structuring mechanisms to support their growth. The ML module language also supportsmodule hierarchies, tree-structured config- urations of modules that reflect the architecture of a large system.
21.1
Substructures
Asubstructureis a “structure within a structure”. Structures bindings (ei- ther opaque or transparent) are admitted as components of other struc- tures. Structure specifications of the form
structure strid : sigexp
may appear in signatures. There is no distinction between transparent and opaque specifications in a signature, because there is no structure to ascribe!
The type checking and evaluation rules for structures are extended to substructures recursively. The principal signature of a sub-structure binding is determined according to the rules given in chapter 19. A sub- structure binding in one signature matches the corresponding one in an- other iff their signatures match according to the rules inchapter 19. Eval- uation of a sub-structure binding consists of evaluating the structure ex- pression, then binding the resulting structure value to that identifier.
21.1 Substructures 166
To see how substructures arise in practice, consider the following pro- gramming scenario. The first version of a system makes use of a polymor- phic dictionary data structure whose search keys are strings. The signature for such a data structure might be as follows:
signature MY STRING DICT = sig
type ’a dict
val empty : ’a dict
val insert : ’a dict * string * ’a -> ’a dict val lookup : ’a dict * string -> ’a option end
The return type oflookupis’a option, since there may be no entry in the dictionary with the specified key.
The implementation of this abstraction looks approximately like this: structure MyStringDict :> MY STRING DICT =
struct
datatype ’a dict = Empty |
Node of ’a dict * string * ’a * ’a dict val empty = Empty
fun insert (d, k, v) = ... fun lookup (d, k) = ... end
The omitted implementations ofinsertandlookupmake use of the built- in lexicographic ordering of strings.
The second version of the system requires another dictionary whose keys are integers, leading to another signature and implementation for dictionaries.
signature MY INT DICT = sig
type ’a dict
val empty : ’a dict
val insert : ’a dict * int * ’a -> ’a dict val lookup : ’a dict * int -> ’a option end
21.1 Substructures 167
structure MyIntDict :> MY INT DICT = sig
datatype ’a dict = Empty |
Node of ’a dict * int * ’a * ’a dict val empty = Empty
fun insert (d, k, v) = ... fun lookup (d, k) = ... end
The ellided implementations ofinsertandlookupmake use of the prim- itive comparison operations on integers.
At this point we may observe an obvious pattern, that of a dictionary with keys of a specific type. To avoid further repetition we decide to ab- stract out the key type from the signature so that it can be filled in later. signature MY GEN DICT =
sig
type key type ’a dict
val empty : ’a dict
val insert : ’a dict * key * ’a -> ’a dict end
Notice that the dictionary abstraction carries with it the type of its keys. Specific instances of this generic dictionary signature are obtained us- ingwhere type.
signature MY STRING DICT =
MY GEN DICT where type key = string signature MY INT DICT =
MY GEN DICT where type key = int
A string dictionary might then be implemented as follows: structure MyStringDict :> MY STRING DICT =
struct
type key = string datatype ’a dict =
21.1 Substructures 168
Node of ’a dict * key * ’a * ’a dict val empty = Empty
fun insert (Empty, k, v) = Node (Empty, k, v, Empty) fun lookup (Empty, ) = NONE
| lookup (Node (dl, l, v, dr), k) =
if k < l then (* string comparison *) lookup (dl, k)
else if k > l then (* string comparison *) lookup (dr, k)
else v end
By a similar process we may build an implementation MyIntDict of the signature MY INT DICT, with integer keys ordered by the standard integer comparison operations.
Now suppose that we require a third dictionary, with integers as keys, but ordered according to the divisibility ordering.1 This implementation, say MyIntDivDict, makes use of modular arithmetic to compare opera- tions, but has the same signatureMY INT DICTasMyIntDict.
structure MyIntDivDict :> MY INT DICT = struct
type key = int datatype ’a dict =
Empty |
Node of ’a dict * key * ’a * ’a dict fun divides (k, l) = (l mod k = 0) val empty = Empty
fun insert (None, k, v) = Node (Empty, k, v, Empty) fun lookup (Empty, ) = NONE
| lookup (Node (dl, l, v, dr), k) =
if divides (k, l) then (* divisibility test *) lookup (dl, k)
else if divides (l, k) then (* divisibility test *) lookup (dr, k)
else
1For which,m<niffmdividesnevenly.
21.1 Substructures 169
v end
Notice that we required an auxiliary function, divides, to implement the comparison in the required sense.
With this in mind, let us re-consider our initial attempt to consolidate the signatures of the various versions of dictionaries in play. In one sense there is nothing to do — the signature MY GEN DICT suffices. However, as we’ve just seen, the instances of this signature, which are ascribed to particular implementations, do not determine the interpretation. What we’d like to do is to package the type with its interpretation so that the dictionary module is self-contained. Not only does the dictionary module carry with it the type of its keys, but it also carries the interpretation used on that type.
This is achieved by introducing a substructure binding in the dictio- nary structure. To begin with we first isolate the notion of an ordered type. signature ORDERED = sig type t val lt : t * t -> bool val eq : t * t -> bool end
This signature describes modules that contain a type tequipped with an equality and comparison operation on it.
An implementation of this signature specifies the type and the inter- pretation, as in the following examples.
(* Lexicographically ordered strings. *) structure LexString : ORDERED =
struct
type t = string val eq = (op =) val lt = (op <) end
(* Integers ordered conventionally. *) structure LessInt : ORDERED =
21.1 Substructures 170 struct type t = int val eq = (op =) val lt = (op <) end
(* Integers ordered by divisibility.*) structure DivInt : ORDERED =
struct
type t = int
fun lt (m, n) = (n mod m = 0)
fun eq (m, n) = lt (m, n) andalso lt (n, m) end
Notice that the use of transparent ascription is very natural here, since ORDEREDis not intended as a self-contained abstraction.
The signature of dictionaries is re-structured as follows: signature DICT =
sig
structure Key : ORDERED type ’a dict
val empty : ’a dict
val insert : ’a dict * Key.t * ’a -> ’a dict val lookup : ’a dict * Key.t -> ’a option end
The signatureDICTincludes as a substructure the key type together with its interpretation as an ordered type.
To enforce abstraction we introduce specialized versions of this signa- ture that specify the key type using awhere typeclause.
signature STRING DICT =
DICT where type Key.t=string signature INT DICT =
DICT where type Key.t=int
These are, respectively, signatures for the abstract type of dictionaries whose keys are strings and integers.
How are these signatures to be implemented? Corresponding to the layering of the signatures, we have a layering of the implementation.
21.1 Substructures 171
structure StringDict :> STRING DICT = struct
structure Key : ORDERED = LexString datatype ’a dict =
Empty |
Node of ’a dict * Key.t * ’a * ’a dict val empty = Empty
fun insert (None, k, v) = Node (Empty, k, v, Empty) fun lookup (Empty, ) = NONE
| lookup (Node (dl, l, v, dr), k) = if Key.lt(k, l) then
lookup (dl, k)
else if Key.lt (l, k) then lookup (dr, k)
else v end
Observe that the implementation of insert and lookup make use of the comparison operationsKey.ltandKey.eq.
Similarly, we may implementIntDict, with the standard ordering, as follows:
structure LessIntDict :> INT DICT = struct
structure Key : ORDERED = LessInt datatype ’a dict =
Empty |
Node of ’a dict * Key.t * ’a * ’a dict val empty = Empty
fun insert (None, k, v) = Node (Empty, k, v, Empty) fun lookup (Empty, ) = NONE
| lookup (Node (dl, l, v, dr), k) = if Key.lt(k, l) then
lookup (dl, k)
else if Key.lt (l, k) then lookup (dr, k)
else v
21.1 Substructures 172
end
Similarly, dictionaries with integer keys ordered by divisibility may be implemented as follows:
structure IntDivDict :> INT DICT = struct
structure Key : ORDERED = IntDiv datatype ’a dict =
Empty |
Node of ’a dict * Key.t * ’a * ’a dict val empty = Empty
fun insert (None, k, v) = Node (Empty, k, v, Empty) fun lookup (Empty, ) = NONE
| lookup (Node (dl, l, v, dr), k) = if Key.lt(k, l) then
lookup (dl, k)
else if Key.lt (l, k) then lookup (dr, k)
else v end
Taking stock of the development, what we have done is to structure the signature of dictionaries to allow the type of keys, together with its interpretation, to vary from one implementation to another. TheKeysub- structure may be viewed as a “parameter” of the signatureDICTthat is “in- stantiated” by specialization to specific types of interest. In this sense sub- structures subsume the notion of a parameterized signaturefound in some languages. There are several advantages to this:
1. A signature with one or more substructures is still a complete sig- nature. Parameterized signatures, in contrast, are incomplete signa- tures that must be completed to be used.
2. Any substructure of a signature may play the role of a “parameter”. There is no need to designate in advance which are “arguments” and which are “results”.
In chapter 23 we will introduce the mechanisms needed to build a generic implementation of dictionaries that may be instantiated by the key type and its ordering.
21.2 Sample Code 173
21.2
Sample Code
Chapter 22
Sharing Specifications
Inchapter 21we illustrated the use of substructures to express the depen- dence of one abstraction on another. In this chapter we will consider the problem ofsymmetric combinationof modules to form larger modules.
22.1
Combining Abstractions
The discussion will be based on a representation of geometry in ML based on the following (drastically simplified) signature.
signature GEOMETRY = sig
structure Point : POINT structure Sphere : SPHERE end
For the purposes of this example, we have reduced geometry to two con-