Declarative Languages
Lecture #2
Purpose: To introduce enough basic concepts that you can
start to write simple programs for yourself
2.1 A glorified pocket calculator
Lisp systems are interactive. Type some lisp at the prompt - it will
go away, think about it and hopefully return some lisp in exchange. Then
it gives you another prompt and the process repeats itself. More formally,
we are in a loop (known as the "read-eval-print" loop) which does
the following:
-
read in a lisp form
-
evaluate this form
-
print the result
Now, it happens that numbers always evaluate to themselves (self-evaluating).
Therefore, when you type in a lisp number you will get that number back.
Note by the way that lisp offers the following types of number
-
integers (with arbitrary magnitude)
-
ratios (such as 1/2 or 1234567/890123343456)
-
floats (sometimes with a choice of precisions, e.g. 64-bit IEEE format)
-
complex numbers (!)
To make our calculator a little bit more interesting, we can apply arithmetic
operations to these numbers. The syntax for doing so is
-
left parenthesis
-
name of operator
-
various arguments to that operator, each preceded by whitespace
-
right parenthesis
For example:
(+ 1 2 3)
- in this case, the operator is + and the arguments
are the numbers 1, 2 and 3. Similarly, we have
the operators (or, to be precise, functions)
-
- (which takes its first argument and subtracts from that
all following arguments)
-
*
-
/ (which takes its first argument and divides that by all following
arguments)
Note that you can mix types (e.g. integer and float) within a call to any
of the above and lisp will (a) not complain and (b) do the intelligent
thing on your behalf. For example, (+ 1/5 0.8) => 1.0
Note about fonts. Anything in fixed width font is lisp - either
what you entered or what the lisp system returned. Anything else is just
me talking. I use the symbol "=>" to mean "returns". So in the
above, if you type in the lisp form (+ 1/5 0.8) and press the
Return key, I claim that a lisp system will return and print out the number
1.0.
2.2 Nested calculations
We can nest one function call within another one, for example:
(+ (* 2 3) (* 4 5 6))
.
There is an unambiguous rule for evaluating function calls, as follows:
-
process the arguments to the function, in order ("from left to right")
-
evaluate each argument in turn
-
once all the arguments have been evaluated, call the original function
with these values
-
return the result
So, to evaluate (+ (* 2 3) (* 4 5 6))
-
We start by noting that we have a call to the function + with
arguments (* 2 3) and (* 4 5 6)
-
We evaluate the first argument, namely (* 2 3)
-
We note that this is itself a function call (the function is *
and its arguments are 2 and 3)
-
We must therefore evaluate the first argument to * - this is the
number 2
-
The number 2 evaluates to itself
-
We next evaluate the second argument to * - this is the number
3
-
The number 3 evaluates to itself
-
We can now call the function * with arguments 2 and 3
-
The result of this function call is 6
-
The value of the first argument to the function + is therefore
6
-
Similarly the value of the second argument to the function + is
120
-
We can now call the function + with arguments 6 and 120
and finally return the value 126.
When written out in full like this, it seems very long-winded and complicated,
but it isn't really. Just bear in mind that lisp will evaluate everything
in sight, going from left to right, and evaluating inner expressions before
outer ones. Think of it as the obvious thing to do!
2.3 Interim summary
We have a uniform syntax for function calls:
(function arg-1 arg-2 ....)
To evaluate a function call, lisp first evaluates all the function's
arguments (in order) and then calls the function itself, with the evaluated
arguments.
2.4 Some simple functions
float takes one argument
(of any numerical type) and returns the equivalent float.
list takes
any number of arguments (of any type / types) and returns a list of those
arguments.
identity takes one argument (anything)
and returns it.
As we are beginning to see, the types of arguments to functions and
of values returned by them are not necessarily tied down in advance. In
lisp you do not have to declare types of variables, or what types a function
expects to receive and return. This may take some getting use to, but it
really does make programming considerably less painful :-)
Aside: there may be circumstances (optimizing inner loops)
when you need to declare types, to get lisp to run that bit faster. A mechanism
for this does exist but is outside the scope of this course.
2.5 Some examples of operators which are not functions
quote takes one argument (anything) and returns
it UNEVALUATED
defun used for defining more functions -
see below
Recall - a function evaluates all its arguments, using the rules described
in 2.2 / 2.3 above. Always. Therefore, if an operator does not evaluate
all of its arguments as per these rules, then it cannot be a function.
(Logic.)
The operator quote suspends evaluation - it returns its argument
as-is. For example:
(quote (foo bar wibble))
=> (foo bar wibble)
Therefore quote is not a function.
For another example, consider the difference between
(quote (+ 1 2 3 4))
and (identity (+ 1 2 3 4))
- identity is a function and therefore it evaluates its argument,
whereas quote
is not and does not.
Aside: lisp has two sorts of "not a function" - macros
and
special operators. When using one of these you do not ever need
to know whether it was a macro or a special operator. The only distinctions
between macros and special operators are that (i) macros always work by
"transforming source code" - a bit like #define in C and (ii) you can define
more macros yourself, but the set of special operators is fixed.
In fact, quote is a special operator. (Aren't you glad you know
that?) To see the value of being able to quote constructs in a language,
visit http://www.cob.ohio-state.edu/~tomassin/whotext.html
(or, if terribly enthusiastic, read "Godel,
Escher, Bach" by Douglas R. Hofstadter or at least http://www.ncsu.edu/felder-public/kenny/papers/godel.html).
Finally note the following shorthand for quote:
'(foo bar wombat)
is the same as
(quote (foo bar wombat))
The ' character is sometimes referred to as syntactic sugar
- it's there to make the syntax that bit sweeter but doesn't add anything
that couldn't be done another way. Nevertheless, lisp programmers are lazy
(the language encourages this) and you'll never find them typing the symbol
quote
in full. Ever.
2.6 Defining your own functions
To define a function of your own, use the macro defun. Examples:
;;; returns the number of days in a non-leap year
(defun days-in-year ()
; no arguments
(+ (* 30 4)
(* 31 7)
28))
;;; takes one argument and returns a list containing that argument
twice
(defun list-of-two (thing)
; one argument
(list thing thing))
Note:
-
the syntax of defun is
(defun name (arg-1 arg-2 ...) form-1 form-2 ...)
where name is the name of the function, arg-1 etc. are
the arguments to that function, and form-1 etc. are the lisp forms
to evaluate in order to evaluate a call to that function.
-
there is no need to declare types of arguments, or of the function itself
:-)
-
in the first example there were no arguments, and so the argument list
was empty - () - and the function is called thus
-
in the second example there is one argument thing and the function
is called thus
(list-of-two (+ 2 2))
=> (4 4)
During the execution of the function call, the local variable thing
has the value passed as an argument (in this case, that is the result of
evaluating (+ 2 2))
2.7 Beauty tips
A few stylistic layout conventions (please observe them rather than
inventing your own, as they will make your code more readable both for
you and for the examiners):
-
it is traditional for function definitions to be laid out as in the examples
above, namely with defun, the function name and the argument list
on one line, and the body of the function starting on the next line
-
any lisp editor worth its salt will indent (the current line of) lisp code
correctly when you press the Tab key. Use it
-
if you have several parentheses together, keep them together, eg
)))))))
or whatever. Do not separate them with spaces or newlines
Comments:
-
the semicolon ; introduces a comment that lasts until the end
of the current line.
-
semicolon comments appended to the end of a line of code should be introduced
by a single semicolon, whereas comments which take up the whole line have
two or more (HyperSpec section 2.4.4 for enthusiasts).
-
to comment out several lines of code, start the comment with #|
and terminate it with |#
2.8 Lists
As you will probably have spotted by now, a list is any collection of
objects with a left parenthesis at one end and a right parenthesis at the
other. For example,
(round the ragged rocks the rugged rascal ran)
Lists can be of any length, and can hold data of mixed type (as in
the example below).
Lisp code is built with lists. Lists can also be used to hold program
data.
The function list can be used to build lists, e.g.
(list "my" (+ 1 2) 'sons)
=> ("my" 3 SONS)
To get at the contents of a list, use the functions first,
second,
third,...
(up to tenth). So:
(second (list "my" (+ 1 2) 'sons)) =>
3
To "drop the front" off a list, use the function rest:
(rest (list "my" (+ 1 2) 'sons))
=> (3 SONS)
2.9 The empty list and nil
A list can be empty - in other words it doesn't contain anything
at all. You would type it like this:
()
but the weird thing is that when you do that, lisp stores it (and,
indeed, prints it) not as a list but as the symbol:
nil
This is because the empty list () and the symbol nil
are the same object. .
Be aware that nil is self-evaluating, so
nil => NIL
() => NIL
; not the usual rule for evaluating lists
2.10 True or false?
To add to the fun, nil is also used by lisp to denote logical
falsehood.
Anything which is non-nil denotes logical truth. To prevent
a total free for all, functions which are required to answer "true" or
"false" (predicates) usually return the symbol t to denote
truth.
Stylistic issue: the names of lisp predicates generally end in the letter
p.
If the name contains hyphens already, then hyphenate the p also.
So:
zerop
array-has-fill-pointer-p
Example:
CL-USER 1 > (defun equal-to-fourty-two-p (number)
;; is number equal to 42?
(= number 42))
EQUAL-TO-FOURTY-TWO-P
CL-USER 2 > (equal-to-fourty-two-p 15)
NIL
CL-USER 3 > (equal-to-fourty-two-p 42)
T
CL-USER 4 >
Note here the predicate =, which is an immediate exception to
the above naming convention (sorry about that). This function takes any
number of arguments - so long as they are all numbers - and returns t
provided they are all numerically the same:
(= 3 3.0) => T
(= 5/2 2.5) => T
(= 3 3 3 3 3) => T
(= 3 3 17 3 3) => NIL
2.11 If...
The syntax for the operator if is
-
if
-
predicate (always evaluated)
-
what to do if the predicate is true, ie if the predicate does not evaluate
to nil (this argument is not evaluated if the predicate is false)
-
what to do if the predicate is false, ie if the predicate evaluates to
nil
(this argument is not evaluated if the predicate is true)
For example:
(if (saucepan-is-cool-enough-p)
(pick-it-up)
(let-it-cool-down))
It's just as well that if is not a function (if it were, you'd
always get burned).
Another example:
;;; Given an integer n and a list,
;;; return the nth (zero based) member of list.
;;; Make it easier by assuming list is short.
(defun nth-member (n list)
;; there has to be a better way... (we hope)
(if (= n 0)
(first list)
(if (= n 1)
(second list)
(if (= n 2)
(third list)
"etc"))))
2.12 Summary
-
Lisp sits there and evaluates objects
-
Some objects evaluate to themselves (the act of evaluation is effectively
a no-op)
-
numbers
-
strings
-
certain symbols: t and nil
-
A non-nil list whose first element is a function is evaluated
according to the rules described in 2.2 / 2.3 above.
-
A non-nil list whose first element is not a function is handled
specially. Examples of non-functions include
2.13 How to put this to use
Let's take as an example the following sample problem. You may want
to work through this one carefully before moving on to the following exercises.
-
Write a function which takes two arguments, both numbers, and returns the
sum of their squares.
You may find that the following steps are helpful:
-
Come up with a name for the function before you attempt to do anything
else. Hmm, so how about sum-of-squares ?
-
Make sure you're clear what calls to this function would look like and
what value would be returned. We get this information directly from the
statement of the problem: the function takes two (number) arguments, squares
each one, adds these together and returns the result. So a call would look
like (for example)
and the return value in this case should be 25.
-
Decide on names for the arguments. In this case, I would suggest something
like first-number and second-number or, for brevity,
first
and second.
-
Now you can write the "outer" part of the function:
(defun sum-of-squares (first second)
)
With this much written, you should be happy with the statement that our
sample call would execute the (as yet unwritten) body of this function
with first having the value 3 and second having
the value 4
-
Next you have to write the function calls which will take two values and
evaluate the sum of their squares. The square of the first number is expressed
thus:
(* first first)
(and similarly for the square of the other), and the sum of the two quantities
is therefore:
(+ (* first first) (* second second))
-
Now you can slot this into the function template you wrote earlier:
(defun sum-of-squares (first second)
(+ (* first first)
(* second second)))
and type this into a lisp listener.
-
Finally you test that the function works, by typing in sample calls and
checking that the correct answer is returned.
2.14 Suggested activity / exercises etc
-
Read Graham to page 18
-
The functions + - * / = list all take a variable number of arguments.
Experiment to find out what each of these does with exactly one argument.
Now try each function with zero arguments, e.g. (+) and find out
what happens.
-
Don't forget the key-binding Meta-Shift-A to bail out of errors.
-
As in 2.2. above, describe in detail what happens when lisp evaluates each
of the following
-
(list 1 (+ 2 3))
-
(if (= (+ 2 2) 4)
(* 2 2)
(/ 1 0)) ; why does this apparent division
by zero not take place?
-
Write a function my-identity which takes one argument and returns
that value. Sample call:
(my-identity "anything") => "anything"
-
Write a function which takes two arguments and returns them in a list,
second argument first.
-
Write a function which takes one argument, guaranteed to be a list of precisely
two elements, and returns a new list which contains both these elements
but in reverse order.
-
Write a function which takes three numbers as separate arguments and computes
their average
-
Write a function which takes a list of three numbers (i.e. one argument),
and computes the average of these numbers
-
Write a function called minutes-in-one-year which takes one argument
leapp.
If leapp is true (i.e. non-nil) the function returns
the number of minutes in a leap year; if leapp is false (i.e.
nil)
then it returns the number of minutes in a non-leap year.
-
Example call: (minutes-in-one-year t) => ?
-
Given that lisp defines first,
second,..., tenth
for you, how would you define eleventh?
If you get seriously stuck, look at the solutions
but please have a good go at each problem first.
Nick Levine
last modified 2000-09-13
Copyright (C) Nick Levine 1999-2001. All rights reserved.
$Id: //info.ravenbrook.com/user/ndl/lisp/declarative/lectures/lectures/lecture-2.html#2
$