Inneh˚ all

advertisement
TDDC74 Programming: Abstraction and Modelling
Supplement Document
SICP, Chapter 01
Innehåll
1 Overview: SICP 01 – Procedural Abstraction
2
2 Week 1: SICP 1.1-1.2
2
2.1
10 Things to Remember . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2
2.2
No variables named “X” . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2
2.3
Operations versus Commands . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3
2.4
The Rules of Evaluation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3
2.4.1
Evaluating Simple Expressions . . . . . . . . . . . . . . . . . . . . . . . . . .
4
2.4.2
Evaluating Compound Expressions . . . . . . . . . . . . . . . . . . . . . . . .
4
2.4.3
Evaluating Variable Definitions . . . . . . . . . . . . . . . . . . . . . . . . . .
5
2.4.4
Evaluating Procedure Definitions . . . . . . . . . . . . . . . . . . . . . . . . .
5
2.4.5
Evaluating Procedure Application by Substitution . . . . . . . . . . . . . . .
5
2.4.6
Evaluating “Special Forms” . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6
2.5
Variables versus Procedures . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6
2.6
Defining Recursive Procedures
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7
2.7
Internal Definitions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
8
2.8
Procedures & Processes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
8
2.8.1
Recursive Procedure Definitions
9
2.8.2
Recursive Procedures: Recursive & Iterative Processes . . . . . . . . . . . . . 10
2.9
. . . . . . . . . . . . . . . . . . . . . . . . .
How to Describe a Procedure: contracts & notation . . . . . . . . . . . . . . . . . . . 12
3 Week 2: SICP 1.3
14
3.1
10 Things to Remember . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
3.2
LAMBDA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
3.3
LET . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
3.3.1
Use local variables with let when necessary . . . . . . . . . . . . . . . . . . . 16
3.4
DEFINE & “Explicit” LAMBDA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
3.5
Procedures that Return Procedures . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
3.6
Writing Higher-order Procedures . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
4 Vocabulary
20
1
1
Overview: SICP 01 – Procedural Abstraction
This document contains supplemental information for PRAM; this material should be studied and
understood in addition to the material in SICP, Chapter 01.
Note that some of the issues and concepts described here will probably only make sense after
attending the first Lectures, beginning to read Chapter 01 of SICP, and doing some work on the
first Lab Assignment. In particular, this document is divided into two parts – the first part covers
issues relevant to Week 1 (SICP 1.1-1.2) and the second part covers issues relevant to Week 2 (SICP
1.3).
2
Week 1: SICP 1.1-1.2
2.1
10 Things to Remember
For the first Lab Assignment, we want to make sure you remember the following 10 things:
1. In Scheme/Racket, parentheses are significant; they (almost always) indicate a procedure
application.
2. Most expression forms follow the same (recursive) rules of evaluation: an operator is applied
to operands after the operands are fully evaluated (ie, after they return a value).
3. Special forms have special rules of evaluation.
4. Operations return values; commands have effects (and return undefined values).
5. The evaluation of every expression returns a (single) value.
6. In Scheme/Racket, define is used to “permanently” bind values to names. Until we get
to chapter 03 in SICP, define is the only Scheme/Racket primitive we will use to create
“permanent” bindings (or to permanently change bindings) between names and values. All
other bindings (such as parameter names to argument values) are temporary; that is, you
should assume such bindings do not exist once a procedure finishes evaluation.
7. For variable definitions, the second argument to define is fully evaluated to return a single
value – before any value is bound to a name.
8. Substitute (evaluated) procedure arguments for parameter names in the body of the procedure
before evaluating the body of the expression.
9. Recursive procedure definitions can generate either recursive or iterative processes.
10. Recursive processes involve “deferred operations” – and cost more than iterative processes.
2.2
No variables named “X”
Consider which of the following two programs you find easier to read, easier to figure out what it
is supposed to do, and easier to debug (if necessary):
(define (ssq x y)
(define (s x)
(* x x))
(+ (s x) (s y)))
(define (sum-of-squares num1 num2)
(define (square base)
(* base base))
(+ (square num1) (square num2)))
2
The difference should be clear, even in this trivial example.
Many bugs “magically vanish” when coders go through and replace too-short abbreviations with
meaningful names. Not only that, but single-letter names make code extremely difficult for other
people to read. Students should not expect us to accept – or help anyone debug – code with variablenames consisting of a single letter or other cryptic abbreviations. You may lose credits on quizzes
for such mistakes.
Yes, we have heard all the arguments “in favor” of using very short names. But when we sit and
review page after page of student-code, this one-letter habit is exhausting. So, even if there are
students who remain unconvinced about the advantages of meaningful names, please do it for the
code you write in this course.
2.3
Operations versus Commands
Most of the procedures we study and implement in this course will be operations – not commands.
Operations return values; commands have effects (and return undefined values).
Operations are analogous to mathematical functions – they always return the same value for a
given argument. To help students remember the distinction between operations and commands, we
will sometimes refer to operations as reporters. That is, a reporter “reports something” (a value) –
it doesn’t “do something” (to change the world).
Consider:
Reporter : “Tell me, how many beers are left in the refrigerator?”
Command : “Replace the beers you took from the refrigerator!”
It should be clear that commands are for their effect – and it is usually less important what a
command “reports back” (ie, its returned value).1 In particular, we typically use commands when
we want to either change some aspect of the world or change some aspect of a program. Thus, we
say that reporters “leave everything the way they found it” when they are done reporting.
Note that although the beer example above should vividly highlight the difference, it is misleading
in an important way. In the universe of mathematics and programming, we try to design systems
so that reporters – operations – will always return the same answer to the same question about the
same data. But in the “world of beer” we sometimes do want to “change the world” in a permanent
way. The example above should serve as a warning for the kinds of problems that can arise when
we later start mixing reporters and commands. However, in the first part of the course, we will be
dealing with situations where reporters work reliably: “Tell me, what is 2 plus 2?”
2.4
The Rules of Evaluation
When we evaluate expressions in Scheme/Racket, in most cases the goal is to return a single value.
Below is a summary of evaluation for the following situations:
• Evaluating Simple Expressions
• Evaluating Compound Expressions
1
Yes, it would be nice if the person reports back, “done” in response to the command “Replace the beers you took
from the refrigerator!”
3
• Evaluating Variable Definitions
• Evaluating Procedure Definitions
• Evaluating Procedure Application by Substitution
• Evaluating “Special Forms”
2.4.1
Evaluating Simple Expressions
So, if we evaluate a number, we get back the number:
1 → 1
Note that if you type a string of simple expressions at the Scheme/Racket prompt and then evaluate
them, the results from all of them will be returned.
1 2 3 → 1 2 3
1 (+ 1 2) 3 → 1 3 3
Note: There is an important point here. When you hit the “return” key, Scheme/Racket returns
an independent value for each expression it evaluates. In the case above, 1 (+ 1 2) 3 is three
expressions: the simple expression 1, the compound expression (+ 1 2) (which consists of the
expressions +, 1 and 2), and the simple expression 3.
In the example above, the Scheme/Racket interpreter returns the results of evaluating each of them;
it is not the case that Scheme/Racket is treating everything on the same prompt-line as a “single
expression that is returning multiple values.” There is usually no need to type multiple expressions
on the same prompt line (in fact, it is often a source of errors for beginners.)
Do not spend time trying to write expressions or design procedures that return multiple values!
2.4.2
Evaluating Compound Expressions
If we want to do anything more complex, we need to use parentheses. Remember that in most cases,
an “open parenthesis” means: “apply the first thing after the parenthesis (operator/procedure) to
the remaining elements within the parentheses (the operands/arguments).”
(+ 1 2 3 4 5) → 15
In general, Scheme/Racket evaluates expressions by:
1. evaluating arguments (operands)
2. applying an operator to the result (the “returned value”) of step 01.
Note: if one or more operands are, themselves, parenthetical expressions, the evaluation rules are
recursively applied to those expressions.
(+ 1 (+ (+ 2 3) (+ 4 5))) → (+ 1 (+ 5 9)) → (+ 1 14) → 15
You can think of this as “reducing operands down to their primitive elements”– and then applying
operations to the results. For now, the primitive operands will be numbers; the primitive operators
will be mathematical operations.
4
For most Scheme/Racket expressions, you should think of sub-expressions “at the
same level of indentation” as occurring “in parallel.”
(* (+ 2 3)
(+ 3 4)
(+ (/ 5 6)
(- 7 8)))
In Racket (as opposed to older Scheme/Racket versions), evaluation is defined as from left to right.
For special form expressions, there are different (special ) rules of evaluation, and these often involve
a particular sequence of evaluation; note that mostly it is fairly intuitive whether there is a specified
sequence of evaluation or not. A series of cond statements, for example, must happen in a specific
order. And in general, this issue is not something you need to worry about much. (This note is
mostly for people experienced in other languages who tend to think of everything in terms of
creating “sequences of instructions”.)
2.4.3
Evaluating Variable Definitions
Expressions that use define are “special forms.” When evaluating a variable definition:
1. Evaluate define’s second argument
2. “Bind” the result to define’s first argument (the name)
(define foobar (+ 1 2 (* 3 4) 5)) → (define foobar 20) → <void>
Answer: first, evaluate the second argument to define – in this case, the result is 20. Then, and
only then, “bind” the result to define’s first argument (the “name”) – in this case, foobar. (Note
that define does not return a value; it binds a name to a value. Therefore, the “returned value”
of define is “undefined” or ¡void¿.)
2.4.4
Evaluating Procedure Definitions
When evaluating a procedure definition:
• Do not evaluate the parameters or body of the define expression; treat them as a “procedure
object”
• Do “bind” the “procedure object” to define’s first argument (the name)
For now, you do not need to worry to much about “what happens” when a procedure is defined.
It is enough if you think of it as follows: the parameters (if any) and the body of the procedure are
treated as a “procedure object” – and that procedure-object is bound to the name you specify.
2.4.5
Evaluating Procedure Application by Substitution
The rules for applying Scheme/Racket primitive procedures to arguments are covered in the evaluation rules above. When evaluating an application of a compound procedure (ie, a procedure that
has been defined by a user of Scheme/Racket), use the following steps:
1. Evaluate any compound expressions that are passed arguments
5
2. Substitute the argument values for the parameter names in the body of the procedure
3. Follow the usual rules of evaluation
Assume the following definition:
(define (foobar num)
(* num (+ num num))) → <void>
What is the substitution model of evaluating the following:
(foobar (+ 1 (+ 2 3)) → ???
Substitution steps for evaluation:
1. (foobar (+ 1 (+ 2 3)))
2. (foobar (+ 1 5))
3. (foobar 6)
4. (* 6 (+ 6 6))
5. (* 6 12)
6. 72
If the procedure definition includes a reference to additional compound procedures, recursively
follow the same rules.
2.4.6
Evaluating “Special Forms”
There are slightly different rules of evaluation for different “special forms,” such as if, cond, or,
and. In most of these cases, the main difference is when – and, in some cases, whether – argument
expressions are evaluated. See SICP for more details.
2.5
Variables versus Procedures
Although we will loosely refer to “variables” and “procedures” as different things, it is important
to understand that the word “variable” has a technical meaning. A variable is a name that can be
bound to different (“varying”) values. This means, in fact, that the name foobar in both of the
following examples is technically a variable:
(define foobar (/ 22 7))
(define foobar
(lambda (num1) (/ 22 num1)))
The latter define is a short-cut for the following full expression, which shows more clearly that
foobar is a variabel bound to a procedure as the form of a lambda-expression.
(define foobar (lambda (num1) (/ 22 num1)))
The only difference is that, in the first case, Scheme/Racket completely evaluates the expression and
returns a single value to be bound to the name – and, in the second case, Scheme/Racket treats
the expression as something to be evaluated after the name is applied.
6
2.6
Defining Recursive Procedures
It usually takes a bit of practice to begin “thinking recursively.” Recursive procedures typically
have four components:
Base argument: the simplest (“base case”) possible argument the procedure is expected
to work on
Base value: the value the procedure returns when it operates on the base case argument
Value construction: if the argument is not the base case, this is the operation performed
to build the appropriate returned value.
Argument reduction: if the argument is not the base case, this is the operation the procedure performs to reduce the current argument (one step) towards the base argument
You should be able to identify these elements in recursive code – and you should be able to identify
them in written problem descriptions (in order to then turn those descriptions into code).
Example 1: For the procedure definition below, identify the base argument, the base value, the
value construction, and the argument reduction.
(define (foobar num)
(if (= num 0)
0
(+ num
(foobar (- num 1)))))
;
;
;
;
base argument: 0
base value: 0
value construction: + num
argument reduction: (- num 1)
Example 2: “Define a procedure that takes two numbers and, by recursively incrementing by 1,
returns the sum of them.”
Base argument: 0
Base value: num2
Value construction: + 1
Argument reduction: (- num1 1)
(define (sum num1 num2)
(if (= num1 0)
num2
(+ 1 (sum
(- num1 1) num2))))
Notice that there is a relationship between the base value and the recursive construction:
• If the base value is 0, the value construction typically uses +
• If the base value is 1, the value construction typically uses *
This is because these values do not change the result of these operations. Note that there are subtle
differences for more complex forms of recursive procedures (and data), but this should help for now.
Tip for writing recursive procedures – the first two elements of a recursive procedure definition are
almost always:
• testing for the base argument, and
• returning the base value if the actual argument equals the base argument
7
2.7
Internal Definitions
It is often very helpful to make procedure-definitions local to a larger procedure that calls them. However, we are also trying to help students understand essential aspects of functional programming,
so students are not allowed to use define to create local variable bindings within a procedure.
This means that this kind of local procedure-definition is acceptable:
(define (my-proc num1)
(define (local-proc loc-num)
(+ num1 loc-num))
(local-proc 5))
(my-proc 2) → 7
<- internal procedure definition
But, even though it is possible, this kind of local variable-definition is not acceptable:
(define (my-proc num1)
(define loc-num 5)
(+ num1 loc-num))
(my-proc 2) → 7
<- internal variable definition
In principle, define may only be used to create NEW variable or procedure bindings.
Generally, define will look within the local scope for an existing binding with the same name:
if it finds such a local binding, it will return an error; if it does not find such a local binding, it
creates one.
WARNING!
In violation of what has just been said, in practice most Scheme/Racket implementations do allow
the use of define at the top-level to assign a new value to an existing name. The motivation
is this: the top-level is assumed to be an interactive environment for program-revision and
debugging – and preventing a user from using define to change an existing binding at the toplevel seems too restrictive. Nonetheless, repeat the following mantra:
“define is used to create NEW bindings . . .
Students with experience programming in non-functional languages should study the following
examples closely!
Repeat this mantra:
define may only be used to create new variable- or procedure-bindings
define may only be used to create new variable- or procedure-bindings
define may only be used to create new variable- or procedure-bindings
...
Local variables are preferrably introduced with let or let*. We will read about those below.
2.8
Procedures & Processes
Below is some useful information about procedures and processes.
8
2.8.1
Recursive Procedure Definitions
A recursive procedure is a procedure that includes a reference to itself in its definition. So, a
procedure is recursively defined if its name appears anywhere in the body of its definition. For now,
this means: if you see the procedure name anywhere in the body of the procedure definition, you
can be almost certain it is a recursively defined procedure, even if an application of the procedure
generates an error.
Once you are clear on this, there is an additional subtlety. The appearance of a parameter name in
the body of a procedure is not a recursive call. So, it should be fairly obvious that the call to baz
in the body of the procedure below is not recursive:
(define (foobar baz)
(+ baz baz))
Obs! This is true even if that parameter name is the same as the procedure name. So, in the example
below, foobar is used as both a procedure name and as the parameter name. And an application
of the procedure will successfully return a value.
(define (foobar foobar)
(+ foobar foobar))
; this is usually VERY VERY BAD!
(foobar 3) → 6
Conceptually, this is just like the earlier example: Scheme/Racket “knows” to bind the parametername foobar to the argument of 3 – and then evaluate the body of the expression with that
value.
This works because the procedure name foobar is considered to be part of the “global environment”
– and the parameter name foobar is considered to be “local” to the body of the procedure. So,
Scheme/Racket is not “confused” about what to do with the different versions of foobar.
So, yes, it is possible to create program definitions that use the same name for a procedure, a
parameter, and/or a variable. BUT even though this is possible, it is usually VERY BAD practice
and results in errors or very confusing programs.
However, even though you should not deliberately write such code, you may still encounter it –
and should be able to reason about it. In the example above, the “substitution model” shows us
that both instances of foobar in the body are names for parameters. Thus, the procedure is not
recursively defined.
(foobar 3)
(+ 3 3)
6
THE SUBSTITUTION MODEL WILL HELP YOU, HERE. “Substitution” only involves replacing
parameter names (in the body) with actual arguments. So, even though both of the definitions below
are “bad” (for different reasons), you should be able to do a substitution evaluation and determine
a) which procedure will add the value of variable (parameter) bindings, and b) which procedure
will try to add a (recursive) procedure call to a variable-binding.2
2
You may wonder why Scheme/Racket “allows” the “very very bad” option. The answer is that there are special
situations where Scheme/Racket programmers want this flexibility.
9
(define (foobar foobar)
(+ foobar foobar))
; this is usually VERY VERY BAD!
(foobar 3) → ???
(define (foobar2 foo)
(+ foobar2 foo))
(foobar2 3) → ???
2.8.2
; this will generate an error
Recursive Procedures: Recursive & Iterative Processes
Recursive procedures can generate either recursive or iterative processes.
• The key feature of a recursive process is that there are “deferred operations”; one way to
think about this is that the returned value is built “on the way back ” from the recursive calls.
• The key feature of a recursive definition that generates an iterative process is that there are
no “waiting operations”; one way to think about this is that the value is built “on the way
along” through recursive calls.
To understand this difference, imagine you and your friends are going to book a room for a party.
When you go to book the room, the company-representative asks you for 10 kronor – so you decide
to collect 1 kronor from each of your 10 friends (one at a time). Consider two different procedures
– both of the procedures are recursive (because they refer to themselves), but only one of them
generates a recursive process:
Recursive process This approach involves “waiting for the total amount to accumulate on its
way back to the person who started the process.”
1. First check to see whether the “desired amount” is at zero.
2. If it is, you should turn in zero kronor to the company.
3. Otherwise
(a) take out one kronor from your pocket, and be prepared to add it to the number of
kronor you will get back from your friend
(b) tell your friend that you need “desired amount” minus 1 kronor, and
(c) wait for a value to come back from your friend (and when it does, you should add
your 1 kroner to the money you got back and hand over the total to the person who
first asked you for money, i.e., the company-representative.)
The friend will do exactly what you did:
1. First check to see whether the “desired amount” is at zero.
2. If it is, the friend should return zero kronor to you.
3. Otherwise, she should:
(a) take out one kronor from her pocket, and be prepared to add it to the number of
kronor she will get back from her friend
(b) tell friend that she needs her “desired amount” (which is 9) minus 1 kronor, and
(c) wait for a value to come back from her friend (and when it does, she should add
her 1 kroner to the money she got back and return the total to the person who first
asked her for money, i.e., you)
10
The process above is recursive (or “self-similar”) in the sense that each procedure-call is
similar to the first one: the one from the company-representative. In each case, the caller is
waiting for a returned value.
In the next example, the process of each procedure-call is not the same as the first one. The
company-representative still asks for (and waits for) 10 kronor, but each procedure-call after
that does not wait for a returned value. The company-representative gets the total amount
of money from the last procedure (person) called.
Iterative process This approach involves “accumulating the total amount as it moves away from
the person who started the process.”
Starting with zero as the “starting-amount”, you should:
• First check to see whether the “starting amount” is equal to the “desired amount”
• If it is, you should turn in the “desired-amount” to the company.
• Otherwise, you should:
– Add one kronor to the “starting-amount”
– Pass the new “starting-amount” on to your friend
– and then you are free of responsibility!
The friend will do exactly what you did:
• First check to see whether the “starting amount” is equal to the “desired amount”
• If it is, she should turn in the “desired-amount” to the company.
• Otherwise, she should:
– Add one kronor to the “starting-amount”
– Pass the new “starting-amount” on to her friend
– and then she is also free of responsibility!
The difference between the “shape” of these processes is most clear if you do “substitution modeling”
of the steps.
Recursive process:
(define (rec-collect-fee desired-amount)
(if (= desired-amount 0)
0
(+ 1 (rec-collect-fee (- desired-amount 1)))))
(rec-collect-fee 3) →
(+
(+
(+
(+
(+
(+
(+
(+
(+
3
1
1
1
1
1
1
1
1
1
(rec-collect-fee (- 3 1)))
(rec-collect-fee 2))
(+ 1 (rec-collect-fee (- 2 1))))
(rec-collect-fee 1))
(+ 1 (+ 1 (rec-collect-fee (- 1 1)))))
(+ 1 (+ 1 (rec-collect-fee 0))))
(+ 1 (+ 1 0)))
(+ 1 1))
2)
Iterative process:
11
(define (iter-collect-fee starting-amount desired-amount)
(if (= starting-amount desired-amount)
desired-amount
(iter-collect-fee (+ starting-amount 1) desired-amount)))
(iter-collect-fee 0 3) →
(iter-collect-fee
(iter-collect-fee
(iter-collect-fee
(iter-collect-fee
(iter-collect-fee
(iter-collect-fee
3
(+ 0 1) 3)
1 3)
(+ 1 1) 3)
2 3)
(+ 2 1) 3)
3 3)
Note that in the case of iter-collect-fee, the base value is not 0 or 1. It is possible to rewrite
the procedure so that it uses 0 as its base value, but such code would result in a more obscure
algorithm. Here we have chosen to highlight the fact that many recursive procedures that generate
iterative processes do so by making use of a counter (in this case, it is starting-amount that
functions as the counter).
2.9
How to Describe a Procedure: contracts & notation
Voluntary We will not in general require students to use this notation to describe programs for
the labs. However, students may encounter problems described with this notation, so it is worth
spending a few minutes just to become familiar with it.
When we define procedures, we want to emphasize several things:
• Whether it is an operation or a command
• Arguments: what type(s) and how many
• If the procedure is an operation, the type of the returned values
For this, we will use a short-hand way to specify a procedure. As an example, consider a description
of a procedure to square its arguments.
Procedure Specifications
Operation: (square num-to-be-squared) : (number → number)
Purpose: computes the square of a number
Example: (square 4) → 16
Things to notice about the first line of this specification.
Before the colon It gives the type of procedure – in this case, an operation. It also
gives the name of the procedure (in this case, square). Finally, it gives the name of the
parameter(s); in this case, num-to-be-squared. (Usually it will not be necessary to use
such long names for parameter-names. We are exaggerating in this example for a couple
of reasons. First, we want these examples to be fairly clear. Second, it is important to
use meaningful names when you write code, even when those names are not long and
complicated.)
After the colon It shows the number and type of arguments (in this case, one argument
that is a number). And it shows the type of the returned value (after the arrow).
12
Note also that sometimes we will impose restrictions on the form of your solutions. So, for example,
we may require that you use a particular technique – or even particular sub-procedures.
Now we look at an example of a primitive that takes several arguments:
Operation: (+
Purpose: adds
Example: (+ 1
Example: (+ 1
num1 num2 ...): (number x number x ... → number)
together the arguments and returns the resulting value
2) → 3
2 3 4) → 10
Note that “dots” mean that there can be any number of additional arguments.
13
3
Week 2: SICP 1.3
During Week 2 we introduce higher-order functions.
3.1
10 Things to Remember
For the second week, we want to make sure you remember the following 10 things:
1. Creating and naming a procedure are two conceptually different things
2. In Scheme/Racket, we can create useful nameless procedures
3. lambda means “make a procedure”
4. The evaluation of a simple lambda expression returns a procedure as its value – a nameless
procedure
5. let is “syntactic sugar” for a lambda application with specific arguments
6. Using let with the name of an existing variable name does not permanently or globally
“rebind” that variable
7. There are two syntactic styles for define – the “sugared” version “hides” the two operations (of naming and procedure-creation), and the “explicit lambda” version highlights the
similarity with define-ing variables
8. When reading code, remember that procedures that return procedures always include at least
two lambda*s in their definition
9. A Scheme/Racket procedure that returns a procedure is returning a lambda expression
10. When writing higher-order procedures, it is helpful to think of the “outside” procedure as
“setting some default values” for the inside (“returned”) procedure
3.2
LAMBDA
The most important thing to remember about lambda is that it means “make a procedure.”
For example, the expression below makes a procedure that takes an argument (double-me) and
doubles it.
(lambda (double-me) (* 2 double-me))
It may be easier to see if the formatting is changed slightly:
(lambda (double-me)
(* 2 double-me))
<- make-procedure" and parameter
<- body of the procedure
The second thing to remember about lambda is that the evaluation of a lambda expression returns
a procedure as its value.
For example, evaluating the following expression will only return a procedure:
(lambda (double-me) (* 2 double-me)) →
#<procedure-object>
If you want the lambda procedure to be applied to actual arguments, the lambda expression itself
must:
14
• be enclosed in parentheses, and
• include specific arguments within the enclosing parentheses.
For example, evaluating the expression below will a) return a procedure that multiples its argument
by 2, and b) apply the procedure to the argument 5.
((lambda (double-me) (* 2 double-me)) 5)
It may be easier to see if the formatting is changed slightly:
((lambda (double-me)
(* 2 double-me))
5)
<- make-procedure" and parameter
<- body of the procedure
<- argument to procedure
Obs! Do not confuse these two things:
(lambda (double-me) (* 2 double-me))
((lambda (double-me) (* 2 double-me)))
The first one simply returns a procedure of a single parameter. The second one tries to apply a
procedure of a single parameter – but it is an application in which no argument are supplied, so an
error results.
3.3
LET
The most important thing to remember about let is that it is “syntactic sugar” for a lambda
application with specific arguments. That is, it is simply a different syntactic form that highlights
“what is happening.”
And what is happening? Well, for a lambda expression, the specific arguments come at the end
(after the lambda expression). This parallels the way arguments come after any ordinary procedure:
(sqrt 5) → 2.23606797749979
On the other hand, let begins by binding values to parameter-names – and then evaluating some
expression. We can see the various parameter-bindings “at the top” of the code – and then read
the body of the code already knowing those parameter-bindings:
(let ((double-me 5)) <- bind double-me parameter to 5
(* 2 double-me))
<- evaluate double-me in body of procedure
Keep in mind that let only makes sense in the case where it is used to bind parameter-names to
specific arguments. So, for example, since there are no explicit argument values to bind to double-me
in the following expression, there is no way to create a meaningful let expression out of it:
(lambda (double-me)
(* 2 double-me))
On the other hand, this next lambda expression is an application with specific arguments, so we
can create an equivalent let expression:
((lambda (double-me)
(* 2 double-me))
5)
(let ((double-me 5))
(* 2 double-me))
Remember, there is nothing “special” about let – it is just a convenience for the programmer. In
fact, when Scheme/Racket evaluates a let expression, it simply transforms it (internally) into a
lambda application.
15
Remember also that at this point, the only way we know how to permanently change a variablebinding is with define. So, using let with the name of an existing variable name does not permanently or globally “rebind” that variable; the let binding only exists within the scope of the let
statement, only for the duration of evaluating the let expression – and the variable bound with
define is untouched.
(define *my-var* 3)
*my-var* → 3
(let ((*my-var* 10))
(+ *my-var* 10)) → 20
*my-var* → 3
Note: yes, we can create let expressions at the top-level – that is, without putting them inside
define expressions.
3.3.1
Use local variables with let when necessary
If you want to have local variables, use let3 . It might be useful for readability, or when you need
to store temporary values.
Compare these two versions:
Version 1:
(if (> (compute-nearest-prime n) n)
(compute-nearest-prime n)
n)
Version 2:
(let
((closest-prime (compute-nearest-prime n)))
(if (> closest-prime n)
closest-prime
n))
Both are declarative, and the code tells us what we want - the biggest of a number n and its closest
prime - rather than how to compute it. However, version 1 calculates the prime twice, if it happens
to be bigger than n. In version 2, we temporarily store the value and can re-use it.
3
Technically, these are equivalent to generating a procedure and then applying it. You will see more of this in lab
3.
16
3.4
DEFINE & “Explicit” LAMBDA
NOT IN LAB 1 This is discussed in chapter 1 of SICP, but in PRAM we allow the usage of
”sugared” define notation only starting with lab 2, since the notation created some confusion. The
same goes for higher-order procedures, which are treated below.
It is often convenient to use the “sugared” version of define:
(define (foo num)
(* num num))
But always remember that this is just “sugar” for the “explicit” lambda version:
(define foo
(lambda (num)
(* num num)))
Most of the time, it does not matter which form you use. However, you should be able to see which
definitions are the same – and which are not. Also, later in the course we will use techniques where
we must use the “explicit lambda” form of define.
Tip: if you see lambda in a definition, be careful! Double-check to see whether the procedure is
using the “explicit lambda” format – or whether it is meant to return a procedure.
3.5
Procedures that Return Procedures
The most important thing to remember about procedures that return procedures is that they always
include at least two lambda*s in their definition. This is true even if the second lambda is “implied”
(as in the “sugared” version of define).
These procedures are identical – and both will return procedures when applied to arguments:
(define (power-maker pwr)
(lambda (par1)
(expt par1 pwr)))
(define power-maker
(lambda (pwr)
(lambda (par1)
(expt par1 pwr)))
In a sense, it is very easy to understand “what is returned” by a Scheme/Racket procedure that
returns a procedure: a lambda expression. So, just as there are procedures that return numbers as
values, we now have procedures that return lambda expressions as values. We strongly suggest that
you mentally “draw a box” around the returned procedure (lambda expression) of a higher- order
procedure.
Either of the procedures above will return the “boxed” procedure below:
(lambda (pwr)
(expt par1 pwr))
One good way to distinguish between the “outside” procedure and the “inside” (or “returned”)
procedure is that arguments to the outside procedure can be a way to “set some default values”
for the inside (“returned”) procedure.
So, conceptually this application
(power-maker 2)
Returns the boxed (procedure) value below:
(lambda (pwr)
(expt par1 2))
And the returned procedure – ie, the returned lambda expression – is evaluated like any other
lambda expression.
17
3.6
Writing Higher-order Procedures
It is easy to get “lost” when first learning to implement higher-order procedures, especially when
they are intended to return procedures. Below we offer some brief tips for how to approach writing
such procedures.
Imagine you are asked to implement pred-maker, a procedure of one argument that will return a
predicate of two arguments. Arguments to pred-maker will be mathematical predicates; arguments
to the returned procedure will be numbers. The idea is that pred-maker will be a machine for
creating special-purpose predicates.
(pred-maker =) →
(pred-maker >) →
<procedure> ; returned procedure is a predicate for testing equality
<procedure> ; returned procedure is a predicate for testing greater
((pred-maker =) 2 2) →
((pred-maker =) 3 2) →
((pred-maker >) 3 2) →
#t
#f
#t
(define are-they-equal? (pred-maker =))
(are-they-equal? 2 2) → #t
(are-they-equal? 3 2) → #f
→
<void>
Given the specification and examples, how to approach writing pred-maker?
First, keep in mind that there are two procedures – pred-maker and the procedure it returns.
When constructing pred-maker, start by creating a “skeleton” of the code
(define (pred-maker <argument> )
<body> ))
We know from the specification that pred-maker is a procedure of one argument, so begin with
specifying that. In particular, choose a meaningful parameter name – something that reflects what
the arguments will be. In this case, the arguments will be mathematical predicates, so:
(define (pred-maker math-pred)
<body> ))
Now, pred-maker will return a procedure. For now, you will use an explicit lambda expression to
do this. And we know that the returned procedure will take two arguments.
(define (pred-maker math-pred)
(lambda (<argument> <argument> )
<body> ))
And then we give the paramaters meaningful names.4
(define (pred-maker math-pred)
(lambda (num1 num2)
<body> ))
What do we have at this point? We have pred-maker, a procedure that will take a predicate as an
argument – and which will return a procedure that accept two numbers arguments.5
4
In this trivial example, it may seem silly to emphasize the naming – especially since the names we can choose
aren’t terribly “meaningful.” However, as programs get more complex, it will be possible to choose more and more
meaningful names, and it will make a huge difference for improving your ability to create, understand, and debug
code.
5
Technically, of course, we have not implemented anything in this code to check that arguments are the correct
type. We leave out such details for this example.
18
Now, for the last part. It is important at this point to focus on what the returned procedure is
intended to do. In this case, it is supposed to be a predicate that compares its two arguments. So,
the <body> of the returned procedure will work something like this:
<compare> num1 num2
<return #t or #f>
And what is <compare> – it is the predicate math-pred that is passed as an argument to the outer
procedure, pred-maker.
(define (pred-maker math-pred)
(lambda (num1 num2)
(math-pred num1 num2)))
And whenever a predicate is applied to its arguments, either #t or #f is returned. So, we are done.
19
4
Vocabulary
You will often be hearing the terms and phrases below: lectures, quizzes, and lab assignments will
assume you are familiar with the terms. Note, however, that we will not be asking questions of the
form, “Describe in words what the term abstraction means.”
Abstraction Roughly, “abstraction” allows us to think about and work with large units without
worrying about the details of those units. Procedural abstraction, for example, allows us to
separate “what something does” from the details of “how something is done.” Data abstraction
allows us to separate “what the data is” from “how the data is represented.” Thus, naming
is a very powerful abstraction technique that allows us to forget the details that the name
stands for – and simply use the name. More importantly, the abstraction mechanisms of a
language allow us to build larger programs from smaller programs so that we can think in
terms of the problem-domain that interests us.
Application In Scheme/Racket, procedures are applied to arguments. We still speak of a procedure “application” even if there are no arguments. These are both applications: (foo 3) and
(foobar)
Argument In a procedure application, the arguments are the actual values bound to the parameter
names. In the following example, 4 is the argument: (sqrt 4). (Obs! This may seem obvious,
but we will soon see examples where argument values look like parameter names.)
Command A command is a procedure that has an “effect”; that is, it makes a change that
lasts beyond the evaluation of the command (the change is “permanent”). Commands return
“unspecified” values – often indicated by <void> or “void”. (See also imperative.)
Compound procedure This is a procedure that is not built-in to Scheme/Racket. When we
talk about compound procedures we will usually mean those that are written by users of
Scheme/Racket; but we will also see that programs can create procedures.
Eager Evaluation This is the default evaluation for Scheme/Racket expressions: fully evaluate
the arguments to a procedure and then apply the procedure to them. (Also called applicative
order evaluation.)
Evaluation “Reducing” an expression down to a single value.
Expression For now, you can think of an “expression” as a “unit that returns a value when
evaluated.” A single number is an expression – and so is a “compound expression” (a sequence
of expressions grouped with parentheses). Note: expressions are sometimes called Symbolic
Expressions or S-expressions.
Foo Used very generally as a sample name for absolutely anything, especially programs and files.
See also bar, baz, qux, quux, and garply in The Hacker’s Dictionary (aka, “the jargon file”)
(http://www.outpost9.com/reference/jargon/jargon toc.html)
Forms Forms are expressions that are meant to be evaluated. Also called combinations.
Imperative (literally, to command ) Imperative-style programming relies on giving commands that
“permanently” change the state of the world – or the state of some procedure(s). Commands
are very useful for certain programming tasks, but it is often difficult to control and debug
imperative programs. We will study commands and imperative programming in detail in SICP
03.
20
Lazy Evaluation This is “evaluation on demand” – that is, evaluating expressions to return values
only when the expressions are actually called. We will not explore this model in any detail
until SICP, chapter 03. (Also called normal order evaluation.)
Operation (or “reporter”) An operation is a procedure that returns a value. Operations are
analogous to mathematical functions – they always return the same value for a given argument. To help students remember the distinction between operations and commands, we will
sometimes refer to operations as reporters. (In this course, most of the procedures we study
and implement will be operations.)
Operators and operands This is another way to talk about procedures and arguments.
Parameter In a procedure definition, the parameters are the “placeholders” or “slots” for arguments. In the following example, num1 is the parameter name:
(define (foo num1)
(* num1 num1))
Predicate (or “recognizer”) A predicate tests its argument(s) and returns true (#t) or false
(#f).
(number? 2) → #t
Primitive Procedure A procedure that is built-in to Scheme/Racket. (Note: in languages such
as Scheme/Racket, we don’t usually make a distinction between the “original primitives”
and the ones that we build and use ourselves. So you will often hear people use the term
“primitive” when they talk about the procedures they have written themselves as part of a
larger program.)
Procedure body All the code in a compound procedure definition that is not the name or parameters.
Procedure For now, you can think of a procedure as a function. (Obs! The term “procedure”
is used in a number of different ways in different languages and traditions. In this course, a
procedure is either an operation or a command.)
Special Forms A “special form” is a form that doesn’t follow the standard evaluation rules (see,
for example, and or if). Note: technically, a “special form” consists of expressions of “special
primitives” plus their arguments; so, for example, this expression is a special form: (define
pi 3.14) – but in practice most people will refer to the primitive as the special form (example:
“define is a special form”).
Scope The “space” (or “region”) within which a name/value binding is in effect. Scheme/Racket
uses lexical (or “textual”) scope; thus, it is possible to see a binding’s region by examining
the indentation of the code.
Extent The “time” that a name/value binding is in effect. Some bindings are permanent (such as
those created with define) and some are temporary (such as argument/parameter bindings).
Thunk A “thunk” is a procedure with no arguments. (Note: it may not be obvious why thunks
are interesting or important, but we will be examining them more closely later in the course.)
(define (my-three) (+ 1 2))
(my-three) → 3
Variables A variable is the name that is bound to a value. That is, the value associated with a
name can “vary.” In the context of this course, we will sometimes loosely contrast a variable
with a procedure. (Note: as with many terms in computer science, different communities and
21
traditions use words to mean slightly different things. For those students with experience in
other languages, we do not use “variable” here to mean a “storage location in memory.”)
22
Download