341 : 7 Feb 2002 : ML

Constructors

You probably have a particular idea of what a "constructor" is from C++. Forget what you know. In general, for any language, a "constructor" is an expression that creates a fresh value of a particular type. Languages commonly support two kinds of types:

  1. atomic types, which typically include scalars (numbers) and strings. Constructors for atomic types are typically just a syntactic representation of the atomic value: "hello", 1, 3.14159, etc.
  2. compound types, whose values are created by applying constructor functions to values of other types. In a properly orthogonal language, the types of a compound type's components may also be compound types. In this fashion, users can build up types of arbitrary complexity.

ML has built-in constructor functions corresponding to each of the built-in compound types:

x :: xs          (* List constructor: 2 components (head, tail) *)
(x, y, z)        (* Tuple constructor: 3 components (#1, #2, #3) *)
{x=p, y=q}       (* Record constructor: 2 components (#x, #y) *)

The square bracket constructor [a,b,c] is just "syntactic sugar" for the :: constructor form.

Patterns

    A pattern is either:
  1. a constant (i.e., an atomic constructor),
  2. a variable,
  3. a wildcard (the special _ pattern),
  4. a constructor applied to one or more patterns

Patterns are used on the "left-hand side" whenever a binding occurs. So far, we know of two places where bindings may occur:

  1. val bindings
  2. function arguments

For function arguments, the "left-hand side" is the formal parameter, and the "right-hand side" is the actual parameter.

Intuitively, "matching" allows us to perform an implicit computation when we bind a value. The computation attempts to answer the question: "Does the thing on the right hand side have a shape and value like the thing on the left-hand side?" (As an incidental bonus, we can bind designated pieces of the right-hand side to identifiers, for later use.)

If you think about this intuition for a while, you will see why it makes sense that constructors, and not arbitrary expressions, are allowed in a pattern: constructors unambiguously describe "shapes" of data. By contrast, operators like addition (op +) and list append (@) do not unambiguously describe the shape of their arguments. For example, if you have the list

   [1, 2, 3, 4]

that was constructed by appending two other lists x and y, you cannot precisely deduce the identity of x and y! On the other hand, if you know that the list above was constructed using ::, then you can unambiguously deduce the identity of the parts.

Random historical trivia: the idea of pattern-matching as a style of describing computation comes from Prolog and the family of logic programming languages.

An informal matching algorithm

Notice that patterns are recursive (case (iv) from the previous page). In determining whether two patterns "match", we can therefore use a recursive algorithm, in pseudocode:

fun matches(left, right) =
    if left is a constant then
        if left = right then true else false

    else if left is a variable then true

    else if left is a wildcard then true

    else if left is a compound constructor then
        if right has the same top-level constructor then
             if each component of left matches each component of right then
                 true
             else
                 false
        else
            false

    else (* left may not appear in a pattern! *)
        raise some kind of error

Exercises

Trace the execution of the pattern-matching algorithm over the following value bindings:

val 3 = 3;

val 3 = 4;


val _ = "walrus";


val (3+5, x) = (8, 9);



val x :: _ :: xs = [2,3,4];




val ({baz="hi", foo=seal}, fish::nil) = ({foo="bar", baz="hi"}, [827]);







Types

What makes a (static) type system "bad" or "good"?

Absurd type system #1

In this type system, it is impossible to perform any illegal operation on any value. Furthermore, it is impossible to mingle values of inappropriate type.

Obviously, this type system is useless, because it is not permissive enough. That is, we cannot express any interesting programs. Here is another type system:

Absurd type system #2

This type system is useless as well, but for a different reason: a programmer can write anything, including ridiculously incorrect programs. The type system is not restrictive enough to express any useful properties. (Some people wouldn't even call this a type system.)

Hence, one important aspect of "good":

A good type system is expressive: it allows the programmer to write interesting legal programs, but also to check interesting properties and restrictions for these programs.

Inference review and examples

At a very high level, here is the informal algorithm for determining types:

  1. Label every parameter and every value binding with a fresh type variable.
  2. Progressively refine polymorphic types by observing how their corresponding values are used, until no more information can be inferred.

Note that the second definition implies that "how values are used" produces constraints on the types of values. This statement, by itself, is fairly intuitive. The way that type constraints are produced can be called the inference rules of the system. Inference rules are commonly written in the following format:

   "If it rains, I will get wet."    "It rains."
----------------------------------------------------
                "I will get wet."                 

The "premises" of the rule are stated above the line, and the "conclusion" of the rule is below the line. We say that if the premises are true, then the conclusion is true. Here's a simple type inference rule:

 p:t0   x:t1   y:t2   t = t1 = t2   t0 = bool
---------------------------------------------
           (if p then x else y):t

This means that "if x is of type t1, and y is of type t2, then, when you see the expression (if p then x else y), assign it the type t, where t = t1 = t2. Also, unify the condition p with bool." In other words, we must unify the types of the two branches of the if/then/else, and the entire expression should in turn be unified with the resulting type.

The complete ML type system has many inference rules (and, actually, real type theorists use a rather different format for the rules). However, here are a few more in the same style as the above:

  x:t1   xs:t2 list    t1 = t2
--------------------------------
       (x::xs):t2 list             f:t0 -> t1   x:t2    t = t1   t0 = t2
                                  ----------------------------------------
                                                  f(x):t
     x:t1      y:t2
 ------------------------
     (x, y):t1 * t2
                                     x:t1    y:t2     t1 = t2 = int
                                   ----------------------------------
                                             (x mod y):int
  x:t1  y:t2   t1 = t2 = bool
 -----------------------------
     (x andalso y): bool

Essentially, every time you see an = sign for types, you should perform unification. Individually, the rules for unification are pretty simple. Using them in a deterministic algorithm that actually computes all the types for every expression in the program takes some effort; the algorithm for ML's type inference is due to Robin Milner. We'll never ask you to use Milner's algorithm, but you should be able to systematically construct an argument for the typing of a function.

Unification

What is unification? Actually, unification over types is a lot like pattern matching over values. There are various type constructors, like the tuple type constructor *, the function type constructor -> and the list type constructor list (which is a unary constructor, i.e. a constructor with only one argument: the list element type).

I won't go into unification in detail, but here are some sample unifications (note: this is NOT ML code, this is a series of examples of how types unify). Technically, the result of unification is a type variable substitution and a "unified type" for the entire expression; however, to save space, I'll write only the substitutions you need to perform to make the resulting types unify:

   unify('a -> 'b, 'c)     => 'c = 'a -> 'b

   unify('a * 'b, 'c list)    => ERROR, * type constructor cannot match list

   unify(('a * 'b) list, ('c * ('d * 'e -> 'f)) list)
       => 'b = ('d * 'e -> 'f)

   unify(('a -> 'b) * 'c, 'd * ('e list * 'f))
       => 'd = 'a -> 'b
          'c = 'e list * 'f

If you draw the type trees for these expressions, you'll see it looks a lot like the pattern matching algorithm---they're both just tree matching.

Inference examples

fun myIf(x, y, z) = if x then y else z;




fun squid(a, b, c) =
    let val d = [2.0,3.0];
        val e:(int * (int * string)) = (3, c);
    in
        if a > hd(d) then [hd(b), #2(c)] else tl(b)
    end;












Type synonyms and datatypes

Like most languages, ML allows you to define your own named types. There are two principal mechanisms for doing this: type synonyms, and datatypes (actually there are some elaborations on these themes, but we'll stick to these two).

Type synonyms simply allow you to say that a given name means the same thing as some other type name:

   (* A long and basically pointless type synonym *)
   type StringIntTuple = (int * string);

   (* Notice the syntax for declaring polymorphic synonyms *)
   type 'a chain = 'a list;
   type ('a, 'b) pair = ('a * 'b);

   (* Function types are some of the best candidates for synonyms. *)
   type MyWeirdType = (int * real * string list) -> (string * real);

Type synonyms are useful when you want to use the x:t notation for explicitly declaring types. They're also a form of documentation, and they have other uses when defining structures and signatures (which you'll learn about in the next few lectures).

More interesting, perhaps, are datatypes. A datatype declaration creates a brand new, fresh type, and a set of constructors for that type:

   (* General form of a datatype declaration *)
   datatype TypeName = Constructor1 of (type1 * type2 * ... * typeN)
                     | Constructor2 of (type1' * type2' * ... * typeN')
                     | ... ;

   (* A color datatype... 3 real-valued components, for red/green/blue *)
   datatype Color = RGB of real * real * real;

   (* A Color value *)
   val red = RGB(1.0, 0.0, 0.0);

   (* A "union" datatype... can wrap either an int or a string. *)
   datatype IntegerOrString = Int of int | Str of string;

   (* Two values of the above type... *)
   val x = Int(0);
   val y = Str("hi");

   (* A string tree datatype: either the empty tree, or a node with a
      string and two subtrees. *)
   datatype StringTree = Empty
                       | Node of string * StringTree * StringTree;

   val t = Node("hi", Empty, Node("bye", Empty, Empty));

   (* Datatypes also fill the role that "enums" fill in C... *)
   datatype States = Yes | No | Maybe;

Recall that constructors can be used in patterns. Datatype constructors behave exactly like the built-in constructors, except for a bit of syntax:

  (* Extracts the left subchild of a tree. *)
  fun leftChild(Empty) = raise Fail("Tried to get child of Empty.")
    | leftChild(Node(_, L, _)) = L;

Each datatype declaration is distinct; if you have to datatypes with identical cases, they are not the same type. Type theorists say that datatypes have "by-name type equivalence", whereas type synonyms have "structural type equivalence".

Mini-homework

Suppose you had a Color datatype defined as above. Define a function that, given a point in the real plane---i.e., a record of type {x:real, y:real}---returns a color that is black (i.e., RGB(0.0, 0.0, 0.0)) if the point is less than 2.0 units from the origin, and white (RGB(1.0, 1.0, 1.0)) otherwise.

This is a functional image: it is a function that defines the black circle centered at 0, with radius 2. Your ML project will employ functional images, so if you start to wrap your head around this idea now, you'll find the homework easier.

Now, define a function that takes two parameters: a foreground color, and a radius; and that returns a circle function with the given color and radius. This is a generator for functional images.


cse341-webmaster@cs.washington.edu
Last modified: Thu Feb 7 10:35:08 PST 2002