Elements of Programming Scheme
Elements of Programming Scheme
Mira Balaban
Lecture Notes
Many thanks to Tamar Pinhas, Ami Hauptman, Eran Tomer, Barak Bar-Orion,
Azzam Maraee, Yaron Gonen, Ehud Barnea, Rotem Mairon, Igal Khitron, Rani
Etinger, Ran Anner, Tal Achimeir, Michael Elhadad, Michael Frank for their great
help in preparing these notes and the associated code.
Introduction
1. modeling :
This course deals with both aspects, with a greater emphasis on programming languages
and their properties. The course emphasizes the value of modularity and abstraction in
modeling, and insists on writing contracts for programs.
What is this course about?
It is about the building, implementation and usage of programming languages in a way that
enables construction of manageable, useful and reusable products. The secret word for all
this is ABSTRACTION!
1
Introduction Principles of Programming Languages
5. Programming styles:
2
Introduction Principles of Programming Languages
5. Delayed evaluation.
7. Lexical scoping.
Programming paradigms:
1. Functional programming (Scheme, Lisp, ML): Its origins are in the lambda
calculus.
3. Imperative programming (ALGOL-60, Pascal, C): Its origins are in the Von-
Neumann computer architecture.
For each computational paradigm we define its syntax and operational semantics and imple-
ment a meta tool for its evaluation. We use it to solve typical problems,and study essential
properties.
Most subjects and techniques are taught using the scheme language: A small and
powerful language, designed for educational purposes.
− small – It has a very simple syntax, with few details. Can be taught in half an hour.
3
Chapter 1
Partial sources: SICP [1] 1.1, 1.2, 1.3, 2.2.2 (online version http://deptinfo.unice.fr/
~roy/sicp.pdf);
HTDP [2] 2.5. (online version http://htdp.org/2003-09-26/Book/)
4
Chapter 1 Principles of Programming Languages
Atomic expressions:
Some atomic expressions are primitive: Their evaluation process and the returned values
are already built into Scheme semantics, and all Scheme tools can evaluate them.
Numbers are primitive atomic expressions in Scheme.
> 467
467
;;; This is a primitive expression.
> #t
#t
> +
#<procedure:+>
5
Chapter 1 Principles of Programming Languages
Composite expressions:
> (+ 45 78)
123
> (- 56 9)
47
> (* 6 50)
300
> (/ 25 5)
5
Nesting forms:
> (/ 25. 6)
4.16666666666667
> (+ 1.5 3)
4.5
> (+ 2 10 75.7)
87.7
> (+ (* 3 15) (- 9 2))
52
> (+ (* 3 (+ (* 2 4) (+ 13 5))) (+ (- 10 5) 3))
86
Pretty printing:
> (+ (* 3
(+ (* 2 4)
(+ 13 5)))
(+ (- 10 5)
3))
86
6
Chapter 1 Principles of Programming Languages
The value of a define combination is void – the single value in type Void.
A form with a special operator is called a special form.
> size
6
> (* 2 size)
12
> (define a 3)
> (+ a (* size 1.5))
12
> (define result (+ a (* size 1.5)) )
> result
12
Note: size is an atomic expression but is not a primitive. define provides the sim-
plest means of abstraction: It allows for using names for the results of complex operations.
The global environment: The global environment is a function from a finite set of
variables to values, that keeps track of the name-value bindings. The bindings defined by
define are added to the global environment function. A variable-value pair is called a
binding . The global environment mapping can be viewed as (and implemented by) a data
structure that stores bindings.
Characters in variable names: Any character, except space, parentheses, “,”, “ ‘ “, and
no “ ’ ” in the beginning. Numbers cannot function as variables.
Note: Most Scheme applications allow redefinition of primitives. But then we get
“disastrous” results:
7
Chapter 1 Principles of Programming Languages
and even:
Note: Redefinition of primitives is a bad practice, and is forbidden by most language appli-
cations. Evaluation rule for define special forms (define ⟨variable⟩ ⟨expression⟩):
2. Variables evaluate the values they are mapped to by the global environment map-
ping (via define forms).
Note the status of the global environment mapping in determining the meaning of atomic
symbols. The global environment is consulted first. Apart from primitive and special sym-
bols, all evaluations are global environment dependent.
8
Chapter 1 Principles of Programming Languages
2. Apply the procedure which is the value of expr0 , to the values of the other subex-
pressions.
The evaluation rule is recursive:
> (* (+ 4 (+ 5 7))
(* 6 1 9))
864
Note that (5* 9), (5 * 9) and (not a b) are syntactically correct Scheme forms, but
their evaluation fails.
We can visualize the evaluation process by drawing an evaluation tree, in which each
form is assigned an internal node, whose direct descendents are the operator and operands
of the form. The evaluation process is carried out from the leaves to the root, where every
form node is replaced by the value of applying its operator child to its operands children:
------------------864-------------
| |
* ------16---- ----54-------
| | | | | | |
+ 4 ---12---- * 6 1 9
| | |
+ 5 7
9
Chapter 1 Principles of Programming Languages
The procedure created by the evaluation of this lambda form is called closure, and denoted
⟨Closure (x) (* x x)⟩. Its parameters is (x) and its body is (* x x). The body is not
evaluated when the closure is created. “lambda” is called the value constructor of the
Procedure type. The evaluation of the syntactic form (lambda (x) (* x x)) creates the
semantic value ⟨Closure (x) (* x x)⟩, i.e., the closure. A procedure is a value like any
other value. The origin of the name lambda is in the lambda calculus.
Multiple expression body and side-effects: The body of a lambda expression can
include several Scheme expressions:
But – no point in including several expressions in the body, since the value of the procedure
application is that of the last one. Having several expressions in a body is useful only
when their evaluations have side effects, i.e., affect some external state of the computation
environment. For example, a sequence of scheme expressions in a procedure body can be
used for debugging purposes:
10
Chapter 1 Principles of Programming Languages
display is a primitive procedure of Scheme. It evaluates its argument and displays it:
> display
#<procedure:display>
> newline
#<procedure:newline>
>
display is a side effect primitive procedure! It displays the value of its argument, but its
returned value (like the special operator define), is void, the single value in Void, on which
no procedure is defined.
Style Rule: display and newline forms should be used only as internal forms in a pro-
cedure body. A deviation from this style rule is considered an bad style. The procedure
below is a bad style procedure – why? What is the type of the returned value?
(lambda (x) (* x x)
(display x)
(newline))
11
Chapter 1 Principles of Programming Languages
(display x)
(newline))
3)
4)
3
. . +: expects type <number> as 1st argument, given: #<void>; other
arguments were: 4
>
Summary:
4. When a procedure is created it is not necessarily applied! Its body is not evaluated.
12
Chapter 1 Principles of Programming Languages
> square
#<procedure:square>
Recall that the evaluation of the define form is dictated by the define evaluation rule:
1. Evaluate the 2nd parameter: The lambda form – returns a procedure value: ⟨Closure (x) (* x x)⟩
Note that a procedure is treated as just any value, that can be given a name!! Also,
distinguish between procedure definition to procedure call/application.
Syntactic sugar for named procedure-definition: A special, more convenient, syntax
for procedure definition:
This is just a syntactic sugar : A special syntax, introduced for the sake of convenience. It
is replaced by the real syntax during pre-processing. It is not evaluated by the interpreter.
The syntax of the syntactic sugar syntax of named procedure-definition:
It is transformed into:
> (square 4)
16
> (square (+ 2 5))
49
> (square (square 4))
256
> (define (sum-of-squares x y)
(+ (square x) (square y )))
> (sum-of-squares 3 4)
25
13
Chapter 1 Principles of Programming Languages
> (define (f a)
(sum-of-squares (+ a 1) (* a 2)) )
> (f 3)
52
Intuitively explain these evaluations! Note: We did not provide, yet, a formal semantics!
Summary:
1. In a define special form for procedure definition: First: the 2nd argument is evaluated
(following the define evaluation rule), yielding a new procedure.
Second: The binding ⟨variable – ⟨Closure <parameters> <body>⟩⟩ is added to the
global environment.
2. The define special operator has a syntactic sugar that hides the call to lambda.
(define abs
(lambda (x)
(cond ((> x 0) x)
((= x 0) 0)
(else (- x)))
))
> (abs -6)
6
> (abs (* -1 3))
3
14
Chapter 1 Principles of Programming Languages
The arguments of cond are clauses (<p> <e1> ...<en>), where <p> is a predication
(boolean valued expression) and the <ei>s are any Scheme expressions. A predication is an
expression whose value is false or true. The operator of a predication is called a predicate.
The false value is represented by the symbol #f, and the true value is represented by the
symbol #t.
> #f
#f
> #t
#t
> (> 3 0)
#t
> (< 3 0)
#f
>
<p1 > is evaluated first. If the value is false then <p2 > is evaluated. If its value is false then
<p3 > is evaluated, and so on until a predication <pi > with a non-false value is reached.
In that case, the <ei > elements of that clause are evaluated, and the value of the last
element <eiki > is the value of the cond form. The last else clause is an escape clause: If
no predication evaluates to true (anything not false), the expressions in the else clause are
evaluated, and the value of the cond form is the value of the last element in the else clause.
Definition: A predicate is a procedure (primitive or not), that returns the values true or
false.
(define abs
(lambda (x)
15
Chapter 1 Principles of Programming Languages
(if (< x 0)
(- x)
x)))
> (define a 3)
> (define b 4)
> (+ 2 (if (> a b) a b) )
6
> (* (cond ( (> a b) a)
( (< a b) b)
(else -1 ) )
(+ a b) )
28
> ( (if (> a b) + -) a b)
-1
Example 1.1 (Newton’s method for computing square roots). (SICP 1.1.7)
In order to compute square-root we need a method for computing the function square-root.
One such method is Newton’s method. The method consists of successive application of
steps, each of which improves a former approximation of the square root. The improvement
is based on the proved property that if y is a non-zero guess for a square root of x, then
(y + (x/y)) / 2 is a better approximation. The computation starts with an arbitrary
non-zero guess (like 1). For example:
x=2, initial-guess=1
1st step: guess=1 improved-guess= (1+ (2/1))/2 = 1.5
2nd step: guess=1.5 improved-guess= (1.5 + (2/1.5) )/2 = 1.4167
A close look into Newton’s method shows that it consists of a repetitive step as follows:
Call the step sqrt-iter. Then the method consists of repeated applications of sqrt-iter.
The following procedures implement Newton’s method:
16
Chapter 1 Principles of Programming Languages
(define sqrt
(lambda (x) (sqrt-iter 1 x)))
(define sqrt-iter
(lambda (guess x)
(if (good-enough? guess x)
guess
(sqrt-iter (improve guess x)
x))))
(define improve
(lambda (guess x)
(average guess (/ x guess))))
(define average
(lambda (x y)
(/ (+ x y) 2)))
(define good-enough?
(lambda (guess x)
(< (abs (- (square guess) x)) .001)))
> square
#<procedure:square>
> (sqrt 6.)
2.44949437160697
> (sqrt (+ 100 44.))
12.0000000124087
> (sqrt (+ (sqrt 2) (sqrt 9.)))
2.10102555114187
> (square (sqrt 4.))
4.00000037168919
17
Chapter 1 Principles of Programming Languages
1. Atomic:
(a) Primitives:
− Number symbols.
− Boolean symbols: #t, #f.
− Primitive procedure symbols.
(b) Non-primitives:
− Variable symbols.
− Special operator symbols.
2. Composite:
Evaluation semantics:
1. Atomic expressions: Special operators are not evaluated; Variables evaluate to their
associated values given by the global environment mapping; primitives evaluate to
their defined values.
3. Non-special forms:
18
Chapter 1 Principles of Programming Languages
Signature: area-of-ring(outer,inner)
Purpose: To compute the area of a ring whose radius is
’outer’ and whose hole has a radius of ’inner’
Type: [Number*Number -> Number]
Example: (area-of-ring 5 3) should produce 50.24
Pre-conditions: outer >= 0, inner >= 0, outer >= inner
Post-condition: result = PI * outer^2 - PI * inner^2
Tests: (area-of-ring 5 3) ==> 50.24
Signature: area-of-ring(outer,inner)
Purpose: To compute the area of a ring whose radius is
’outer’ and whose hole has a radius of ’inner’
Type: [Number*Number -> Number]
Example: (area-of-ring 5 3) should produce 50.24
Pre-conditions: outer >= 0, inner >= 0, outer >= inner
Post-condition: result = PI * outer^2 - PI * inner^2
Tests: (area-of-ring 5 3) ==> 50.24
Definition: [refines the contract]
(define area-of-ring
(lambda (outer inner)
(- (area-of-disk outer)
(area-of-disk inner))))
1. Signature
2. Purpose
19
Chapter 1 Principles of Programming Languages
3. Type
4. Example
5. Pre-conditions
6. Post-conditions
7. Invariantss
8. Tests
Signature, purpose and type are mandatory to all procedures. Examples are desirable.
Tests are mandatory for complex procedures. Pre/post-conditions and invariants are rel-
evant only if non-trivial (not vacuous).
The specification of Types, Pre-conditions and Post-conditions requires special
specification languages. The keyword result belongs to the specification language for
post-conditions.
− Impose a certain obligation to be guaranteed on entry by any client module that calls
it: The routine’s precondition – an obligation for the client, and a benefit for the
supplier.
20
Chapter 1 Principles of Programming Languages
--------------------------------------------------------------------
| Client Supplier
------------|-------------------------------------------------------
Obligation: | Guarantee precondition Guarantee postcondition
Benefit: | Guaranteed postcondition Guaranteed precondition
--------------------------------------------------------------------
DbC is an approach that emphasizes the value of developing program specification to-
gether with programming activity. The result is more reliable, testable, documented soft-
ware.
Many languages have now tools for writing and enforcing contracts: Java, C#, C++, C,
Python, Lisp, Scheme:
http://www.ccs.neu.edu/scheme/pubs/tr01-372-contract-challenge.pdf
http://www.ccs.neu.edu/scheme/pubs/tr00-366.pdf
The contract language is a language for specifying constraints. Usually, it is based in
Logic. There is no standard, overall accepted contract language: Different languages have
different contract languages. In Eiffel, the contracts are an integral part of the language. In
most other languages, contract are run by additional tools.
2. Contract mandatory parts: The Signature, Purpose, Type, Tests are mandatory
for every procedure!
4. Pre-conditions should be written when the type does not prevent input for which
the procedure does not satisfy its contract. The pre-condition can be written in En-
glish. When a pre-condition exists it is recommended to provide a precondition-test
procedure that checks the pre-condition. This procedure is not part of the supplier
procedure (e.g., not part of area-of-ring) (why?), but should be called by a client
procedure, prior to calling the supplier procedure.
5. Post-conditions are recommended whenever possible. They clarify what the proce-
dure guarantee to supply. Post-conditions provide the basis for tests.
21
Chapter 1 Principles of Programming Languages
Signature: area-of-disk(radius)
Purpose: To compute the area of a disk whose radius is the
’radius’ parameter.
Type: [Number -> Number]
Example: (area-of-disk 2) should produce 12.56
Pre-conditions: radius >= 0
Post-condition: result = PI * radius^2
Tests: (area-of-disk 2) ==> 12.56
Definition: [refines the contract]
(define area-of-disk
(lambda (radius)
(* 3.14 (* radius radius))))
Area-of-ring must fulfill area-of-disk precondition when calling it. Indeed, this can
be proved as correct, since both parameters of area-of-disk are not negative. The post
condition of area-of-ring is correct because the post-condition of area-of-disk guaran-
tees that the results of the 2 calls are indeed, P I ∗outer2 and P I ∗inner2 , and the definition
of area-of-ring subtracts the results of these calls.
We expect that whenever a client routine calls a supplier routine the client routine
will either explicitly call a pre-condition test procedure, or provide an argument
for the correctness of the call!
22
Chapter 1 Principles of Programming Languages
Pre-conditions: x >= 0
(define sqrt
(lambda (x) (sqrt-iter 1 x)))
Signature: sqrt-iter(guess,x)
Purpose: To compute the square root of x, starting with ’guess’ as initial guess
Type: [Number*Number -> Number]
Example: (sqrt 1 16.) should produce 4.000000636692939
Pre-conditions: x >= 0, guess != 0
(define sqrt-iter
(lambda (guess x)
(if (good-enough? guess x)
guess
(sqrt-iter (improve guess x)
x))))
Assume that we have a request to compute the square root only if the computation does
not exceed a given number of iterations. Otherwise, the necessary action varies by the caller
request. Therefore, if the number of approximations exceeds the bound, we cannot just
return an error. One possibility is to return a value that marks this case, for example #f.
We need to precede each iteration by testing against the bound:
Signature: bounded-sqrt(x,bound)
Purpose: To compute the square root of x, using Newton’s approximations
method, if number of iterations does not exceed ’bound’
Type: [Number*Number -> ?]
Example: (sqrt 16. 7) should produce 4.000000636692939
(sqrt 16. 4) should produce #f
Pre-conditions: x >= 0, bound >= 0
(define bounded-sqrt
(lambda (x bound)
(bounded-sqrt-iter 1 x bound)))
Signature: bounded-sqrt-iter(guess,x,bound)
Purpose: To compute the square root of x, starting with ’guess’ as
initial guess, if number of iterations does not exceed ’bound’
Type: [Number*Number*Number -> ?]
Example: (sqrt 1 16. 7) should produce 4.000000636692939
(sqrt 1 16. 4) should produce #f
Pre-conditions: x >= 0, bound >= 0, guess != 0
(define bounded-sqrt-iter
(lambda (guess x bound)
(if (zero? bound)
23
Chapter 1 Principles of Programming Languages
#f
(if (good-enough? guess x)
guess
(bounded-sqrt-iter (improve guess x) x (sub1 bound))))
))
Signature: bounded-sqrt(x,bound)
Purpose: To compute the square root of x, using Newton’s approximations
method, if number of iterations does not exceed ’bound’
Type: [Number*Number -> Number union Boolean]
Example: (sqrt 16. 7) should produce 4.000000636692939
(sqrt 16. 4) should produce #f
Pre-conditions: x >= 0, bound >= 0
Signature: bounded-sqrt-iter(guess,x,bound)
Purpose: To compute the square root of x, starting with ’guess’ as
initial guess, if number of iterations does not exceed ’bound’
Type: [Number*Number*Number -> Number union Boolean]
Example: (sqrt 1 16. 7) should produce 4.000000636692939
(sqrt 1 16. 4) should produce #f
Pre-conditions: x >= 0, bound >= 0, guess != 0
Side comment on artificial “flag” values: The returned value #f in the last example
is a flag value, that signs a failure to compute the square root under the given bound.
The only role of this value is to signal this case. Using flags is not a good style. There are
various programming approaches that help in avoiding them. For example, an approach that
24
Chapter 1 Principles of Programming Languages
abstracts the results of the clauses of a conditional as procedure parameter can help here.
Such an approach, named Continuation Passing Style (CPS ) is taught in Chapter ?? (an
example appears also at the end of Section 1.5.5 in this chapter). The idea is to replace flags
in “conditional-legs” by continuation procedures, that are given as additional parameters:
(define f
(lambda (x)
(if (<predicate> x)
#f
(<some-computation> x)) ))
==>
(define f
(lambda (x consequent-procedure alternative-procedure)
(if (<predicate> x)
(consequent-procedure x)
(alternative-procedure x)) ))
This way, a caller of f instead of receiving the flag value #f and determining the desired
action based on it, directly pass the consequent action and the alternative actions as argu-
ments, when calling f.
25
Chapter 1 Principles of Programming Languages
This is an example of the dotted notation for pairs: This is the way Scheme prints out
values of the Pair type.
Syntax of pair creation: (cons <exp> <exp>)
Value (semantics) of pair creation: A dotted pair
This is the way Scheme denotes (prints out) Pair values whose cdr is a pair. It results from
the way Scheme denotes (prints out) List values – data objects that will be discussed later.
26
Chapter 1 Principles of Programming Languages
Mixed-type pairs:
> (define pair (cons 1 #t))
> (pair? pair)
#t
> (pair? (car pair))
#f
> (car (car pair))
car: expects argument of type <pair>; given 1
> (procedure? pair?)
#t
Denotation of Pair values: The distinction between Pair typed Scheme expressions
(syntax), to their Pair values (semantics) must be made clear. (cons 1 2) is a Scheme
expression that evaluates into the Pair value (1 . 2). The Pair value cannot be evaluated
by the interpreter. Evaluation of (1 . 2) causes an error.
Signature: successive-pair(n)
Purpose: Construct a pair of successive numbers: (n . n+1).
Type: [Number -> Pair(Number,Number)]
(define successive-pairself
(lambda(n) (cons n (+ n 1))))
> (successive-pair 3)
(3 . 4)
> (pairself (+ 3 4))
(7 . 8)
27
Chapter 1 Principles of Programming Languages
Symbol type are atomic names, i.e., (unbreakable) sequences of keyboard characters (to
distinguish from strings). Values of the Symbol type are introduced via the special operator
quote, that can be abbreviated by the macro character ’. quote is the value constructor
of the Symbol type; its parameter is any sequence of keyboard characters. The Symbol type
identifying predicate is symbol? and its equality predicate is eq?. It has no operations (a
degenerate type).
> (quote a)
’a
;the " ’ " in the returned value marks that this is a returned semantic
;value of the Symbol type, and not a syntactic variable name
> ’a
’a
> (define a ’a)
> a
’a
> (define b ’a)
> b
’a
> (eq? a b)
#t
> (symbol? a)
#t
> (define c 1)
> (symbol? c)
#f
> (number? c)
#t
> (= a b)
. =: expects type <number> as 2nd argument, given: a; other arguments
were: a
>
Notes:
1. The Symbol type differs from the String type: Symbol values are unbreakable names.
String values are breakable sequences of characters.
2. The preceding " ’ " letter is a syntactic sugar for the quote special operator. That
is, every ’a is immediately pre-processed into (quote a). Therefore, the expression
’a is not atomic! The following example is contributed by Mayer Goldberg:
> (define ’a 5)
28
Chapter 1 Principles of Programming Languages
> ’b
. . reference to undefined identifier: b
> quote
#<procedure:quote>
> ’0
5
> ’5
5
Explain!
3. quote is a special operator. Its parameter is any sequence of characters (apart of
few punctuation symbols). It has a special evaluation rule: It returns its argument
as is – no evaluation. This differs from the evaluation rule for primitive procedures
(like symbol? and eq?) and the evaluation rule for compound procedures, which first
evaluate their operands, and then apply.
Question: What would have been the result if the operand of quote was evaluated
as for primitive procedures?
Pair components can be used for tagging information:
A tagging procedure:
Signature: tag(tg,n)
Purpose: Construct a pair of tg and n.
Type: [Symbol*(Number union Symbol) -> Pair(Symbol,(Number union Symbol))]
(define tag
(lambda (tg n)
(cons ’tg n)))
(define get-tag
(lambda (tagged-pair)
(car tagged-pair)))
(define get-content
(lambda (tagged-pair)
(cdr tagged-pair)))
29
Chapter 1 Principles of Programming Languages
2. For a list (v1 v2 . . . vn ), and a value v0 , (v0 v1 v2 . . . vn ) is a list, whose head is v0 , and
tail is (v1 v2 . . . vn ).
The List type has two value constructors: cons and list.
1. The empty list is constructed by the value constructor list: The value of the expres-
sion (list) is (). The empty list () is also the value of two built in variables: null
and empty.
(a) (cons head tail): cons takes two parameters. head is an expression denoting
some value v0 , and tail is an expression whose value is a list (v1 v2 . . . vn ). The
value of (cons head tail) is the list (v0 v1 v2 . . . vn ).
(b) (list e1 e2 ... en): list takes indefinite number of parameters. If e1,
e2..., en (n ≥ 0) are expressions whose values are (v1 , v2 , . . . vn ), respectively,
then the value of (list e1 e2 ... en) is the list (v1 v2 . . . vn ). For n = 0
(list) evaluates to the empty list.
List implementation: Scheme lists are implemented as nested pairs, starting from the
empty list value. The list created by (conse1(conse2(cons...(consen(list))...))) is imple-
mented as a nested pair (it’s the same value constructor, anyway), and the list created by
(list e1 e2 ... en) is translated into the former nested cons application.
Printing form: The printing form of lists is: (<a1> <a2> ... <an>). The printing
form describes the value (semantics) of List-typed expressions (syntax).
The selectors of the List type are car – for the 1st element, and cdr – for the tail of the
given list (which is a list). The predicates are list? for identifying List values, and null?
for distinguishing the empty list from all other lists. The equality predicate is equal?.
30
Chapter 1 Principles of Programming Languages
> (list 3 4 5 7 8)
(3 4 5 7 8)
> (define x (list 5 6 8 2))
> x
(5 6 8 2)
Note: The value constructor cons is considered more basic for lists. (list <a1> <a2> ...
<an>) can be (and is usually) implemented as (cons <a1> (cons <a2> (cons...(cons
<an> (list))...))).
Note on the Pair and List value constructors and selectors: The Pair and the
List types have the same value constructor cons, and the same selectors car, cdr. This is
unfortunate, but is actually the Scheme choice. Scheme can live with this confusion since it
is not statically typed (reminder: a language is statically typed if the type of its expressions
is determined at compile time.) A value constructed by cons can be a Pair value, and also a
List value – in case that its 2nd element (its cdr) is a List value. At run time, the selectors
car and cdr can be applied to every value constructed by cons, either a list value or not (it
is always a Pair value).
Note: Recall that some Pair values are not printed "properly" using the printed form of
Scheme for pairs. For example, we had:
> (define x (cons 1 2))
> (define y (cons x (quote a)))
> (define z (cons x y))
> z
((1 . 2) (1 . 2) . a)
31
Chapter 1 Principles of Programming Languages
while the printed form of z should have been: ((1 . 2) . ( (1 . 2) . a)). The
reason is that the principal type of Scheme is List. Therefore, the Scheme interpreter tries
to interpret every cons value as a list, and only if the scanned value encountered at the list
end appears to be different than (list), the printed form for pairs is restored. Indeed, in
the last case above, z = (cons (cons 1 2) (cons (cons 1 2) ’a)) is not a list.
− A non empty list is visualized as a sequence of 2-cell boxes. Each box has a pointer
to its content and to the next box in the list visualization. The list ((1 2) ((3)) 4)
is visualized by:
Complex list values that are formed by nested applications of the list value constructor,
are represented by a list skeleton of box-and-pointers, and the nested elements form
the box contents.
32
Chapter 1 Principles of Programming Languages
Note: The layout of the arrows in the box-and-pointer diagrams is irrelevant. The arrow
pointing to the overall diagram is essential – it stands for the hierarchical data object as a
whole.
Predicate null?: Tests if the list is empty or not.
> null?
#<primitive:null?>
> (null? (list))
#t
> null
’()
> (equal? ’( ) null)
#t
> (null? null)
#t
> (define one-through-four (cons 1 (cons 2 (cons 3 (cons 4 (list) )))))
> (null? one-through-four)
#f
> list?
#<procedure:list?>
> (list? (list))
#t
> (list? one-through-four)
#t
> (list? 1)
#f
> (list? (cons 1 2))
#f
> (list? (cons 1 (list)))
#t
Homogeneous and Heterogeneous lists Lists of elements with a common type are
called homogeneous lists, while lists whose elements have no common type are termed
heterogeneous lists. For example, (1 2 3), ((1 2) (3 4 5)) are homogeneous lists,
while ((1 2) 3 ((4 5))) is a heterogeneous list. This distinction divides the List type into
two types: Homogeneous-List and Heterogeneous-List. The empty list belongs both to the
Homogeneous-List and the Heterogeneous-List types.
The Homogeneous-List type is a polymorphic type, with a type constructor List that
takes a single parameter. For example:
33
Chapter 1 Principles of Programming Languages
Question: Consider the expressions (cons 1 2) and (list 1 2). Do they have the
same value?
34
Chapter 1 Principles of Programming Languages
2. Selector list-ref: Selects the nth element of a list. Its type is [List*Number –>
T] or [List(T)*Number –> T].
3. Operator length:
Reductional (recursive, inductive) definition:
35
Chapter 1 Principles of Programming Languages
Consider Example 1.1 for implementing Newton’s method for computing square roots. The
implementation in that example computes a sequence of approximations, which is lost, once
a result is returned. Assume that we are interested in obtaining also the sequence of approx-
imations, for example, in order to observe it, measure its length, etc. The implementation
below returns the approximation sequence as a list.
36
Chapter 1 Principles of Programming Languages
2 3
In order to represent a labeled tree, the first element in every nesting level can represent
the root of the sub-tree. A leaf tree l is represented by the singleton list (l). A non-leaf
tree, as for example, the sorted number labeled tree in Figure 1.4 is represented by the list
(1 (0) (3 (2) (4))).
Example 1.5. An unlabeled tree operation – using the first representation, where a leaf tree
is not a list:
Signature: count-leaves(x)
Purpose: Count the number of leaves in an unlabeled tree (a selector):
** The count-leaves of an empty tree is 0.
37
Chapter 1 Principles of Programming Languages
0 3
2 4
38
Chapter 1 Principles of Programming Languages
managed by loop variables whose changing values determine loop exit. Loop constructs
provide abstraction of the looping computation pattern. Iteration is a central computing
feature.
Functional languages like the Scheme part introduced in this chapter do not posses
looping constructs like while. The only provision for computation repetition is repeated
function application. The question asked in this section is whether iteration by function call
obtains the advantages of iteration using loop constructs, as in other languages. We show
that recursive function call mechanism can simulate iteration. Moreover, the conditions
under which function call simulates iteration can be syntactically identified: A computing
agent (interpreter, compiler) can determine, based on syntax analysis of a procedure body,
whether its application can simulate iteration.
For that purpose, we discuss the computational processes generated by procedures.
We distinguish between procedure expression – a syntactical notion, to process – a
semantical notion. Recursive procedure expressions can create iterative processes. Such
procedures are called tail recursive.
Signature: factorial(n)
Purpose: to compute the factorial of a number ’n’.
This procedure follows the rule: 1! = 1, n! = n * (n-1)!
Type: [Number -> Number]
Pre-conditions: n > 0, an integer
Post-condition: result = n!
Example: (factorial 4) should produce 24
Tests: (factorial 1) ==> 1
(factorial 4) ==> 24
(define factorial
(lambda (n)
(if (= n 1)
1
(* n (factorial (- n 1))))
))
39
Chapter 1 Principles of Programming Languages
(define factorial
(lambda (n)
(fact-iter 1 1 n) ))
fact-iter:
Signature: fact-iter(product,counter,max-count)
Purpose: to compute the factorial of a number ’max-count’.
This procedure follows the rule:
counter = 1; product = 1;
repeat the simultaneous transformations:
product <-- counter * product, counter <-- counter + 1.
stop when counter > n.
Type: [Number*Number*Number -> Number]
Pre-conditions:
product, counter, max-count > 0
product * counter * (counter + 1) * ... * max-count = max-count!
Post-conditions: result = max-count!
Example: (fact-iter 2 3 4) should produce 24
Tests: (fact-iter 1 1 1) ==> 1
(fact-iter 1 1 4) ==> 24
(define fact-iter
(lambda (product counter max-count)
(if (> counter max-count)
product
(fact-iter (* counter product)
(+ counter 1)
max-count))))
(factorial 6)
(* 6 (factorial 5))
...
(* 6 (* 5 (...(* 2 factorial 1 )...)
(* 6 (* 5 (...(* 2 1)...)
...
(* 6 120)
720
40
Chapter 1 Principles of Programming Languages
We can see it in the trace information provided when running the procedure:
> (require (lib "trace.ss"))
> (trace factorial)
> (trace *)
> (factorial 5)
"CALLED" factorial 5
"CALLED" factorial 4
"CALLED" factorial 3
"CALLED" factorial 2
"CALLED" factorial 1
"RETURNED" factorial 1
"CALLED" * 2 1
"RETURNED" * 2
"RETURNED" factorial 2
"CALLED" * 3 2
"RETURNED" * 6
"RETURNED" factorial 6
"CALLED" * 4 6
"RETURNED" * 24
"RETURNED" factorial 24
"CALLED" * 5 24
"RETURNED" * 120
"RETURNED" factorial 120
120
>
Every recursive call has its own information to keep and manage – input and procedure-
code evaluation, but also a return information to the calling procedure, so that the calling
procedure can continue its own computation. The space needed for a procedure call evalu-
ation is called frame. Therefore, the implementation of such a sequence of recursive calls
requires keeping the frames for all calling procedure applications, which depends on the
value of the input. The computation of (factorial 6) requires keeping 6 frames simultane-
ously open, since every calling frame is waiting for its called frame to finish its computation
and provide its result.
41
Chapter 1 Principles of Programming Languages
That is, the iterative factorial computes its result using a looping construct: Repetition
of a fixed process, where repetitions (called iterations) vary by changing the values of
variables. Usually there is also a variable that functions as the loop variable. In contrast
to the evaluation process of the recursive factorial, the evaluation of a loop iteration does
not depend on its next loop iteration: Every loop iteration hands-in the new variable values
to the loop manager (the while construct), and the last loop iteration provides the returned
result. Therefore, all loop iterations can be computed using a fixed space, needed
for a single iteration. That is, the procedure can be computed using a fixed space, which
does not depend on the value of the input. This is a great advantage of looping
constructs. Their great disadvantage, though, is the reliance on variable value change, i.e.,
assignment.
In functional languages there are no looping constructs, since variable values cannot be
changed – No assignment in functional languages. Process repetition is obtained by
procedure (function) calls. In order to achieve the great space advantage of iterative looping
constructs, procedure calls are postponed to be the last evaluation action, which means
that once a procedure-call frame calls for a new frame, the calling frame is done, no further
actions are needed, and it can be abandoned. Therefore, as in the looping construct case,
every frame hands-in the new variable values to the next opened frame, and the last frame
provides the returned result. Therefore, all frames can be computed using a fixed
space, needed for a single frame.
A procedure whose body code includes a procedure call only as a last evaluation step1 , is
called iterative. If the evaluation application is “smart enough” to notice that a procedure
is iterative, it can use a fixed space for procedure-call evaluations, and enjoy the advantages
of iterative loop structures, without using variable assignment. Such evaluators are called
tail recursive.
Indeed, there is a single procedure call in the body of the iterative factorial, and it occurs
last in the evaluation actions, implying that it is an iterative procedures. Since Scheme ap-
plications are all tail recursive, the evaluation of (factorial 6) using the iterative version,
yields the following evaluation sequence:
(factorial 6)
(fact-iter 1 1 6)
(fact-iter 1 2 6)
...
(fact-iter 720 7 6)
720
1
There can be several embedded procedure calls, each occurs last on a different branching computation
path.
42
Chapter 1 Principles of Programming Languages
> (factorial 3)
"CALLED" factorial 3
"CALLED" fact-iter 1 1 3
"CALLED" * 1 1
"RETURNED" * 1
"CALLED" fact-iter 1 2 3
"CALLED" * 2 1
"RETURNED" * 2
"CALLED" fact-iter 2 3 3
"CALLED" * 3 2
"RETURNED" * 6
"CALLED" fact-iter 6 4 3
"RETURNED" fact-iter 6
"RETURNED" fact-iter 6
"RETURNED" fact-iter 6
"RETURNED" fact-iter 6
"RETURNED" factorial 6
6
In the first case – the number of deferred computations grows linearly with n. In the
second case – there are no deferred computations. A computation process of the first kind
is called linear recursive. A computation process of the second kind is called iterative.
In a linear recursive process, the time and space needed to perform the process, are
proportional to the input size. In an iterative process, the space is constant – it is the
space needed for performing a single iteration round. These considerations refer to the space
needed for procedure-call frames (the space needed for possibly unbounded data structures
is not considered here). In an iterative process, the status of the evaluation process is
completely determined by the variables of the procedure (parameters and local variables). In
a linear recursive process, procedure call frames need to store the status of the deferred
computations.
Note the distinction between the three notions:
43
Chapter 1 Principles of Programming Languages
procedure whose application does not create deferred computations can be performed as an
iterative process.
Typical form of iterative processes: Additional parameters for a counter and an
accumulator , where the partial result is stored . When the counter reaches some bound ,
the accumulator gives the result.
> (count2 4)
|(count2 4)
| (count2 3)
| |(count2 2)
| | (count2 1)
| | |(count2 0)
0| | |#<void>
1| | #<void>
2| |#<void>
3| #<void>
4|#<void>
44
Chapter 1 Principles of Programming Languages
Recursive FIB
Signature: (fib n)
Purpose: to compute the nth Fibonacci number.
This procedure follows the rule:
fib(0) = 0, fib(1) = 1, fib(n) = fib(n-1) + fib(n-2).
Type: [Number -> Number]
Example: (fib 5) should produce 5
Pre-conditions: n >= 0
Post-conditions: result = nth Fibonacci number.
Tests: (fib 3) ==> 2
(fib 1) ==> 1
(define fib
(lambda (n)
(cond ((= n 0) 0)
((= n 1) 1)
(else (+ (fib (- n 1))
(fib (- n 2)))))
))
The evaluation process generated by this procedure has a tree structure, where nested
forms lie on the same branch:
+-----------------(fib 5)----------------+
| |
+-----(fib 4)---------+ +-----(fib 3)---------+
| | | |
+--(fib 3)--+ +--(fib 2)-+ +-(fib 2)-+ (fib 1)
| | | | | | |
+-(fib 2)-+ (fib 1) (fib 1) (fib 0) (fib 1) (fib 0) 1
| | | | | | |
(fib 1) (fib 0) 1 1 0 1 0
1 0
The time required is proportional to the size of the tree, since the evaluation of (fib 5)
requires the evaluation of all fib forms. Hence, the time required is exponential in the
input of fib. The space required is proportional to the depth of the tree, i.e., linear in
45
Chapter 1 Principles of Programming Languages
the input.
Note: The exponential growth order applies to balanced (or almost balanced) trees. Highly
pruned computation trees can yield a smaller growth order.
Iterative FIB
(define fib
(lambda (n) (fib-iter 0 1 n)))
fib-iter:
Signature: fib-iter(current,next,count)
Purpose: to compute the nth Fibonacci number.
We start with current = 0, next = 1, and count as the Fibonacci goal,
and repeat the simultaneous transformation ’count’ times:
next <-- next + current, current <-- next,
in order to compute fib(count).
Type: [Number*Number*Number -> Number]
Example: (fib-iter 0 1 5) should produce 5
Pre-conditions: next = (n+1)th Fibonacci number, for some n >= 0;
current = nth Fibonacci number;
Post-conditions: result = (n+count)th Fibonacci number.
Tests: (fib-iter 1 2 3) ==> 5
(fib-iter 0 1 1) ==> 1
(define fib-iter
(lambda (current next count)
(if (= count 0)
current
(fib-iter next (+ current next) (- count 1)))
))
Given an amount A of money, and types of coins (5 agorot, 10 agorot, etc), ordered in
some fixed way. Compute the number of ways to change the amount A. Here is a rule:
Try it!
46
Chapter 1 Principles of Programming Languages
(define count-change
(lambda (amount)
(cc amount 5)))
(define cc
(lambda (amount kinds-of-coins)
(cond ((= amount 0) 1)
((or (< amount 0) (= kinds-of-coins 0)) 0)
(else (+ (cc (- amount
(first-denomination kinds-of-coins))
kinds-of-coins)
(cc amount
(- kinds-of-coins 1)))))))
(define first-denomination
(lambda (kinds-of-coins)
(cond ((= kinds-of-coins 1) 1)
((= kinds-of-coins 2) 5)
((= kinds-of-coins 3) 10)
((= kinds-of-coins 4) 25)
((= kinds-of-coins 5) 50))))
What kind of process is generated by count-change? Try to design a procedure that generates
an iterative process for the task of counting change. What are the difficulties?
− For the iterative factorial and Fibonacci processes: T ime(n) = O(n), but Space(n) =
O(1).
47
Chapter 1 Principles of Programming Languages
− For the tree recursive Fibonacci process: T ime(n) = O(C n ), and Space(n) = O(n).
Order of growth is an indication of the change in resources implied by changes in the
problem size.
− O(1) – Constant growth: Resource requirements do not change with the size of the
problem. For all iterative processes, the space required is constant, i.e., Space(n) =
O(1).
− O(n) – Linear growth: Multiplying the problem size multiplies the resources by
the same factor.
For example: if T ime(n) = Cn then
T ime(2n) = 2Cn = 2T ime(n), and
T ime(4n) = 4Cn = 2T ime(2n), etc.
Hence, the resource requirements grow linearly with the problem size.
A linear iterative process is an iterative process that uses linear time (T ime(n) =
O(n)), like the iterative versions of factorial and of fib.
A linear recursive process is a recursive process that uses linear time and space
(T ime(n) = Space(n) = O(n) ), like the recursive version of factorial.
− O(C n ) – Exponential growth: Any increment in the problem size, multiplies the
resources by a constant number.
For example: if T ime(n) = C n , then
T ime(n + 1) = C n+1 = T ime(n) ∗ C, and
T ime(n + 2) = C n+2 = T ime(n + 1) ∗ C, etc.
T ime(2n) = C 2n = (T ime(n))2 .
Hence, the resource requirements grow exponentially with the problem size. The
tree-recursive Fibonacci process uses exponential time.
− O(log n) – Logarithmic growth: Multiplying the problem size implies a constant
increase in the resources.
For example: if T ime(n) = log(n), then
T ime(2n) = log(2n) = T ime(n) + log(2), and
T ime(6n) = log(6n) = T ime(2n) + log(3), etc.
We say that the resource requirements grow logarithmically with the problem size.
− O(na ) – Power growth: Multiplying the problem size multiplies the resources by
a power of that factor.
For example: if T ime(n) = na , then
T ime(2n) = (2n)a = T ime(n) ∗ (2a ), and
T ime(4n) = (4n)a = T ime(2n) ∗ (2a ), etc.
Hence, the resource requirements grow as a power of the problem size. Linear grows
is a special case of power grows (a = 1). Quadratic grows is another special common
case (a = 2, i.e., O(n2 )).
48
Chapter 1 Principles of Programming Languages
(define exp-iter
(lambda (b counter product)
(if (= counter 0)
product
(exp-iter b
(- counter 1)
(* b product)))))
49
Chapter 1 Principles of Programming Languages
> even?
#<primitive:even?>
They can be defined via another primitive procedure, remainder (or modulo), as follows:
(define even?
(lambda (n)
(= (remainder n 2) 0)))
The complementary procedure to remainder is quotient, which returns the integer value
of the division: (quotient n1 n2) ==> n1/n2.
T ime(n) = Space(n) = O(log n), since fast-exp(b, 2n) adds a single additional multipli-
cation to fast-expr(b, n) (in the even case). In this approximate complexity analysis, the
application of primitive procedures is assumed to take constant time.
Example 1.9. – Greatest Common Divisors (GCD) (no contracts) (SICP 1.2.5)
The GCD of 2 integers is the greatest integer that divides both. The Euclid’s algorithm
is based on the observation:
Lemma 1.4.1. If r is the remainder of a/b, then: GCD(a, b) = GCD(b, r). Successive
applications of this observation yield a pair with 0 as the second number. Then, the first
number is the answer.
Proof. Assume a > b. Then a = qb + r where q is the quotient. Then r = a − qb. Any common
divisor of a and b is also a divisor of r, because if d is a common divisor of a and b, then
a = sd and b = td, implying r = (s − qt)d. Since all numbers are integers, r is divisible by d.
Therefore, d is also a common divisor of b and r. Since d is an arbitrary common divisor of
a and b, this conclusion holds for the greatest common divisor of a and b.
(define gcd
(lambda (a b)
(if (= b 0)
a
(gcd b (remainder a b)))))
Iterative process:
Space(a, b) = O(1).
T ime(a, b) = O(log(n)), where n = min(a, b). √
T ime(a,b)
The Time order of growth results from
√ the theorem: n ≥ F
√ ib(T ime(a, b)) = (Θ(C )/ 5)
which implies: T ime(a, b) ≤ log(n ∗ 5) = log(n) + log( 5) Hence: T ime(a, b) = O(log(n)),
where n = min(a, b).
50
Chapter 1 Principles of Programming Languages
Straightforward search:
(define smallest-divisor
(lambda (n)
(find-divisor n 2)))
(define find-divisor
(lambda (n test-divisor)
(cond ((> (square test-divisor) n) n)
((divides? test-divisor n) test-divisor)
(else (find-divisor n (+ test-divisor 1))))))
(define divides?
(lambda (a b)
(= (remainder b a) 0)))
(define prime?
(lambda (n)
(= n (smallest-divisor n))))
Based on the observation that if n is not a prime, then it must have a divisor less than or
equal than its square root.
√
Iterative process. T ime(n) = O( n), Space(n) = O(1).
Another algorithm, based on Fermat’s Little Theorem:
Theorem 1.4.2. If n is a prime, then for every positive integer a, (an ) mod n = a mod n.
The following algorithm picks randomly positive integers, less than n, and applies Fer-
mat’s test for a given number of times: expmod computes be mod n. It is based on the
observation:
(x ∗ y) mod n = ((x mod n) ∗ (y mod n)) mod n, a useful technique, as the numbers involved
stay small.
(define expmod
(lambda (b e n)
(cond ((= e 0) 1)
((even? e)
(remainder (square (expmod b (/ e 2) n))
n))
(else
(remainder (* b (expmod b (- e 1) n))
n)))))
51
Chapter 1 Principles of Programming Languages
The created process is recursive. The rate of time grows for expmod is T ime(e) = O(log(e)),
i.e., logarithmic grow in the size of the exponent, since: T ime(2 ∗ e) = T ime(e) + 2.
(define fermat-test
(lambda (n a)
(= (expmod a n n) a))))
(define fast-prime?
(lambda (n times)
(cond ((= times 0) t)
((fermat-test n (+ 2 (random (- n 2))))
(fast-prime? n (- times 1)))
(else #f))))
This is a common notion in mathematics, where we discuss notions like Σf (x), without
specifying the exact function f . Σ is an example of a higher order procedure. It introduces
the concept of summation, independently of the particular function whose values are being
summed. It allows for discussion of general properties of sums.
In functional programming (hence, in Scheme) procedures have a first class status:
In this section, we introduce higher-order procedures that have the first three features listed
above. In the next section (Section 1.3), we also introduce data structures that can include
procedures as their components.
52
Chapter 1 Principles of Programming Languages
Signature: sum-integers(a,b)
Purpose: to compute the sum of integers in the interval [a,b].
Type: [Number*Number -> Number]
Post-conditions: result = a + (a+1) + ... + b.
Example: (sum-integers 1 5) should produce 15
Tests: (sum-integers 2 2) ==> 2
(sum-integers 3 1) ==> 0
(define sum-integers
(lambda (a b)
(if (> a b)
0
(+ a (sum-integers (+ a 1) b)))))
2. sum-cubes:
Signature: sum-cubes(a,b)
Purpose: to compute the sum of cubic powers of
integers in the interval [a,b].
Type: [Number*Number -> Number]
Post-conditions: result = a^3 + (a+1)^3 + ... + b^3.
Example: (sum-cubes 1 3) should produce 36
Tests: (sum-cubes 2 2) ==> 8
(sum-cubes 3 1) ==> 0
(define sum-cubes
(lambda (a b)
(if (> a b)
0
(+ (cube a) (sum-cubes (+ a 1) b)))))
3. pi-sum:
Signature: pi-sum(a,b)
Purpose: to compute the sum
1/(a*(a+2)) + 1/((a+4)*(a+6)) + 1/((a+8)*(a+10)) + ...
53
Chapter 1 Principles of Programming Languages
(define pi-sum
(lambda (a b)
(if (> a b)
0
(+ (/ 1 (* a (+ a 2))) (pi-sum (+ a 4) b)))))
(define <name>
(lambda (a b)
(if (> a b)
0
(+ (<term> a)
(<name> (<next> a) b)))))
The 3 procedures can be abstracted by a single procedure, where the empty slots <term>
and <next> are captured by formal parameters that specify the <term> and the <next>
functions, and <name> is taken as the defined function sum:
sum:
Signature: sum(term,a,next,b)
Purpose: to compute the sum of terms, defined by <term>
in predefined gaps, defined by <next>, in the interval [a,b].
Type: [[Number -> Number]*Number*[Number -> Number]*Number -> Number]
Post-conditions: result = (term a) + (term (next a)) + ... (term n),
where n = (next (next ...(next a))) =< b,
(next n) > b.
Example: (sum identity 1 add1 3) should produce 6,
where ’identity’ is (lambda (x) x)
Tests: (sum square 2 add1 2) ==> 4
(sum square 3 add1 1) ==> 0
54
Chapter 1 Principles of Programming Languages
(define sum
(lambda (term a next b)
(if (> a b)
0
(+ (term a)
(sum term (next a) next b)))))
Using the sum procedure, the 3 procedures above have different implementations (same
contracts):
(define sum-integers
(lambda (a b)
(sum identity a add1 b)))
(define sum-cubes
(lambda (a b)
(sum cube a add1 b)))
(define pi-sum
(lambda (a b)
(sum pi-term a pi-next b)))
(define pi-term
(lambda (x)
(/ 1 (* x (+ x 2)))))
(define pi-next
(lambda (x)
(+ x 4)))
Discussion: What is the advantage of defining the sum procedure, and defining the three
procedures as concrete applications of sum?
1. First, the sum procedure prevents duplications of the computation pattern of sum-
ming a sequence elements between given boundaries. Duplication in software is bad
for many reasons, that can be summarized by management difficulties, and lack of
abstraction – which leads to the second point.
2. Second, and more important, the sum procedure expresses the mathematical notion of
sequence summation. Having this notion, further abstractions can be formulated, on
top of it. This is similar to the role of interface in object-oriented languages.
55
Chapter 1 Principles of Programming Languages
(define add-dx
(lambda (x) (+ x dx)))
For example:
> (integral cube 0 1 0.01)
0.2499875
> (integral cube 0 1 0.001)
0.249999875
True value: 1/4.
(define sequence-operation
(lambda (operation start a b)
(if (> a b)
start
(operation a (sequence-operation operation start (+ a 1) b)))))
56
Chapter 1 Principles of Programming Languages
where operation stands for any binary procedure, such as +, *, -, and start stands for the
neutral (unit) element of operation, i.e., 0 for +, and 1 for *. For example:
> (sequence-operation * 1 3 5)
60
> (sequence-operation + 0 2 7)
27
> (sequence-operation - 0 3 5)
4
> (sequence-operation expt 1 2 4)
2417851639229258349412352
> (expt 2 (expt 3 4))
2417851639229258349412352
lambda forms can be evaluated during computation. Such closures are termed anony-
mous procedures, since they are not named. Anonymous procedures are useful whenever
a procedural abstraction does not justify being named and added to the global environment
mapping.
For example, in defining the pi-sum procedure, the procedure (lambda (x) (/ 1 (* x
(+ x 2)))) that defines the general form of a term in the sequence is useful only for this
computation. It can be passed directly to the pi-sum procedure:
(define pi-sum
(lambda (a b)
(sum (lambda (x) (/ 1 (* x (+ x 2))))
a
(lambda (x) (+ x 4))
b)))
The body of the pi-sum procedure includes two anonymous procedures that are created at
runtime, and passed as arguments to the sum procedure. The price of this elegance is that
the anonymous procedures are redefined in every application of pi-sum.
The integral procedure using an anonymous procedure:
57
Chapter 1 Principles of Programming Languages
(define integral
(lambda (f a b dx)
(* (sum f
(+ a (/ dx 2.0))
(lambda (x) (+ x dx))
b)
dx)))
Note that once the next procedure is created anonymously within the integral procedure,
the dx variable can become a parameter rather than a globally defined variable.
Parameters, scope, bound and free variable occurrences: A lambda form includes
parameters and body . The parameters act as variable declarations in most program-
ming languages. They bind their occurrences in the body of the form, unless, there is a
nested lambda form with the same parameters. The body of the lambda form is the scope
of the parameters of the lambda form. The occurrences that are bound by the parameters
are bound occurrences, and the ones that are not bound are free. For example, in the
integral definition above, in the lambda form:
58
Chapter 1 Principles of Programming Languages
(lambda (f a b dx)
(* (sum f
(+ a (/ dx 2.0))
(lambda (x) (+ x dx))
b)
dx))
The parameters, or declared variables, are f, a, b, dx. Their scope is the entire lambda
form. Within the body of the lambda form, they bind all of their occurrences. But the
occurrences of +, *, sum are free . Within the inner lambda form (lambda (x) (+ x
dx)), the occurrence of x is bound, while the occurrence of dx is free.
A define form also acts as a variable declaration. The defined variable binds all of its free
occurrences in the rest of the code. We say that a defined variable has a universal scope.
In order to compute the value of the function for given arguments, it is useful to define two
local variables:
a = 1+xy
b = 1-y
then:
f (x, y) = xa2 + yb + ab
The local variables save repeated computations of the 1 + xy and 1 − y expressions. The
body of f (x, y) can be viewed as a function in a and b (since within the scope of f , the
occurrences of x and y are already bound). That is, the body of f (x, y) can be viewed as
the function application
f (x, y) = f _helper(1 + xy, 1 − y)
where for given x, y:
f _helper(a, b) = xa2 + yb + ab
The Scheme implementation for the f _helper function:
(lambda (a b)
(+ (* x (square a))
(* y b)
(* a b)))
59
Chapter 1 Principles of Programming Languages
f (x, y) can be implemented by applying the helper function to the values of a and b, i.e.,
1 + xy and 1 − y:
(define f
(lambda (x y)
((lambda (a b)
(+ (* x (square a))
(* y b)
(* a b)))
(+ 1 (* x y))
(- 1 y))
))
The important point is that this definition of the polynomial function f provides the behavior
of local variables: The initialization values of the parameters a, b are computed only once,
and substituted in multiple places in the body of the f_helper procedure.
Note: The helper function cannot be defined in the global environment, since it has x
and y as free variables, and during the evaluation process, while the occurrences of a and b
are replaced by the argument values, x and y stay unbound:
The let abbreviation: A conventional abbreviation for this construct, which internally
turns into a nested lambda form application, is provided by the let special operator. That is,
a let form is just a syntactic sugar for application of a lambda form. Such abbreviations
are termed derived expressions. The evaluation of a let form creates an anonymous closure
and applies it.
(define f
(lambda ( x y)
(let ((a (+ 1 (* x y)))
(b (- 1 y)))
(+ (* x (square a))
60
Chapter 1 Principles of Programming Languages
(* y b)
(* a b)))))
2. The values of the <expi>s are replaced for all free occurrences of their corresponding
<vari>s, in the let body.
These rules result from the internal translation to the lambda form application:
Therefore, the evaluation of a let form does not have any special evaluation rule (unlike
the evaluation of define and lambda forms, which are true special operators).
Notes about let evaluation:
3. The <expi>s reside in the outer scope, where the let resides. Therefore, variable
occurrences in the <expi>s are not bound by the let variables, but by binding oc-
currences in the outer scope.
4. The <body> is the let scope. All variable occurrences in it are bound by the let
variables (substituted by their values).
61
Chapter 1 Principles of Programming Languages
> (define x 5)
> (+ (let ( (x 3) )
(+ x (* x 10)))
x)
==>
> (+ ( (lambda (x) (+ x (* x 10)))
3)
x)
38
> (define x 5)
> (define y (+ (let ( (x 3) )
(+ x (* x 10)))
x))
> y
38
> (+ y y)
76
In evaluating a let form, variables are bound simultaneously. The initial values are
evaluated before all let local variables are substituted.
> (define x 5)
> (let ( (x 3) (y (+ x 2)))
(* x y))
21
First, let us see how repeated lambda abstractions create procedures (closures) that create
procedures.
62
Chapter 1 Principles of Programming Languages
− A further lambda abstraction of the lambda form: (lambda (y) (lambda (x) (+ x
y y))), evaluates to a procedure with formal parameter y, whose application (e.g.,
((lambda (y) (lambda (x) (+ x y y))) 3) evaluates to a procedure in which y is
already substituted, e.g., <Closure (x) (+ x 3 3)>.
> (define y 0)
> (define x 3)
> (+ x y y)
3
> (lambda (x) (+ x y y))
#<procedure>
> ((lambda (x) (+ x y y)) 5)
5
> (lambda (y) (lambda (x) (+ x y y)))
#<procedure>
> ((lambda (y) (lambda (x) (+ x y y))) 2)
#<procedure>
> (((lambda (y) (lambda (x) (+ x y y))) 2) 5)
9
> (define f (lambda (y) (lambda (x) (+ x y y)) ) )
> ((f 2) 5)
9
> (define f (lambda (y) (lambda (x) (+ x y y))))
> ((f 2) 5)
9
> ((lambda (y) ((lambda (x) (+ x y y)) 5)) 2)
9
> ((lambda (x) (+ x y y)) 5)
5
>
The following examples demonstrate high order procedures that return procedures (closures)
as their returned value.
Average damping is average taken between a value val and the value of a given
function f on val. Therefore, every function defines a different average. This function-
specific average can be created by a procedure generator procedure:
63
Chapter 1 Principles of Programming Languages
average-damp:
Signature: average-damp(f)
Purpose: to construct a procedure that computes the average damp
of a function average-damp(f)(x) = (f(x) + x )/ 2
Type: [[Number -> Number] -> [Number -> Number]]
Post-condition: result = closure r,
such that (r x) = (average (f x) x)
Tests: ((average-damp square) 10) ==> 55
((average-damp cube) 6) ==> 111
(define average-damp
(lambda (f)
(lambda (x) (average x (f x)))))
For example:
> ((average-damp (lambda (x) (* x x))) 10)
55
> (average 10 ((lambda (x) (* x x)) 10))
55
((average-damp cube) 6)
111
> (average 6 (cube 6))
111
> (define av-damped-cube (average-damp cube))
> (av-damped-cube 6)
111
Example 1.13. The derivative function:
For every number function, its derivative is also a function. The derivative of a function
can be created by a procedure generating procedure:
derive:
Signature: derive(f dx)
Purpose: to construct a procedure that computes the derivative
dx approximation of a function:
derive(f dx)(x) = (f(x+dx) - f(x) )/ dx
Type: [[Number -> Number]*Number -> [Number -> Number]]
Pre-conditions: 0 < dx < 1
Post-condition: result = closure r, such that
(r y) = (/ (- (f (+ x dx)) (f x))
64
Chapter 1 Principles of Programming Languages
dx)
Example: for f(x)=x^3, the derivative is the function 3x^2,
whose value at x=5 is 75.
Tests: ((derive cube 0.001) 5) ==> ~75
(define derive
(lambda (f dx)
(lambda (x)
(/ (- (f (+ x dx)) (f x))
dx))))
The value of (derive f dx) is a procedure!
> (define cube (lambda (x) (* x x x)))
Closure creation time – Compile time vs runtime Creation of a closure takes time
and space. In general, it is preferred that expensive operations are done before a program is
run, i.e., in static compile time. When a closure (i.e., a procedure created at runetime)
depends on other closures, it is preferred that these auxiliary closures are created at compile
time. The following example, presents four procedures that differ in the timing of the
creation of the auxiliary procedures.
Example 1.14 (Compile time vs runtime creation of auxiliary closures).
Given a definition of a procedure that approximates the derivative of a function of one
argument, f: x:
(define dx 0.00001)
Signature: derive(f)
Type: [[Number -> Number] -> [Number -> Number]]
(define derive
(lambda (f)
(lambda(x)
(/ (- (f (+ x dx))
(f x))
dx))))
65
Chapter 1 Principles of Programming Languages
The following four procedures approximate the nth derivative of a given function of one
argument, f:
Signature: nth-deriv(f,n)
Type: [[Number -> Number]*Number -> [Number -> Number]]
(define nth-deriv
(lambda (f n)
(lambda (x)
(if (= n 0)
(f x)
( (nth-deriv (derive f) (- n 1)) x)))
))
(define nth-deriv
(lambda (f n)
(if (= n 0)
f
(lambda (x)
( (nth-deriv (derive f) (- n 1)) x)))
))
(define nth-deriv
(lambda (f n)
(if (= n 0)
f
(nth-deriv (derive f) (- n 1)) )
))
(define nth-deriv
(lambda (f n)
(if (= n 0)
f
(derive (nth-deriv f (- n 1)) ))
))
The four procedures are equivalent in the sense that they evaluate to the same function.
However, there is a crucial difference in terms of the creation time of the auxiliary closures,
i.e., the closures used by the final n-th derivative procedure. Consider for example:
66
Chapter 1 Principles of Programming Languages
1. The first and second versions create no closure at compile time. Every application
of fourth-deriv-of-five-exp to an argument number, repeatedly computes all the
lower derivative closures prior to the application. In the first version a closure is
created even for the 0-derivative, and a test for whether the derivative rank is zero
is performed as part of the application to an argument number, while in the second
version this test is performed prior to the closure creation, and no extra closure is
created for the 0 case.
2. The third and forth versions create four closures – the first, second, third and fourth
derivatives of five-exp at compile time. No closure is created when fourth-deriv-of-five-exp
is applied to an argument number. They differ in terms of the process recursive-
iterative properties: The third version is iterative, while the fourth is recursive.
67
Chapter 1 Principles of Programming Languages
The problem is, of course, that once the condition does not evaluate to #f, the value of the
overall expression is the value of the consequence, even if the consequence value is #f. But
in this case the or operator evaluates the alternative. The or form can be corrected by
delaying the computation of the consequence and the alternative, using lambda abstraction:
Delayed computation for obtaining iterative processes Recall the distinction be-
tween recursive to iterative processes. A procedure whose computations are recursive is
characterized by a storage (frames in the Procedure-call stack) that stores the future com-
putations, once the recursion basis is computed. In Section 1.4 we provided iterative versions
for some recursive procedures. But they were based on new algorithms that compute the
same functions. In many cases this is not possible.
A recursive procedure can be turned iterative by using high order procedures that store
the delayed computations. These delayed computation procedures are created at run-time
and passed as arguments. Each delayed computation procedure corresponds to a frame in
the Procedure-call stack. The resulting iterative procedure performs the recursion base, and
calls itself with an additional delayed computation argument procedure.
Example 1.16. Turning the factorial recursive procedure iterative by using procedures that
store the delayed computations:
Signature: factorial(n)
Purpose: to compute the factorial of a number ’n’.
This procedure follows the rule: 1! = 1, n! = n * (n-1)!
Type: [Number -> Number]
Pre-conditions: n > 0, an integer
Post-condition: result = n!
68
Chapter 1 Principles of Programming Languages
(define factorial
(lambda (n)
(if (= n 1)
1
(* n (factorial (- n 1))))
))
We turn it into a procedure fact$ that accepts an additional parameter for the delayed
(future) computation. This procedure will be applied once the factorial procedure apply,
i.e., in the base case. Otherwise, it will be kept in a run-time created closure that waits for
the result of factorial on n − 1:
(define fact$
(lambda (n cont)
(if (= n 0)
(cont 1)
(fact$ (- n 1) (lambda (res) (cont (* n res)))))
))
The cont parameter stands for the delayed computation (cont stands for continuation).
If the given argument is the base case, the continuation is immediately applied. Overawes,
a closure is created that “stores” the cont procedure and waits until the result on n − 1 is
obtained, and then applies. The created continuations are always procedures that just apply
the delayed computation and a later continuation to the result of a recursive call:
69
Chapter 1 Principles of Programming Languages
( (lambda (res)
( (lambda (x) x) (* 3 res)))
(* 2 res)))
(* 1 res))))
==>
( (lambda (res)
( (lambda (res)
( (lambda (res)
( (lambda (x) x) (* 3 res)))
(* 2 res)))
(* 1 res)))
1)
==>
( (lambda (res)
( (lambda (res)
( (lambda (x) x) (* 3 res)))
(* 2 res)))
1)
==>
( (lambda (res)
( (lambda (x) x) (* 3 res)))
2)
==>
( (lambda (x) x) 6)
==>
6
The continuations are built one on top of the other, in analogy to the frames in the
Procedure-call stack. In Section ?? we present this idea as the Continuation Passing Style
(CPS ) approach.
average-damp:
Signature: average-damp(f)
70
Chapter 1 Principles of Programming Languages
71
Chapter 1 Principles of Programming Languages
We see that the closure ⟨closure (x) x⟩ can belong to the types
[Number –> Number],
[Boolean –> Boolean],
[[Number –> Number] –> [Number –> Number]].
− The type of the identity lambda expression and procedure is [T –> T].
− The type of the expression (lambda (f x) (f x)) is [[T1 –> T2]*T1 –> T2].
− The type of the expression (lambda (f x) (f (f x))) is [[T –> T]*T –> T].
− The type of op-damp is [[T1 –> T2]*[T1*T2 –> T3] –> [T1 –> T3]].
Signature: pairself(x)
Purpose: Construct a pair of a common component.
type: [T -> Pair(T,T)]
72
Chapter 1 Principles of Programming Languages
Example 1.18.
Signature: firstFirst(pair)
Purpose: Retrieve the first element of the first element of a pair.
Type: Pair(Pair(T1,T2),T3) -> T1
(define firstFirst (lambda(pair)
(car (car pair))))
Example 1.19.
Signature: member(el, pair)
Purpose: Find whether the symbol el occurs in pair.
Type: [Symbol*Pair(T1,T2) -> Boolean]
(define member (lambda (el pair)
(cond ((and (pair? (car pair))
(member el (car pair))) #t)
((eq? el (car pair)) #t)
((and (pair? (cdr pair))
(member el (cdr pair))) #t)
((eq? el (cdr pair)) #t)
(else #f))))
Note that before recursively applying member, we test the calling arguments for their type.
Indeed, according to the Design by Contract policy, it is the responsibility of clients to call
procedures with appropriate arguments, i.e., arguments that satisfy the procedure’s pre-
condition and type. Procedures that take over the responsibility for testing their arguments
demonstrate defensive programming .
Below is a better version, that uses a type-test procedure. The type of the arguments to
member is not checked in the procedure.
73
Chapter 1 Principles of Programming Languages
3. Run time created procedures (anonymous procedures) that save the population of a
name space with one-time needed procedures.
4. Local variables.
74
Chapter 1 Principles of Programming Languages
6. Polymorphic procedures.
8. Delayed computation.
75
References Principles of Programming Languages
References
[1] H. Abelson and G.J. Sussman. Structure and Interpretation of Computer Programs,
2nd edition. The MIT Press, 1996.
[2] M. Felleisen, R.B. Findler, M. Flatt, and S. Krishnamurthi. How to Design Programs.
The MIT Press, 2001.
[3] D.P. Friedman and M. Wand. Essentials of Programming Languages, 3nd edition. The
MIT Press, 2008.
[9] OMG. The UML 2.0 Superstructure Specification. Specification Version 2, Object
Management Group, 2007.
[10] L.C. Paulson. ML for the Working Programmer, 2nd edition. Cambridge University
Press, 1996.
[11] L. Sterling and E.Y. Shapiro. The Art of Prolog: Advanced Programming Techniques,
2nd edition. The MIT Press, 1994.
76