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
- val < Ide > = < Expression >Examples of expressions are:
5, 5+6, 5*x
true, false, x>5, x>5 orelse y>7
let val x = 7 in 5*x endThey can also be nested. Example:
let val x = 7 in let val y = 5 in y*x end endThe type of a let expression is the same as the type of the body.
if x>0 then x else 0-4The type of an if-then-else is the same as the type of the "then" and the "else" branches. These types have to be the same.
fun fact n = if n = 0 then 1 else n * fact (n-1);
fun fib n = if n = 0 then 0 else if n = 1 then 1 else fib(n-2) + fib(n-1);
fact(fib 6)) + 2which will evaluate to the number 10.
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:
fun fact 0 = 1 | fact n = n * fact (n-1);
fun fib 0 = 0 | fib 1 = 1 | fib n = fib(n-2) + fib(n-1);
Some of the list destructors are also predefined in ML. In particular
fun isnil(nil) = true | isnil(_) = false;
fun append(l,k) = if isnil(l) then k else hd(l)::append(tl(l),k);We could also have used pattern-matching:
fun append(nil,k) = k | append(x::l,k) = x::append(l,k);
fun reverse(l) = if isnil(l) then nil else append(reverse(tl(l)),(hd(l)::nil));
fun rev(l,acc) = if isnil(l) then acc else rev(tl(l),(hd(l)::acc)); fun reverse1(l) = rev(l,nil);Exercise: rewrite the two definitions of reverse by using pattern-matching.
fun merge(l,k) = if isnil(l) then k else if isnil(k) then l else let val x = hd(l) and y = hd(k) in if x <= y then (x :: merge(tl(l),k)) else (y :: merge(l,tl(k))) end;The pattern-matching version of the definition would be
fun merge(nil,k) = k | merge(l,nil) = l | merge(x::l,y::k) = if x <= y then (x :: merge(l,y::k)) else (y :: merge(x::l,k))
fun split(x,nil) = (nil, nil) | split(x,(h::t)) = let val (k,m) = split(x,t) in if h <= x then ((h::k),m) else (k,(h::m)) end; fun quicksort(nil) = nil | quicksort(h::t) = let val (low,high) = split(h,t) in append(quicksort(low), (h::quicksort(high))) end;
val (k,m) = split(x,t)In general, the meaning of a declaration of the form
val (x1,x2,...,xn) = expressionis 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 = expressionThe 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.
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.
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.
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.