



Study with the several resources on Docsity
Earn points by helping other students or get them with a premium plan
Prepare for your exams
Study with the several resources on Docsity
Earn points to download
Earn points by helping other students or get them with a premium plan
An overview of the history of programming languages, focusing on imperative, object-oriented, actor-oriented, functional, and logic languages. It also covers the concept of lambda calculus, which is fundamental to functional programming. Explanations of function composition, free and bound variables, and the order of evaluation.
Typology: Study notes
1 / 6
This page cannot be seen from the preview
Don't miss anything!




Ada Augusta, the Countess of Lovelace, the daughter of the poet Lord Byron, is attributed as being the first “programmer” ever, using Babbage’s Analytical Engine, circa 1843. Imperative programming languages include: Fortran (Backus 1954) as one of the first “high-level” (compiled) programming languages created at IBM and used for numerical computing, Cobol (Hopper
The lambda calculus created by Church and Kleene in the 1930’s is at the heart of functional programming languages. It is Turing-complete, that is, any computable function can be expressed and evaluated using the calculus. It is useful to study programming language concepts because of its high level of abstraction. We will briefly motivate the calculus and introduce its syntax and semantics. The mathematical notation for defining a function is with a statement such as
f (x) = x^2 , f : Z → Z,
where Z is the set of all integers. The first Z is called the domain of the function, or the set of values x can take. The second Z is called the range of the function, or the set containing all possible values of f (x). Suppose f (x) = x^2 and g(x) = x + 1. Traditional function composition is defined as
f ◦ g = f (g(x)).
With our functions f and g,
f ◦ g = f (g(x)) = f (x + 1) = x^2 + 2x + 1.
Similarly g ◦ f = g (f (x)) = g(x^2 ) = x^2 + 1. Therefore, function composition is not commutative. In lambda (λ) calculus, we can use a different notation to define these same concepts. To define a function f (x) = x^2 , we instead write λx.x^2.
Similarly for g(x) = x + 1 we instead write λx.x + 1.
To describe a function application such as f (2) = 4, we write
(λx.x^2 2) ⇒ 22 ⇒ 4.
The syntax for lambda calculus expressions is
e ::= v – variable | λv.e – lambda expression | (e e) – procedure call
The semantics of the lambda calculus, or the way of evaluating or simplifying expressions, is defined by the rule (λx.E M ) ⇒ E{M/x}.
The new expression E{M/x} can be read as “replace ‘fresh’ x’s in E with M ”. Informally, a “fresh” x is an x that is not nested inside another lambda expression. We will cover free and bound variable occurrences in detail later on. For example, in the expression (λx.x^2 2),
E = x^2 and M = 2. To evaluate the expression, we replace x’s in E with M , to obtain
(λx.x^2 2) ⇒ 22 ⇒ 4.
In lambda calculus, all functions may only have one variable. Functions with more than one variable may be expressed as a function of one variable through currying. Suppose we have a function of two variables expressed in the normal way
h(x, y) = x + y, h : (Z × Z) → Z.
With currying, we can input one variable at a time into separate functions. The first function will take the first argument, x, and return a function that will take the second variable, y, and will in turn provide the desired output. To create the same function with currying, let
f : Z → (Z → Z)
and g : Z → Z.
That is, f maps integers to a function, and g maps integers to integers. The function f (x) returns a function gx that provides the appropriate result when supplied with y. For example,
f (2) = g 2 , where g 2 (y) = 2 + y.
So f (2)(3) = g 2 (3) = 2 + 3 = 5.
int f(int x) { return x*x; }
int main() { int x; ... x = x + 1; return f(x); }
In this example, the x in f could have been substituted for y or any other variable name without changing the output of the program. In the same way, the lambda expression
(λx.x^2 x + 1)
is identical to the expression (λy.y^2 x + 1),
since the final x is unbound, or free. To simplify the expression
(λx.(λx.x^2 x + 1) 2)
You could let E = (λx.x^2 x + 1) and M = 2. The only free x in E is the final one so the correct reduction is (λx.x^2 2 + 1).
The x in x^2 is bound, so it is not replaced. However, things get more complicated. It is possible when performing β -reduction to inadvertently change a free variable into a bound variable, which changes the meaning of the expression. In the statement
(λx.λy.(x y) (y w)),
the second y is bound to λy and the final y is free. Taking E = λy.(x y) and M = (y w), we could mistakenly arrive at the simplified expression
λy.((y w) y).
But now both the second and third y’s are bound, because they are both a part of of the λy function definition. This is wrong because one of the y’s should remain free as it was in the original expression. To get around this, we can change the λy expression to a λz expression
(λx.λz.(x z) (y w)),
which again does not change the meaning of the expression. This process is called α -renaming. Now when we perform the β -reduction, the original two y variables are not confused. The result is
λz.((y w) z).
Here, the free y remains free.
There are different ways to evaluate lambda expressions. The first method is to always fully evaluate the arguments of a function before evaluating the function itself. This order is called applicative order. In the expression (λx.x^2 (λx.x + 1 2)),
the argument (λx.x + 1 2) should be simplified first. The result is
⇒ (λx.x^2 2 + 1) ⇒ (λx.x^2 3) ⇒ 32 ⇒ 9.
Another method is to evaluate the left-most redex first. A redex is an expression of the form (λx.E M ), on which β -reduction can be performed. This order is called normal order. The same expression would be reduced from the outside in, with E = x^2 and M = (λx.x + 1 2). In this case the result is
⇒ (λx.x + 1 2)^2 ⇒ (2 + 1)^2 ⇒ 9. As you can see, both orders produced the same result. But is this always the case? It turns out that the answer is “no” for certain expressions whose simplification does not terminate. Consider the expression
(λx.(x x) λx.(x x)).
It is easy to see that reducing this expression gives the same expression back, creating an infinite loop. If we consider the expanded expression
(λx. 3 (λx.(x x) λx.(x x))),
we find that the two evaluation orders are not equivalent. Using applicative order, the (λx.(x x) λx.(x x)) expression must be evaluated first, but this never terminates. If we use normal order, however, we evaluate the entire expression first, with E = 3 and M = (λx.(x x) λx.(x x)). Since there are no x’s in E to replace, the result is simply 3. It turns out that it is only in these particular non-terminating cases that the two orders may give different results. The Church-Rosser theorem (also called the confluence property or the diamond property) states that if a lambda calculus expression can be evaluated in two different ways and both ways terminate, both ways will yield the same result. Also, if there is a way for an expression to terminate, using normal order will cause the termination. In other words, normal order is the best if you want to avoid infinite loops. Take as another example the C program
int loop() { return loop(); }
int f(int x, int y) { return x; }
int main() { return f(3, loop()); }
In this case, using applicative order will cause the program to hang, because the second argument loop() will be evaluated. Using normal order will terminate because the unneeded y variable will never be evaluated. Though normal order is better in this respect, applicative order is the one used by most programming languages. Why? Consider the function f (x) = x + x. To find f (4/2) using normal order, we hold off on evaluating the argument until after placing the argument in the function, so it yields
f (4/2) = 4/2 + 4/2 = 2 + 2 = 4,
and the division needs to be done twice. If we use applicative order, we get
f (4/2) = f (2) = 2 + 2 = 4,
which only requires one division. Since applicative order avoids repetitive computations, it is the preferred method of evaluation in most programming languages, where short execution time is critical.