Imagine a language in which we cannot use names to refer to computational objects. As we write more and more complex programs, keeping track of the details of each step of computation would get increasingly difficult and inconvenient. Thus, we assign values, which are the computational object, to variables, identified by a name, by using
define. This is Racket's simplest means of abstraction.
Though you have seen various variable and procedure definitions scattered throughout the previous sections, we have not yet formally taught how to use
To start off, here are a few example expressions that use
define. Try these out in the Racket interpreter to see what they do.
-> (define x 5) -> (define (square x) (* x x)) -> (define y (square 3)) -> (define z (+ x y))
The general form of a variable definition is as follows:
(define [name] [value])
[name] represents a variable to which values are assigned to. For example:
-> (define x 5) -> x 5
x is a variable, and
5 is its value.
[value] can be replaced with any type of value, even expressions. An important property of variable definitions is that the value of the definition is completely evaluated before being assigned to its variable.
-> (define x (+ 5 5)) -> x 10
(+ 5 5)? Because when we define
x, we must first evaluate the expression
(+ 5 5) to its simplest form,
10. We then assign
(define x (/ 1 0))?
Procedure definition is an even more powerful abstraction technique than variable definition, in which we can give a name to a compound operation and consequently refer to it as a unit. Let's start with a simple example by defining the
(define (square x) (* x x))
We can understand this in the following way:
(define (square x) ( * x x)) To square x, multiply x with x.
The general form of a procedure definition is as follows:
(define ([name] [formal parameters]) [body])
Notice how a significant difference this has from a variable definition is that the name and parameters are bound by parentheses. Recall that, besides quotes, a set of parentheses represents a procedure call. We can translate this by saying: When we call
[formal parameters], we will do
foothat takes in no arguments, and returns
An important property of procedure definitions is that the body of the procedure is not evaluated until the procedure is called. This means that when we define
square, we do not yet know that we need to multiply
x by itself. Only when we call
square on some number, say
3, do we know that we have to call
(* 3 3).
(define (x) (/ 1 0))?
One thing to pay attention to when creating compound procedures is naming. We need to be very careful when naming our procedures and formal parameters. Racket will not accept multiple definitions, which means that any procedure already defined cannot be used as the name of a compound procedure or a formal parameter. This is an example of a compound procedure definition that is NOT ALLOWED (try it in your interpreter to see why):
(define (foo sent word) (word sent word))
In both instances in the body, which
word are we referring to, the parameter or the built-in procedure?
Returning to the concept of nesting expressions, we can also nest procedures within other procedure definitions. As you did in Homework 0-1, we can define the procedure
sum-of-squares by using the procedure
square in its definition:
(define (sum-of-squares x y) (+ (square x) (square y)))
This roughly translates to: When we call
y, we will add the
x to the
(define foo 10)
foois the variable name.
10is the value assigned to
(define (square x) (* x x))
squareis the procedure name.
xis its only formal parameter.
(* x x)is its body.
-> (square 3) 9
squareis the procedure, and is the operator of this expression.
3is the argument to
square, and is the operand of this expression.
9is the return value.
defineas a means of abstraction
The possibility of associating values and operations to symbols and later retrieving them means that the Racket interpreter must have some form of memory to keep track of these associative pairs. We call this memory the environment, which we will expand more on in Lesson 8.