CSE 428: Lecture 19


Introduction to ML and functional programming

Taxonomy of programming languages

Most langauges blend various features. Consider the spectrum of Imperative versus Functional:
   Assembly Languages           C/Pascal              ML   Pure Functions

  <---------------------------------------------------------------------->
   Only commands                                         Only expressions
   
   LOAD R1 5                    R3 :=   5 + 7 ;
   LOAD R2 7                          ----------
   SUM R1 R2 R3                           ^
                                          |
                                      expression

Characteristics of Functional Programming

Interpretation is based on a read-eval-print loop.

Declarations and Expressions in ML

The general form of a declaration is
   - val < Ide > = < Expression >
Examples of expressions are:

Recursive definitions in ML

Recursion is the basic method of programming in ML. Functions can be defined recursively in the usual way, i.e. by making, in the body of the definition, one or more calls to the function we are defining. Examples Function calls are expressions, for instance we can write
   fact(fib 6)) + 2
which will evaluate to the number 10.

Definition of functions by pattern-matching

ML allows to define function by pattern matching, which, intuitively, corresponds to to defining a function "by cases". A pattern is an expression which can contain variables (each occurring only once), constants, tuple constructors (i.e. parentheses and commas), and data type constructors (i.e. operations like "cons" on lists, see below). The general form of a definition of a function f by pattern matching is:
   fun f pattern_1 = expression_1
     | f pattern_2 = expression_2
     ...
     | f pattern_n = expression_n;
where each expression_i can contain the variables in the pattern_i

When we make a function call

   f e;
the argument expression e is evaluated, and the result v is confronted with the various patterns, from the first (pattern_1) to the last (pattern_n). The first pattern pattern_i which "matches" v (i.e. which can be made equal to v by assigning a suitable value to the variables of the pattern) is selected, and the result of the call will be given by the result of the corresponding expression expression_i, with the value of the pattern variables extablished by the matching. Examples Let us see how we could define the above functions of factorial and Fibonacci by pattern matching:

Lists in ML

ML has lists as a predefined type. The syntax of the list constructors is: We can also use the notation [a1,a2,...,an] to represent a list whose elements are a1, a2, ..., an. For instance, [] stands for nil and [1,2,3] stands for 1::2::3::nil.

Some of the list destructors are also predefined in ML. In particular

There are also other functions on list which are predefined in ML. For instance, "append" is predefined and represented in infix notation by the symbol "@".

Examples of functions on lists

Note that in the function split we have used a declaration
   val (k,m) = split(x,t) 
In general, the meaning of a declaration of the form
   val (x1,x2,...,xn) = expression
is the following:
Evaluate the expression. The result should be a n-tupla of values, (v1,v2,...,vn) (otherwise we get an error). Then associate the values v1, v2, ..., vn to x1, x2, ..., xn, respectively.
The declaration of a tuple of variables, like the above, is a special case of declaration using pattern-matching. The general format is:
   val pattern = expression
The meaning is the following:
Evaluate the expression. The result v should be of the same type as the pattern, otherwise we get an error. Then match v against the pattern, and assign to the variables of the pattern the values that make the pattern equal to v.

Tuples in ML

Tuples in ML correspond to records. The generic type for a n_tuple is T1 * T2 * ... * Tn, where T1, T2, ..., Tn are types. "*" represents the cartesian product.

For instance the type T1 * T2 stand for all the set of pairs of the form (v1,v2), where v1 is a value of type T1, and v2 is a value of type T2.

In the example above, split is a function which takes an argument of type T * (T list) and returns a result of type (T list) * (T list). "T list" is the type of the lists whose elements have type T. The compact notation for this is:

   split : T * (T list) -> (T list) * (T list)
where the symbol ":" stands for "has type" and "->" represents the functional arrow.

The precise syntax of types, the type inference mechanism of ML etc. will be explained in future lectures.

Memory management in ML

We do not have to worry about memory management in ML, because the garbage collection takes care of deallocating structures which are not used anymore.

Consider for instance the expression

   append(append([1,2],[3,4]),[5,6])
the evaluation of this expression will create an intermediate structure L as the result of the innermost append, namely the list [1,2,3,4]. L is not used in the final result [1,2,3,4,5,6], because in the final result the elements 1, 2, 3, 4 are created "ex-novo". We do not have any way to refer to L either, so it cannot be used anywhere else in the program. Hence, after the evaluation of the expression, L becomes "garbage". Anyway, we do not have to worry about it, because the automatic garbage collector will clean up the memory at some time and recollect L.

Lack of side effects in ML

In general we do not have side-effects in ML (unless we use the special type "ref"), i.e. we cannot change the value of a name by executing an expression. As a consequence, we do not have to worry about "sharing" or "aliasing".

Consider for example the following expression

   let val l = [1,2,3]
    in let val k = [4]
        in let m = append(l,k)
            in m
           end
       end
   end;
Are m and k sharing a portion of list? There is no way to tell, since there is no way to change their value. So, whether or not they are shared is an implementation issue, not a language issue.