Download as pdf or txt
Download as pdf or txt
You are on page 1of 7

Section D – Functional Paradigm

1 Characteristics
The mathematical basis of functional languages is Lambda Calculus (for introduction to Lambda
Calculus see section 3.) Lambda Calculus is a system for defining functions and evaluating them. The
most relevant aspect of this is that the evaluation of functions can be perfomed using quite simple rules
known as substitution and rewriting. These rules can be easily implemented on a computer in order to
produce interpreters for functional languages. However, compilers for these languages have also been
developed.
A functional program is written as an expression involving an operator and operands. The operands
themselves may be expressions. The program is run by evaluating the expression. The program ends
when the value of the expression has been computed. This differs from imperative programs which
consist of a series of statements, which are executed one after another, starting with the first and ending
with the last statement.
Functional languages make use of such terms as `variable' and `function' which are also found in
imperative languages, but they have quite different meanings. Here, a variable is a name of a value, not
of a memory location. A functional variable, once bound, refers to a fixed value. An imperative
variable's value may be erased and assigned a new value any number of times. Also, it is assumed that
a function, if given the same arguments, will always return a fixed result (see `referential transparency'
below). But this assumption does not apply to functions in imperative languages (e.g. in Pascal/C++
we may write a function to return the current time, and obviously, such a function will return different
value each time it is called.)
The two main characteristics of functional programming are:
1. Referential Transparency. This is defined: an expression with no undefined variables will always
evaluate to the same result. E.g. the expression f()+g(), which means add the results of function f and
function g, will always give us the same result, no matter where it occurs. The same expression in an
imperative language might give us different results at different times (e.g. if f is a function returning the
current time.)
What is the importance of referential transparency? Firstly, from the point of view of a person trying
to understand the program, the fact that the value of expressions is fixed means that each expression
need only be understood once. This can reduce the amount of time needed to understand a program.
Secondly, if a function has been tested and known to work in one program, we can re-use that function
in another program (or in a different part of the same program) and be certain that the function will
continue to work correctly. It will not be necessary to spend time in developing and testing a new
function that will behave identically to an existing one.
2. Higher-Order Functions (HOFs). A higher-order function is one that takes another function as an
argument, or one that returns another function as a result. Understandably, a significant part of
functional programming is concerned with defining functions. Because HOFs can produce new
functions, the programmer doesn't have to define all of the program's functions by himself - instead, he
can write HOFs that will assist in defining the needed functions. A familiar example of a HOF is
composition (written as the . operator): suppose that f and g are functions of 1 argument, then we may
use the . operator to combine f and g to produce a new function, (f.g), which will pass its argument to g,
then give g's result to f as argument, and finally return the result of f. E.g. if square is a function for
squaring a number, then (square.square)(x) will return x to power 4.

Section D p. 1/7
Haskell is a purely functional programming language, i.e. it contains no concepts that are contradictory
to the functional paradigm. In addition to referential transparency and HOFs, it provides several
features:
3. Type Inference. Although Haskell is statically typed, it does not force the programmer to give type
declarations in most cases. The semantic analyser is capable of inferring the types of variables and
functions automatically. E.g. in the following definition of a square function:
square x = x*x

...it is apparent to the semantic analyser that x must belong to some numerical type since the arithmetic
operator * is applied to x.
4. Lazy Evaluation. The actual parameters to a function are not evaluated unless their values are
necessary for the computation of the function's result. E.g. consider function f that disregards its
second argument:
f x y = x

...if f is called: f (1+1) (2+2), then f will return 2 but the expression 2+2 will never be evaluated. The
advantage is that functions spend a minimum amount of time in computing their results.
5. Infinite Data Structures. The programmer is allowed to define data structures (e.g. lists) with
infinite number of elements. However, the elements are not actually stored in memory until they are
needed, thanks to lazy evaluation. E.g. the expression: take 10 [1..], will only cause the first 10
elements of the infinite list of positive integers to be computed and stored in memory.
6. Currying. A function need not be called with all its arguments. Passing fewer than the required
number of arguments to a function will cause that function to return a new function that will expect the
remaining argument to be passed subsequently. E.g. while the take function requires 2 arguments, we
are allowed to pass just 1, such as in the expression (take 10), in which case a new function is
produced that will expect only 1 argument (a list) and return its first 10 elements.
7. Pattern Matching. Haskell allows functions to be defined in a case-by-case (or clausal) format. E.g.
the following function f is defined using 3 clauses, for 3 possible values of the argument:
f 0 = 1
f 1 = 1
f 2 = 10

...when the function is called, Haskell will execute the clause whose formal parameter matches the
actual parameter. The numerical values used as formal parameters are known as number patterns. In
the above function f, if the argument is 0 or 1, the result returned would be 1, else if the argument is 2, f
will return 10. Lists may also be used as patterns, as this function for calculating the length of lists
demonstrates:
len [] = 0
len (h:t) = 1 + len t

...here the formal parameters [] and (h:t) are examples of list patterns. Of course, this function may be
alternatively defined using a single clause, without patterns:
len1 l = if l == [] then 0 else 1 + len1 (tail l)

Section D p. 2/7
8 Nested Functions and Lexical Closures. A nested function is one that is defined inside another. E.g.
conside the function sq below, which is nested inside the definition of function g.
g x =
let sq y = y*y
in sq x + sq (x-1)

.. here sq is `hidden' inside g, and so is not in scope (not visible) outside g. In the above case, the
nested function did not use any of the outer function's variable names. Unlike the following:
add10 x =
let adder y = x+y
in adder 10

... where although adder takes only one argument, y, it also has access to the argument received by the
outer function, x. adder is an example of a lexical closure: a nested function that makes use of names
defined by the outer function.
9. Parametric Polymorphism. Type classes provide this kind of polymorphism.

2 Recursive Functions
Because Haskell lacks statements, it is not possible to repeat computations by `jumping back' from one
statement to a previous one. The only way to perform repetitive computations is to recurse. E.g.
factorials:
fac 0 = 1
fac x = x * fac (x-1) -- subtract, recurse then finally multiply.

The above fac function has one serious drawback. Since each recursive call will push a new activation
record onto the program stack, it is possible that a very large argument will make the program stack
grow until the computer's memory is exhausted. Fortunately, it is possible to rewrite recursive
functions in a special manner known as `tail recursion' which will lead to the creation of only 1
activation record, no matter how large the argument. Tail recursion means that the last action done by
a function before returning is the making of a recursive call. The above fac is non-tail recursive since
the last thing done before returning was multiplication.
Here's a tail-recursive factorial. It takes an additional argument which should be initially 1. So to find
the factorial of 10, evaluate the expression: tailFact 10 1.
tailFact 0 p = p
tailFact x p = tailFact (x-1) (p*x) -- subtract, multiply then
-- finally recurse.

3 Lambda Calculus
The Lambda Calculus is a both a mathematical notation for writing functions and a method for
evaluating functions. As such it can be used as the syntax of a programming language, and also as a
method of interpreting (translating) the program for execution on a computer.
The Lambda Calculus is the main theoretical basis of functional programming languages such as

Section D p. 3/7
Haskell.

3.1 Structure of Expressions


The Lambda Calculus notation is used to write structures called expressions, of which there are three
kinds:
1. Single Identifier. These are operators (e.g. +) and operands (both constant operands like 1 and
variable names like x.) It is assumed that these single identifiers may be combined to form larger
expressions, e.g. 1+x.
2. Function Definition. The main power of Lambda Calculus derives from its ability to define
functions. Some examples of 1-argument function definitions are:
A function that returns 1 plus its argument:
(λx.x+1)
A function that returns the square of its argument:
(λx.x*x)
A function that always returns 1:
(λx.1)
The name found in between the λ (Greek letter lambda) and the full-stop is the name of the formal
parameter. Everything that comes after the full-stop is the function body: an expression whose result is
computed and returned by the function when it is applied (see the next point on Function Application).
Note that while the formal parameters are given names (x in all the above examples), the functions
themselves are anonymous or nameless.
3. Function Application. This is how we express the act of calling a function. Some examples of
function applications are:
Apply the 'one plus' function to 7 (returns 8)
((λx.x+1) 7)
Apply square function to 2 (returns 4):
((λx.x*x) 2)
Apply the 'always one' function to 55 (return 1, as always):
((λx.1) 55)
The notation is very simple: write the function definition as a prefix to the actual parameter. What then
happens is that the actual parameter becomes assigned as the value of the formal parameter, after which
the body of the function is evaluated and the result is returned.
Note that such function application expressions are equivalent to the constant values they evaluate to.
That is, in any expression, you are allowed to substitute a function application for a constant or vice
versa. For example, instead of writing 1+4 one may write any of the following 3, as they are all
equivalent:

Section D p. 4/7
1 + ((λx.x*x) 2)
((λx.1) 55) + 4
((λa.1) 9) + ((λb.b*b) 2)
For clarity, we can view how function application works step-by-step by rewriting the expressions such
that formal parameter names become replaced by the actual parameter values.
For example, ((λx.x+1) 7) returns 8 because it can be rewritten as:
((λx.x+1) 7)
= 7+1
Notice that all we have done is rewrite the body of the function with all occurrences of the formal
parameter substituted by the actual parameter.

3.2 Named and Multiple-Arity Functions


Using these three simple elements, it is possible to write functions of any complexity. This notation
may seem to be limited in a couple of ways, but they are easily overcome:
1. Functions have no name.
2. Functions take only one argument.
To overcome these problems, we have to take advantage of the ability (i) to give a function definition
as an actual parameter, and (ii) to write a function definition in the body of another function definition.
Let's examine these individually.
Firstly, to give a name to a function, we give that function's definition as the actual parameter in a
function application. This incomplete example shows the square function (λx.x*x) being given the
name sq:
((λsq.________) (λx.x*x))
The example is incomplete because the underlined body of the first function definition is left blank.
However, we are now allowed to use the name sq to denote the square function anywhere in the
underlined region (the underlined region is the scope of the sq name). Here is a complete example:
((λsq.(sq 4)) (λx.x*x))
What's happening here is: firstly, the actual parameter (λx.x*x) is assigned to the name sq, and
secondly the actual parameter 4 is passed to that sq function, causing 4 to be assigned to the name x.
Hence the value of the overall expression becomes 4*4 which is 16. To show this step-by-step:
((λsq.(sq 4)) (λx.x*x))
= ((λx.x*x) 4))
= 4*4

Section D p. 5/7
In the next example, we will take the above incomplete expression and fill in a different function body:
((λsq.(sq 4) + (sq 3)) (λx.x*x))
This expression has a value of 4 squared plus 3 squared because:
((λsq.(sq 4) + (sq 3)) (λx.x*x))
= ((λx.x*x) 4) + ((λx.x*x) 3)
= 4*4 + 3*3
Secondly, in order to define a 2-argument function, we need to enclose one function definition inside
the body of another:
(λx.(λy.________))
_____________
In the above incomplete function definition, the long-underlined region is the scope of the name x,
while the short-underlined region is the scope of y. As the latter is completely covered by the former,
we are permitted to use both names, x and y, in the short-underlined region. The following defintions
may be instructive:
A function that returns the sum of its 2 arguments:
(λx.(λy.x+y))
A function that returns the difference between the products of its 2 arguments:
(λx.(λy.x*x - y*y))
A function that returns its first argument:
(λx.(λy.x))
The addition function, applied to 2 and 3 returns 5 because:
( ( (λx.(λy.x+y)) 2 ) 3 )
= ( (λy.2+y) 3 )
= 2+3
In the first application, 2 is passed as actual parameter to the inner function definition, resulting in the
new function application ((λy.2+y) 3). This in turn is evaluated to give 2+3.
This idea may be generalized to allow us to write functions that accept any number of parameters. A
three-argument function is defined in this form:
(λx.(λy.(λz.___________)))
For example, a function that multiples all three arguments, applied to 1, 2 and 3 is:
( ( ((λx.(λy.(λz.x*y*z))) 1) 2 ) 3 )
Its value is 1*2*3 as shown below:
( ( ((λx.(λy.(λz.x*y*z))) 1) 2 ) 3 )
= ( ( (λy.(λz.1*y*z)) 2 ) 3 )
= ( (λz.1*2*z) 3 )
= 1*2*3

Section D p. 6/7
3.3 Reduction to Normal Form
An expression of the Lambda Calculus can be evaluated step-by-step by a simple rewriting process.
This is the main idea underlying the implementation of translators for functional programming
languages. Above, we have seen above a few instances where function applications have been
evaluated. In each step, we wrote a reduced expression by substituting the formal parameter name by
the expression that had been provided as actual parameter.
In the final step we will have an expression containing no lambdas at all, known as a Normal Form
expression. This expression, made up of single identifiers only (operators and their operands) is the
result of evaluating the function application.
Example 1 (Final exam 2004 Q4(d)(ii)): ((λx.((λy.x*y) ((λz.z) 1)) 2)
We start by rewriting the innermost function application (λz whose argument is 1). Note that the
function definitions and corresponding actual parameters are underlined for clarity:
( (λx.((λy.x*y) ((λz.z) 1))) 2 )
= ( (λx.((λy.x*y) 1 )) 2 )
= ( (λx. X*1 ) 2 )
= 2*1
Example 2: (((λa.a) (λb.1+b)) ((λc.c) 2))
We start by rewriting the innermost function application (λa whose argument is (λb.1+b)):
( ( (λa.a ) (λb.1+b) ) ((λc.c) 2) )
= ( (λb.1+b) ((λc.c) 2) )
= ( (λb.1+b) 2 )
= 1+2
Example 3: (((λx.(λy.(x y))) (λz.z+1)) 3)
We start by rewriting the innermost function application (λx whose argument is (λz.z+1)):
( ( (λx.(λy.(x y))) (λz.z+1) ) 3 )
= ( (λy.((λz.z+1) y)) 3 )
= ((λz.z+1) 3)
= 3+1

Section D p. 7/7

You might also like