Lecture 10: Functional Programming Languages

Programming Languages Course
Aarne Ranta (aarne@chalmers.se)

Book: 6.5, 7.3

Plan

Programming with states vs. values

Functions as values

Anonymous functions

Lambda calculus

Evaluation strategies

Simply typed lambda calculus

Polymorphic functions and principal types

Imperative vs. functional programming

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:

  1. enable efficient time and space consumption
  2. make it harder to reason about the program and therefore to optimize it

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 structure of a functional program

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) 

Functions in imperative languages

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)) ;
    }

Functions as first-class citizens

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) ;
    }

Function types and partial application

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

Functions of tuples, currying

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.

Anonymous functions

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.

Desugaring function definitions

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!

Evaluating 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.

Substitution

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

Examples of capture

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)

Call by value vs. call by name

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.

Termination of evaluation

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.

Lazy evaluation and call by need

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.

Types for lambda calculus

In simply typed lambda calculus, there is

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

Polymorphism

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.

Polymorphism in C++ and Java

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.

Type inference in functional languages

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

An example of type inference

  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?

Implementing type inference

The algorithm calls unification: Book 6.5.4

Unification finds solutions to equations with type variables.

Unification algorithm: Book 6.5.5

Polymorphic typing rules

    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.

The unification algorithm

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.

Type inference with unification

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.

Type inference example revisited

  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!