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