To build large systems, need to split code into separate chunks.
Separate name spaces to avoid name clashes.
Separation of specification from implementation to support abstraction.
Separate compilation of portions of the system to speed development.
Standard ML supports these with a module system comprised of structures, signatures, and functors.
Not specifically tied to functional programming.
Currently being revised for new version of SML.
Moscow ML supports only a simpler, file-based module system with no functors.
Structures and Signatures
Basic notion of a module is a collection of types, functions, and values grouped together into a named structure.
Each structure has a signature; in this case, an (anonymous) default one reported back by the system. The signature is a list of type declarations and typed value declarations. More later.
Referring to Structure Components To refer to the members (types and functions) of the structure from outside its definition, we use dot notation:
To make such references, Set must already have been defined.
If we're going to make lots of use of a particular structure, we can open it, allowing its members to be referenced without naming the structure.
Of course, this removes the benefit of each structure having a separate namespace, so open should be used sparingly. Since open behaves like a declaration, it can be used in a let or local, and this is generally preferable to a top-level use.
The SML standard library is organized as a set of tt structures some pre-opened at top level and others not:
We can refer to individual elements of these structures (whether already open or not) using the dot notation. We can also open any of them; note that re-opening something like Integer will destroy overloading.
Similarly, the New Jersey library is a further set of structures. Note that the names of files containing structure definitions don't actually matter, although by convention we usually put one structure in one file with a similar name. (Moscow ML differs.)
More on Signatures
A signature is a description of the contents (types, typed values) of a structure, but says nothing about the implementation of the structure.
Signatures can exist independently of structures, and are useful in their own right as a way of describing interfaces or specifications, e.g., for abstract data types.
Note that signatures can be named or anonymous.
Signatures (named or anonymous) can be used to control which parts of a structure definition are to be visible outside the structure body:
We can also use signature constraints to define multiple views of a single structure:
Rule for constraints: structure must contain everything required by signature, but may contain more.
The default signature for a structure exports everything.
The module system provides three possible levels of abstraction for datatypes:
If signature itself specified a datatype, the structure must have an identical datatype definition, and constructors are fully visible outside.
Abstraction Properties (Cont.)
If signature specifies a type, structure can implement this type in any way it likes, and constructors will not be visible outside, but name and properties of type will be (somewhat similar to a local declaration).
To make properties of type completely invisible, use the abstraction keyword in place of structure:
Generally, should use abstraction instead of abstype.
Often we'd like to write (and compile) code that uses a module specification (signature) even if no implementation (structure) of that specification yet exists. We can do this in ML using functors, which are just structures that are parameterized on other structures, values, or types.
For example, suppose we are writing a application involving sets, but don't want to commit to a particular implementation of the Set structure. We write:
This can be type-checked and compiled, even if there are no structures matching HiddenSet yet defined.
Later, we can write one or more implementations of HiddenSet:
Then we may choose to apply the functor to one (or both) of the HiddenSet implementations.
Functors are useful wherever abstraction at the module level is wanted.
Example: Recall the problem of implementing sets using ordered trees, where all set operations need to be parameterized by a ``less-than'' function.
One approach is to write the Set structure as a functor, parameterized by the underlying type and its lt predicate.
Parameterized Set Example
Core Language vs. Module Language
There is a close correspondence between the basic elements of core ML and the module language:
Not too wrong to think of structures as records of values, etc. (although modules can also have type components).
But modules are static entities; all module-level calculations are performed before the program is ``run.''
Functors are similar to C++ templates, but in most implementations
functor body is compiled just once; functor application can be
thought of as a link-time step, and functors as an elaborate
linking control language.