Variable Basics
As in other languages, in Common Lisp variables are named places that can hold a value. However, in Common Lisp, variables aren’t typed the way they are in languages such as Java or C++. That is, you don’t need to declare the type of object that each variable can hold. Instead, a variable can hold values of any type and the values carry type information that can be used to check types at runtime. Thus, Common Lisp is dynamically typed--type errors are detected dynamically. For instance, if you pass something other than a number to the **+**
function, Common Lisp will signal a type error. On the other hand, Common Lisp is a strongly typed language in the sense that all type errors will be detected—there’s no way to treat an object as an instance of a class that it’s not.3
All values in Common Lisp are, conceptually at least, references to objects.4 Consequently, assigning a variable a new value changes what object the variable refers to but has no effect on the previously referenced object. However, if a variable holds a reference to a mutable object, you can use that reference to modify the object, and the modification will be visible to any code that has a reference to the same object.
One way to introduce new variables you’ve already used is to define function parameters. As you saw in the previous chapter, when you define a function with **DEFUN**
, the parameter list defines the variables that will hold the function’s arguments when it’s called. For example, this function defines three variables—x
, y
, and z
--to hold its arguments.
(defun foo (x y z) (+ x y z))
Each time a function is called, Lisp creates new bindings to hold the arguments passed by the function’s caller. A binding is the runtime manifestation of a variable. A single variable—the thing you can point to in the program’s source code—can have many different bindings during a run of the program. A single variable can even have multiple bindings at the same time; parameters to a recursive function, for example, are rebound for each call to the function.
As with all Common Lisp variables, function parameters hold object references.5 Thus, you can assign a new value to a function parameter within the body of the function, and it will not affect the bindings created for another call to the same function. But if the object passed to a function is mutable and you change it in the function, the changes will be visible to the caller since both the caller and the callee will be referencing the same object.
Another form that introduces new variables is the **LET**
special operator. The skeleton of a **LET**
form looks like this:
(let (variable*)
body-form*)
where each variable is a variable initialization form. Each initialization form is either a list containing a variable name and an initial value form or—as a shorthand for initializing the variable to **NIL**
--a plain variable name. The following **LET**
form, for example, binds the three variables x
, y
, and z
with initial values 10, 20, and **NIL**
:
(let ((x 10) (y 20) z)
...)
When the **LET**
form is evaluated, all the initial value forms are first evaluated. Then new bindings are created and initialized to the appropriate initial values before the body forms are executed. Within the body of the **LET**
, the variable names refer to the newly created bindings. After the **LET**
, the names refer to whatever, if anything, they referred to before the **LET**
.
The value of the last expression in the body is returned as the value of the **LET**
expression. Like function parameters, variables introduced with **LET**
are rebound each time the **LET**
is entered.6
The scope of function parameters and **LET**
variables—the area of the program where the variable name can be used to refer to the variable’s binding—is delimited by the form that introduces the variable. This form—the function definition or the **LET**
--is called the binding form. As you’ll see in a bit, the two types of variables—lexical and dynamic—use two slightly different scoping mechanisms, but in both cases the scope is delimited by the binding form.
If you nest binding forms that introduce variables with the same name, then the bindings of the innermost variable shadows the outer bindings. For instance, when the following function is called, a binding is created for the parameter x
to hold the function’s argument. Then the first **LET**
creates a new binding with the initial value 2, and the inner **LET**
creates yet another binding, this one with the initial value 3. The bars on the right mark the scope of each binding.
(defun foo (x)
(format t "Parameter: ~a~%" x) ; |<------ x is argument
(let ((x 2)) ; |
(format t "Outer LET: ~a~%" x) ; | |<---- x is 2
(let ((x 3)) ; | |
(format t "Inner LET: ~a~%" x)) ; | | |<-- x is 3
(format t "Outer LET: ~a~%" x)) ; | |
(format t "Parameter: ~a~%" x)) ; |
Each reference to x
will refer to the binding with the smallest enclosing scope. Once control leaves the scope of one binding form, the binding from the immediately enclosing scope is unshadowed and x
refers to it instead. Thus, calling foo
results in this output:
CL-USER> (foo 1)
Parameter: 1
Outer LET: 2
Inner LET: 3
Outer LET: 2
Parameter: 1
NIL
In future chapters I’ll discuss other constructs that also serve as binding forms—any construct that introduces a new variable name that’s usable only within the construct is a binding form.
For instance, in Chapter 7 you’ll meet the **DOTIMES**
loop, a basic counting loop. It introduces a variable that holds the value of a counter that’s incremented each time through the loop. The following loop, for example, which prints the numbers from 0 to 9, binds the variable x
:
(dotimes (x 10) (format t "~d " x))
Another binding form is a variant of **LET**
, **LET***
. The difference is that in a **LET**
, the variable names can be used only in the body of the **LET**
--the part of the **LET**
after the variables list—but in a **LET***
, the initial value forms for each variable can refer to variables introduced earlier in the variables list. Thus, you can write the following:
(let* ((x 10)
(y (+ x 10)))
(list x y))
but not this:
(let ((x 10)
(y (+ x 10)))
(list x y))
However, you could achieve the same result with nested **LET**
s.
(let ((x 10))
(let ((y (+ x 10)))
(list x y)))