[Notes by Tibor Janosi] 1. Types Types are sets of values. We will use the following basic (primitive) types: int, real, bool, char, string. Note that types can't mix at all - we can't add and int and a real, nor can we consider a non-zero value as being a boolean true. Out of simple types, we construct complex types. Complex types we will consider here are tuples, lists, records, function types. We can also have custom types. - tuples: (), (3, "aha"). No 1-tuple exists, thus (((x))) is always equal to x. Tuples don't have to be homogeneous in type. The set operation corresponding to creating a tuple is cartesian product. Thus if x is in set X, y in set Y, then (x, y) will be in set X x Y. We can use the # operator to access elements of a tuple: #1 (3, "aha"), but we don't really like this notation. The empty tuple is called "unit." - lists: lists are like tuples, except they must be homogeneous in type: [], [3], [1, 2, 3]. Note that lists of length one exist. If the base type of a list is represented by set X, the list type is the asterate of the set X (the sum of all powers of X, where multiplication is the Cartesian product, X^0 being the empty set). - function types: in ML, as opposed to many other languages, functions are regular objects, we can perform computations with them, e.g. we can generate a composite function. We can have anonymous and named functions. A function defined on X with values in Y is really a subset of the cartesian product of X x (Y U {bottom}), with the additional restriction that for each x in X there is only one element of Y U {bottom} with which it is paired up (i.e. function values are uniquely determined, given the argument). "Bottom" is usually denoted by an inverted T, and it represents a computation that does not finish. - record types: similar to tuples, but with named fields: {}, {x = 3, y = "aha"}. Note that an empty record is treated as being the empty tuple ("unit"). If x is in X, y is in Y, then record { x = ..., y = ... } is an element of the set { (x, "x") | x in X } x { (y, "y") | y in Y }. For most practical purposes records are like tuples, with some notational convenience. We can access records using field names: #first { first="Bart", last="Simpson" }. The order in which we specify record fields is irrelevant, thus: { first="Bart", last="Simpson" } is equal to { last="Simpson", first="Bart" }. - char and string types: #"a" and "this is a string". - custom types: can be just named symbolic constants, containers for separate underlying types (these are "union types"), recursive types. datatype order = LESS | GREATER | EQUAL (predefined in the standard library) datatype BAG = INT of int | REAL of real | BOOL of bool datatype LIST = EMPTY | CONS of int * LIST 2. Functions and Type Inference Consider fn x => x + 3. What is the type of this expression? Because 3 is int, and only ints can be added to ints, SML infers that x must be int. Thus this function does need an integer argument, and computes (returns) and integer value. We usually use fully annotated functions: fn x:int => x + 3, or (fn x:int => x + 3):int. This provides for an added level of checking by SML. The type of this function is int -> int. Sometimes we need to return more than one result from a function, but functions only return one value. Tuples save the day: fn (x: int, y: int) => (x + y, x - y). Functions can return functions: fn (x:int) => fn (y: int) => x + y. Parantheses can help us understand this better: fn (x:int) => (fn (y: int) => x + y)). The type of this expression is int -> int -> int: "a function with one integer argument that returns a function that takes one integer argument and returns an integer." What is the meaning of (fn (x:int) => fn (y: int) => x + y) (3)? It produces a function (fn (y:int):int = 3 + y). Note that the value of x is "set" to 3 in the result. The substitution model will provide a more formal explanation. What is the difference between the previous function and fn (x: int, y: int) => x + y? Well, in the former case we can compute a value when only one argument is available (x), while in the former case we need to have both arguments at hand to evaluate the function. A function in the second form can be transformed into a function in the first form by a process called "currying" (more about this later in the course). 3. New SML statements: fun, let, case. Pattern matching. let val x: LIST = CONS(3, EMPTY) in case x of EMPTY => true | CONS(_, _) => false end What does this expression compute? We can make this into a function. First, the anonymous function: (fn (x: LIST) => (case x of EMPTY => true | CONS(_, _) => false)): LIST -> bool We can give a name to a function using 'let': let val f: LIST -> bool = fn (x: LIST) => (case x of EMPTY => true | CONS(_, _) => false) in f EMPTY end There is one more way to name functions: fun f(x: LIST) = case x of EMPTY => true | CONS(_, _) => false Case statements must be exhaustive. If they aren't, SML issues a warning - we treat such warnings as errors in homeworks and exams. Pattern matching can occur in val statements: let val (a, b): int * string = (3, "aha") val square = a *a in ... end Note that 'val' statements are evaluated sequentially, and we can use previously defined values in subsequent val statements. Such statements are useful to simplify notation, and to avoid the multiple evaluation of certain common subexpressions. In pure functional languages the use (or non-use) of val statements can not change the result of the computation, but it can speed it up significantly. We can also ignore one or more elements from a tuple: val (_, b): int * string = (3, "aha") Note that once a value is assigned, it is not possible to change it. Pattern matching can occur in function headers, but we don't like to see constants in such patterns. Take a look at this: fun sum(z: int * int): int = (#1 z) + (#2 z) fun sum(z: int * int): int = case z of (x, y) => x + y fun sum(x: int, y: int): int = x + y; We like the last version most.