Book: 6.5, 7.3
Programming with states vs. values
Functions as values
Anonymous functions
Lambda calculus
Evaluation strategies
Simply typed lambda calculus
Polymorphic functions and principal types
Two programming language paradigms:
Imperative (aka. procedural): the execution of a program is both evaluation of expressions and changing a state (values of variables).
Functional: the execution of a program is just evaluation of expressions, without changing the values of variables.
States and side effects have a double effect:
In the past, (1) dominated. Currently (2) is getting more important.
In fact, a powerful optimization technique is single static assignments, where all variables are only given value once.
The levels are:
Thus no statements!
Example (could be written in a subset of Haskell):
doub x = x + x twice f x = f (f x) quadruple = twice doub main = print (twice quadruple 2)
So, is functional programming less expressive than imperative?
Sure we can write functions in C/C++/Java, too:
// doub x = x + x int doub (int x) { return x + x ; }
But this is what is called a first-order function: the arguments are objects like numbers and class objects - but not themselves functions.
In C++, it is also possible to write second-order functions, which take functions as arguments:
// twice f x = f (f x) int twice(int f (int n), int x) { return f(f(x)) ; }
In a functional language, a functions are first-class citizens.
This means: a function has a value even if it is not applied.
This is not quite so in C++: you cannot return a function as a value:
// quadruple = twice doub /* not possible (int f (int x)) quadruple(int x) { return twice(doub) ; } */ int quadruple(int x) { return twice(doub, x) ; }
The types of functions in Haskell are written in this way:
max : Int -> Int -> Int
The notation is right-associative, and hence equivalent to
max : Int -> (Int -> Int)
Thus, a "two-place function" is really a one-place function
whose value is also a function. (Haskell uses ::
for typing,
but we stick to :
)
The typing rule for functions is:
Env => f : A -> B Env => a : A --------------------------------- Env => f a : B
Thus partial application is meaningful:
max 4 :: Int -> Int -- returns the greater one of its argument and 4
In many other languages, the value of a function must be a "basic type", i.e. not a function type.
This corresponds in Haskell to having a tuple of arguments:
maxt : (Int,Int) -> Int
The typing rule for tuples is
Env => a : A Env => b : B --------------------------- Env => (a,b) : (A,B)
Partial application is thus not meaningful: you have to form the tuple first.
The move
(A,B) -> C ==> A -> B -> C
is called currying, with reference to Haskell B. Curry.
At the same time as it is a powerful programming technique, it simplifies the semantics and implementation of programming languages.
In a functional language, you can form anonymous functions by using lambda abstraction:
timesNine = twice (\x -> x + x + x)
In C++, you would have to write a named function for tripling:
// triple x = x + x + x int triple(int x) { return x + x + x ; } // timesNine = twice triple int timesNine(int x) { return twice(triple, x) ; }
There is a recent Lambda Library for C++ permitting this.
There is no difference in writing a function definition in these three ways:
foo x y = x + y * y foo x = \y -> x + y * y foo = \x -> \y -> x + y * y
We can thus simplify the abstract syntax of programs by saying that definitions have the form
Def ::= Ident "=" Exp
and treating the other form
Def ::= Ident [Ident] "=" Exp
as syntactic sugar:
id x1 ... xn = exp ===> id = \x1 -> ... \xn -> exp
In this way, the environment is very simple: just mapping identifiers to expressions!
Now it is enough to work with the following tiny language:
Exp ::= Ident | Integer | "(" Exp Exp ")" | "(" "\" Ident "->" Exp ")"
Thus the expressions are variables, integer constants, applications, and
lambda abstractions. (Operations like +
can be treated as functions.)
For simplicity, we define the values to be expressions (notice that values are either integers or functions).
The big-step semantics is:
env => var ⇩ lookup env var env => const ⇩ const env => fun ⇩ (\x -> body) env => arg ⇩ val env => body(val/x) ⇩ result -------------------------------------------------------------------------------- env => (fun arg) ⇩ result env => (\x -> body) ⇩ (\x -> body)
Thus lambda abstractions are not evaluated when alone, but only when they are applied to arguments.
The operation body(val/x)
is substitution: replace all
occurrences of the variable x
by the expression val
in the expression body
.
Example:
(x + x + 3)(5/x) ⇩ 5 + 5 + 3
Definition, by syntax-directed translation
const(val/x) = const var(val/x) = val, if var == x var(val/x) = var, if var != x (fun arg)(val/x) = (fun(val/x) arg(val/x)) (\z -> body)(val/x) = (\z -> body(val/x)), if no capture
For abstraction, we need a condition to prevent capture.
Capture means that some z
in val
becomes bound by
the lambda.
Example evaluation:
twice = \f -> \a -> f (f a) double = \x -> x + x ((twice double) 3) = (((\f -> \a -> f (f a)) (\x -> x + x)) 3) = (((\a -> (\x -> x + x) ((\x -> x + x) a))) 3) = (((\a -> (\x -> x + x) (a + a))) 3) = (((\a -> a + a + a + a)) 3) = 3 + 3 + 3 + 3
Recall the unconditioned substitution rule:
(\z -> body)(val/x) = (\z -> body(val/x))
The following go wrong:
(\x -> x)(5/x) = (\x -> x(5/x)) -- should not replace a bound x = (\x -> 5) (\y -> x)(y/x) = (\y -> x(y/x)) -- should not bind a free y = (\y -> y)
Solution: alpha conversion: change the name of a bound variable to a new one that does not appear in the substitution.
(\x -> x)(5/x) = (\z -> z)(5/x) = (\z -> z(5/x)) = (\z -> z) (\y -> x)(y/x) = (\z -> x)(y/x) = (\z -> x(y/x)) = (\z -> y)
Two evaluation strategies.
Call by value (as above): evaluate argument before substitution
env => fun ⇩ (\x -> body) env => arg ⇩ val env => body(val/x) ⇩ result ---------------------------------------------------------------------------- env => (fun arg) ⇩ result
Call by name : substitute first, then evaluate
env => fun ⇩ (\x -> body) env => body(arg/x) ⇩ result -------------------------------------------------------- env => (fun arg) ⇩ result
What difference does it make?
C, C++, Java, ML use call by value.
Haskell uses a variant of call by name: call by need.
Consider the code
infinite = 1 + infinite first x y = x main = first 5 infinite
With call by value, we get
main = first 5 infinite = (\x -> \y -> x) 5 (1 + infinite) = (\y -> 5) (1 + infinite) = (\y -> 5) (2 + infinite) ...
With call by name,
main = first 5 infinite = (\x -> \y -> x) 5 infinite = (\y -> 5) infinite = 5
Generally: if an evaluation can terminate, it terminates with the call by name order. But not necessarily with call by value.
Call by name is sometimes called lazy evaluation: don't evaluate until you need!
Disadvantage: you may need to evaluate the same expression twice:
doub x = x + x doub (doub 8) = doub 8 + doub 8 -- by name = 8 + 8 + 8 + 8 = 32 doub (doub 8) = doub 16 -- by value = 16 + 16 = 32
Call by need: change the environment by filling in the value when an expression is evaluated. Next time, the value is found from the environment and not computed again.
In simply typed lambda calculus, there is
int
typ1 -> typ2
The typing rules are
env => var : A, if A == lookup env var env => const : int, for integer constants env => fun : A -> B env => arg : A ------------------------------------- env => (fun arg) : B env, x : A => body : B ---------------------------- env => (\x -> body) : A -> B
where A, B
are any types
What is the type of
\x -> x
There are infinitely many types:
int -> int (int -> int) -> (int -> int) (int -> int - int) -> (int -> int -> int)
All these types have the form
A -> A
Conversely, what ever type A
is, the expression has it.
We say that \x -> x
is polymorphic: it has many types.
Some polymorphism is possible in C++ using templates.
// twice : (A -> A) -> A -> A template<class A> A twice(A f (A n), A x) { return f(f(x)) ; }
Similarly in generic Java (Java 1.5):
// id : A -> A public static <A> A id(A x) { return x ; }
(and there are no functions of functions).
Templates in C++ and generics in Java were inspired by ML and Haskell.
In general, the result is polymorphic.
There is an algorithm returning the most general type. Thus
infer (\x -> x) = A -> A infer (\x -> \y -> x) = A -> B -> A infer (\f -> \x -> f (f x)) = (A -> A) -> A -> A infer (\x -> x + x) = int -> int
infer (\f -> \x -> f (f x)) : t t = a -> b -> c ==> f (f x) : c f : d -> e ==> a = d -> e because f : a c = e because f _ : e f x : d because f : d -> _ f x : e because f : _ -> e hence d = e x : d ==> d = b because x : b we have a = d -> e = b -> b c = d = b hence t = (b -> b) -> b -> b
Isn't this mind-twisting - a little bit like Sudoku? Can it be made systematic?
The algorithm calls unification: Book 6.5.4
Unification finds solutions to equations with type variables.
Unification algorithm: Book 6.5.5
env => var : A', if A == lookup env var and A > A' env => const : int, for integer constants env => fun : A -> B env => arg : A ------------------------------------- env => (fun arg) : B env, x : A => body : B ---------------------------- env => (\x -> body) : A -> B
Here A > A'
means that A'
is an instance of A
. Examples:
a > Int a > Int -> Int a > b -> c a -> b > Int -> (Int -> b)
where a, b, c
are type variables.
Input: two types with variables.
Output: type substitution s (mapping from variables to types).
Notation: Ts is type T after performing substitution s.
Property: if s = unify T U, then Ts = Us
unify a b = {a = b} -- a,b distinct variables unify a T = {a = T} -- T non-variable, a doesn't occur in T unify (T -> U) (T' -> U') = s1 := unify T T' s2 := unify Us1 U's1 return s2 + s1
Occurs check: we cannot unify e.g. a
with a -> b
.
Input: environment mapping variables to their types, expression.
Output: substitution, type.
infer env x = ({}, fresh env (lookup x env)) infer env n = ({}, Int) infer env (fun arg) = (s1,T1) := infer env fun (s2,T2) := infer (env s1) arg b := freshVar env s3 := unify (T1 s2) (T2 -> b) return (s3 + s2 + s1, b s3) infer env (\x -> body) = b := freshVar env (s,T) := infer (env, x : b) body return (s, (b s) -> T)
The auxiliary freshVar
returns a type variable not yet occurring in env.
infer () (\f -> \x -> f (f x)) infer (f : a, x : b) (f (f x)) ({},c) <- infer (f:a, x:b) f ({},d) <- infer (f:a, x:b) x {c = d -> e} <- unify c (d -> e) ({c = d -> e}, e) <- infer env (f x) {c = e -> e} <- unify c (e -> e) ({c = d -> e, c = e -> e, d = e}, e) <- infer env (f (f x)) (subst, e -> e) <- infer env (\x -> f (f x)) (subst, (e -> e) -> e -> e) <- infer env (\f -> \x -> f (f x))
with some shortcuts and "obvious" abbreviations env and subst... it's better to implement and try out!