There are three mechanisms for combining simple ideas to form more complex ideas found in every powerful programming language:
- primitive expressions
- means of combination
- means of abstraction
Programming deals with procedures and data (which are almost the same thing in Lisp). Procedures manipulate data.
Expressions#
- The REPL reads and expression, evaluates it, prints the result, and repeats.
- A number is one kind of primitive expression.
- An application of a primitive procedure is one kind of compound expression.
- A combination denotes procedure application by a list of expressions inside parentheses. The first element is the operator; the rest are the operands.
- List combinations use prefix notation (the operator comes first).
- Combinations can be nested: an operator or operand can itself be another combination.
Naming and the Environment#
- Scheme names thing with the
definekeyword. This is the simplest means of abstraction. - The name-value pairs are stored in an environment.
Evaluating Combinations#
- To evaluate a combination:
- Evaluate the subexpressions of the combination.
- Apply the procedure (value of the leftmost subexpression, the operator) to the arguments (values of other subexpressions, the operands).
- Before evaluating a combination, we must first evaluate each element inside it.
- Evaluation is recursive in nature—one of its steps is invoking itself.
- The evaluation of a combination can be represented with a tree.
- Recursion is a powerful technique for dealing with hierarchical, tree-like objects.
- To end the recursion, we stipulate the following:
- Numbers evaluate to themselves.
- Built-in operators evaluate to machine instruction sequences.
- Names evaluate to the values associated with them in the environment.
(define x 3)does not apply define to two arguments; it is not a combination.- Exceptions such as these are special forms. Each one has its own evaluation rule.
Compound Procedures#
- Procedure definition is a powerful technique for abstraction.
- A squaring procedure:
(define (square x) (* x x)). - This is a compound procedure given the name “square.”
- General form of a procedure definition:
(define (name formal-parameters) body). - If the body contains more than one expression, each is evaluated in sequence and the value of the last one is returned
The Substitution Model for Procedure Application#
This is the substitution model:
To apply a compound procedure to arguments, evaluate the body of the procedure with each formal parameter replaced by the corresponding argument.
An example of procedure application:
(f 5)
(sum-of-squares (+ 5 1) (* 5 2))
(sum-of-squares 6 10)
(+ (square 6) (square 10))
(+ 36 100)
136Applicative order versus normal order#
- The example above used applicative order: evaluate all the subexpressions first, then apply the procedure to the arguments.
- With normal order, operands are substituted in the procedure unevaluated. Only when it reaches primitive operators do combinations reduce to values.
- An example of normal-order procedure application:
(f 5)
(sum-of-squares (+ 5 1) (* 5 2))
(+ (square (+ 5 1)) (square (* 5 2)))
(+ (* (+ 5 1) (+ 5 1)) (* (* 5 2) (* 5 2)))
(+ (* 6 6) (* 10 10))
(+ 36 100)
136- Here, normal order causes some combinations to be evaluated multiple times.
Conditional Expressions and Predicates#
- To make more useful procedures, we need to be able to conduct tests and perform different operations accordingly.
- We do case analysis in Scheme using
cond. condexpressions work by testing each predicate. The consequent expression of the first clause with a true predicate is returned, and the other clauses are ignored.- A predicate is an expression that evaluates to true or false.
- The symbol
elsecan be used as the last clause—it wills always evaluate to true. - The
ifconditional can be used when there are only to cases. - Logical values can be combined with
and,or,not. The first two are special forms, not procedures, because they have short-circuiting behavior.
Exercises: 1.1, 1.2, 1.3, 1.4, 1.5
Example: Square Roots by Newton’s Method#
But there is an important difference between mathematical functions and computer procedures. Procedures must be effective.
- In mathematics, you can define square roots by saying. “The square root of $x$ is the nonnegative $y$ such that $y^2 = x$.” This is a not a procedure.
- Mathematical functions describe things (declarative knowledge); procedures describe how to do things (imperative knowledge).
- Declarative is what is, imperative is how to.
Procedures as Black-Box Abstractions#
- Each procedure in a program should accomplish an identifiable task that can be used as a module in defining other procedures.
- When we use a procedure as a “black box”, we are concerned with what it is doing but not how it is doing it.
- This is called procedural abstraction. Its purpose is to suppress detail.
- A user should not need to know how the procedure is implemented in order to use it.
Local names#
- The choice of names for the procedure’s formal parameters should not matter to the user of the procedure.
- Consequentially, the parameter names must be local to the body of the procedure.
- The name of a formal parameter doesn’t matter; it is a bound variable. The procedure binds its formal parameters.
- If a variable is not bound, it is free.
- The expression in which a binding exists is called the scope of the name. For parameters of a procedure, this is the body.
- Using the same name for a bound variable and an existing free variable is called capturing the variable.
- The names of free variables do matter for the meaning of the procedure.
Internal definitions and block structure#
- Putting a definition in the body of a procedure makes it local to that procedure. This nesting is called block structure.
- Now we have two kinds of name isolation: formal parameters and internal definitions.
- By internalizing auxiliary procedures, we can often eliminate bindings by allowing variables to remain free.
- Scheme uses lexical scoping, meaning free variables in a procedure refer to bindings in enclosing procedure definitions.