

In the last lecture I introduced structures and signatures, which form the basis of the SML module system. As useful as these are, however, it is functors which provide the glue that make the module system truely useful.
Consider the following: if functions did not take parameters, then they would have to do all their calculations on global variables, and would always produce the same results as long as the values of the variables were the same at the time the function was defined. In a similar way, structures can only depend on those type names and structure names already defined. Functors allow a structure definition to be given as a template, and instantiated to some appropriate parameters for an actual application.
Suppose we have the following signature for a lookup table:
(* A signature for string dictionaries *)
signature DICTIONARY =
sig
eqtype key
type def
type dict
exception Already_Defined of key;
exception Not_Defined of key;
val newdict : dict;
val lookup : dict -> key -> def option
val defined : dict -> key -> bool
val define : dict -> key -> def -> dict
val undefine: dict -> key -> dict
end;
|
One structure matching this signature is:
(* A structure for string-string dictionaries *)
structure StringStringDictionary : DICTIONARY =
struct
type key = string
type def = string
datatype dict = dict of (key * def) list
exception Not_Defined of key;
exception Already_Defined of key;
val newdict = dict nil;
fun lookup_aux nil _ = NONE
| lookup_aux ((key1,def1)::t) key =
if key1 = key
then SOME def1
else lookup_aux t key1
fun lookup (dict dct) key = lookup_aux dct key
fun defined dct key = not (lookup dct key = NONE)
fun define (dict' as (dict dct)) key def =
if (defined dict' key)
then raise (Already_Defined key)
else dict ((key,def)::dct)
fun undefine_aux nil key = raise (Not_Defined key)
| undefine_aux (dct as ((defn as (key1,def1))::t)) key =
if key1 = key
then t
else defn::(undefine_aux t key)
fun undefine (dict dct) key = dict (undefine_aux dct key)
end;
|
We could similarly define structures for any key/definition types as long as the key type is an equality type. Notice, however, that this very definition should work for all those cases. It would be silly to have to repeat it over an over. Can we just leave out the key and definition types and let the functions be polymorphic? No. The problem is that such a structure would not match the signature we were given.
The solution is to use a functor. Whenever we want to build an instance of this structure for a particular pair of types, we will just call the functor with the appropriate types.
The functor for this example is:
(* A functor that builds dictionary structures *)
functor Dictionary (eqtype K; type D) : DICTIONARY =
struct
type key = K
type def = D
datatype dict = dict of (key * def) list
exception Not_Defined of key
exception Already_Defined of key
val newdict = dict nil
fun lookup_aux nil _ = NONE
| lookup_aux ((key1,def1)::t) key =
if key1 = key
then SOME def1
else lookup_aux t key1
fun lookup (dict dct) key = lookup_aux dct key
fun defined dct key = case (lookup dct key)
of NONE => false
| SOME _ => true
fun define (dict' as (dict dct)) key def =
if (defined dict' key)
then raise (Already_Defined key)
else dict ((key,def)::dct)
fun undefine_aux nil key = raise (Not_Defined key)
| undefine_aux (dct as ((defn as (key1,def1))::t)) key =
if key1 = key
then t
else defn::(undefine_aux t key)
fun undefine (dict dct) key = dict (undefine_aux dct key)
end;
|
If we wish to use this functor to build a structure for dictionaries with integer keys and string definitions, we type:
structure IntStringDict = Dictionary(type K = int;
type D = string);
The syntax of functor headers and applications is, admittedly, a bit
strange. The semi-colons in between parameters are optional, but I
think they make it a little easier to read. Also, be aware that a
functor application (or a struct...end
block, for that matter) are syntactically allowed only on the right
hand side of a structure definition statement. You cannot have
unnamed structures.
Note that I changed the definition of defined slightly. Why?

A functor can be parameterized by a type, a value, or a structure. As an example of the last, consider that if it is possible to build dictionaries that are more efficient if we know that the key type supports an ordering function. One option would be to build the dictionary as a binary search tree. Here I will just show a version that uses ordered lists. The functor now needs to recieve the key and definition types as well as the ordering function for the keys. While these could be passed in separately, it makes sense to group the function with the key type. We will use the following signature for this bundle:
(* A signature for string dictionaries *) signature ORDERING = sig eqtype T val lt : T * T -> bool end; |
Now, we can build a functor for ordered dictionaries as:
(* A functor that builds sorted dictionary structures *)
functor SortedListDictionary (structure Ordering : ORDERING; type D) : DICTIONARY =
struct
type key = Ordering.T
type def = D
datatype dict = dict of (key * def) list
exception Not_Defined of key
exception Already_Defined of key
val newdict = dict nil
fun lookup_aux nil _ = NONE
| lookup_aux ((key1,def1)::t) key =
if key1 = key
then SOME def1
else if Ordering.lt (key1,key)
then lookup_aux t key1
else NONE;
fun lookup (dict dct) key = lookup_aux dct key
fun defined dct key = case (lookup dct key)
of NONE => false
| SOME _ => true
fun define_aux nil key def = [(key,def)]
| define_aux (dct as ((defn as(key1,def1))::t)) key def =
if key1 = key
then raise (Already_Defined key)
else if Ordering.lt(key1,key)
then defn::(define_aux t key def)
else (key,def)::dct
fun define (dict dct) key def = dict (define_aux dct key def)
fun undefine_aux nil key = raise (Not_Defined key)
| undefine_aux (dct as ((defn as(key1,def1))::t)) key =
if key1 = key
then t
else if Ordering.lt(key1,key)
then defn::(undefine_aux t key)
else raise (Not_Defined key)
fun undefine (dict dct) key = dict (undefine_aux dct key)
end;
|
Then, If we have the ORDERING:
(* A structure for the integer ordering *) structure IntOrder : ORDERING = struct type T = int val lt = Int.< end; |
we can use this functor to build a structure for fast dictionaries with integer keys and string definitions, by typing:
structure SortedIntStringDict =
SortedListDictionary(structure Ordering = IntOrder;
type D = string);

Suppose we are writing a functor that makes use of a DICTIONARY and
also made use of a QUEUE, which signature includes a queue
type and an entry type. At some point in the code an element is
taken off the queue and looked up in the dictionary.
The functor would look something like:
functor SomeFunctor (structure Dict : DICTIONARY and Queue : QUEUE) : SOME_SIG = struct ... fun lookUpHead dict queue = Dict.lookup dict (Queue.head queue); ... end
But does (or should) this typecheck?
The types of the two functions are:
val Dict.lookup : Dict.dict -> Dict.key -> Dict.def; val Queue.head : Queue.queue -> Queue.entry;
So, it all depends on the particular dictionary and queue that is sent
in to the functor. If Dict.key is the same type as
Queue.entry then we are fine. Otherwise, not.
We can tell the system that they will be the same, and thus that it should,
on application of the functor, check that they are the same, by adding a
sharing constraint to the functor header. In particular, the
functor becomes:
functor SomeFunctor (structure Dict : DICTIONARY and Queue : QUEUE
sharing type Dict.key = Queue.entry) : SOME_SIG =
struct
...
fun lookUpHead dict queue = Dict.lookup dict (Queue.head queue);
...
end
Here we have enforced a sharing constraint on types. In a similar manner, it is also sometimes necessary to require that two structures share some common substructure. In that case we just use a structure sharing constraint as in:
functor Foo (structure Struct1 : SIG1 and Struct2 : SIG2
sharing structure Struct1.sub1 = Struct2.sub2) : FOO_SIG =
struct
...
end

In discussing programming languages, one must first distinguish between the languages syntax, the rules that determine what strings of symbols are legitimate programs in the language, and its semantics, the rules that determine the meaning of a given string of symbols.
In general, it is the semantics that researchers are interested in. What grammar rules are chosen for a language are rarely very important. So, often when presenting examples, a Programming Languages person will pick some non-specific artificial syntax that is generic to the class of languages she is demonstrating some point about. They will not, for example, use C or Pascal syntax, but rather some generic block-structured-imperative-language syntax.
Nevertheless, to build an interpreter or compiler for a language, we must be in a position to analyze it's grammar. Therefore, for the next three or so lectures, we will be discussing the problem of parsing. That is, analyzing a string of symbols to see if it belongs to the language we are interested in, and converting it to some internal, more manageable form that we can hand to the interpreter/code-generator.
The most common way to specify the grammar of a language is in what is known as Backus-Naur Form or BNF. There are several different styles for writing BNF rules. This is a common one.
A partial BNF for ANSI C functions might be given as:
fundef ::= [type_id] id "(" [param_list] ")" block
block ::= "{" [local_list] stat_list "}"
param_list ::= [type_id] id
| [type_id] id "," param_list
local_list ::= type_id id_list ";" [local_list]
id_list ::= id
| id "," id_list
stat_list ::= statement ";" [stat_list]
We will discuss how to build and analyze grammars starting next class. For the rest of this lecture I want to discuss the difference between concrete syntax and abstract syntax and how they relate to your next assignment. Consider the following two functions:
fun fact x = if (x = 0)
then 1
else x * (fact (x - 1)) );
|
function swap( var x:integer, var y:integer) : integer;
var temp:integer;
begin
temp := x;
x := y;
y := temp;
end;
|
Does everyone recognize these languages and understand what the functions do?
Well, whatever you're thinking, you're almost certainly wrong. When I
wrote them I had in mind a rex program and a c program, and I argue that
that is exactly what they are, for any purpose that you could really
care about.

begin/end pairs for delimiting blocks. We
can describe the structure of a function much more analytically as:
function = Function(name : identifier, return : type,
parameters : var list, body : block)
var = Var(name : identifier, type : type)
block = Block(locals : var list, code : statement list)
Notice that this abstracts away many details. For instance, we no longer care that the parameter list for a function and the list of local variables are defined with different syntaxes.
The beauty of this idea (or perhaps the beauty of ML) is that it should be obvious that abstract syntax can be mimiced directly in ML datatypes. And ML programs can be easily built to analyze these structures.
So, for example, the C function:
int foo(int x)
{
x++;
return x;
}
could be represented by the ML value:
val foo = Function(Id "foo", Type "int", [Var (Id "x", Type "int")],
Block([],[Inc(Var (Id "x")),Return(Var (Id "x"))]));

|
|
This page copyright ©1999 by Joshua S. Hodas. It was built on a Macintosh. Last rebuilt on Monday, February 22, 1999 at 4:10 PM. |
http://cs.hmc.edu/~hodas/courses/cs131/lectures/lecture09.html | |