What is LISP? Why do I like it?

Introduction

The purpose of this book is to show you how to program the proofs of the main theorems about program-size complexity, so that we can see the algorithms in these proofs running on the computer. And in order to write these programs, we need to use a programming language. Unfortunately, no existing programming language is exactly what is needed. So I invented a dialect of LISP that will do the job. In this chapter I'll explain the version of LISP that I invented, and in the next chapter I'll tell you how to use it to program the universal Turing machine that we'll use to measure the size of computer programs and to define the program-size complexity H(x).

So it is a two-stage process. First we'll see how this LISP works, then we'll use it to program a Turing machine that accepts binary programs, not programs in LISP. And the size of those binary programs will be our official program-size measure, H(x).

In this book we are not interested in the size of LISP programs, only in using LISP to write and run the programs whose size we really care about.

The heart of pure LISP

[That's LISP with no side-effects, LISP with pure functions.]

LISP is a non-numerical programming language, and it's a functional, expression based language, not an imperative language. In LISP programs you don't indicate how to calculate with numbers, instead you define functions that deal with symbolic expressions, and then you apply these functions to specific arguments.

The symbolic expressions, or S-expressions, that we'll work with are abstract versions of algebraic expressions in which everything is fully parenthesized, and you can summarize the syntax of LISP by saying that the parentheses have to balance. For example, here are some typical S-expressions:

   abracadabra
   (abc (def ghi) klm)
   (((wonderful)))
   ()
LISP S-expressions are either ``atoms'' which are words, or they're lists. It's either-or, with one exception. The exception is the empty list (), which is also an atom. Otherwise a list consists of one or more atoms or sublists. Successive atoms in a list need to be separated by one or more blanks, but otherwise blanks are ignored. For example, here we need blanks to separate the successive elements of a list:
   (x y z)
and here we don't need any blanks:
   ((x)(y)(z))
Note that, unlike a set, the elements in a list may be repeated, and there is a first element, a second element, and so forth. For example
   (a b a)
is a valid S-expression. Its first element is a, the second element is b, and the third element is also a.

In LISP you're given some primitive functions for dealing with S-expressions, and you have to define more elaborate functions from these primitive functions. The LISP notation for f(x, y, z), for the function f applied to the arguments x, y and z, is (f x y z).

Here is the general rule for evaluating an S-expression. If it is an atom, then its value is the current binding of that atom. Initially each atom is bound to itself, except for the atom nil, which is bound to () and gives us a name for the empty list. If an S-expression is not an atom, then its value is defined recursively in terms of the values of its elements. First evaluate the first element to determine the function to be applied, then evaluate the remaining elements (from left to right) to determine the values of the arguments, then apply the function to these arguments. And there are two pseudo-functions which do not evaluate all of their arguments: quote ' and if-then-else if.

What are the primitive functions for dealing with S-expressions?

The first is car, which gives you the first element of a list. For example, car of (a b c) is a and car of ((a a) (b b) (c c)) is (a a). The way you write this in LISP is

   (car (' (a b c)))
   (car (' ((a a) (b b) (c c))))
The first expression has value a, and the second expression has value (a a). The reason you need the quote function ' in (car (' (a b c))) is to indicate that the argument is literally (a b c), which would otherwise be taken to mean that the function whose name is a should be applied to the value of the variables b and c, which is not what we want. In other words, quote separates the program from the data, it indicates where evaluation stops.

Next comes cdr, which gives you the rest of a list, in other words, what is left when the first element is omitted. For example, cdr of (a b c) is (b c) and cdr of ((a a) (b b) (c c)) is ((b b) (c c)). So the LISP expressions

   (cdr (' (a b c)))
   (cdr (' ((a a) (b b) (c c))))
evaluate to
   (b c)
and to
   ((b b) (c c))
respectively. And if you take the cdr of a list with only one element, then you are left with the empty list (). For example,
   (car (' (abc)))
   (cdr (' (abc)))
give, respectively, the values
   abc
and
   ()

The next primitive function for dealing with lists is cons, which reassembles lists that have been broken in two using car and cdr. In other words, cons is the inverse of car and cdr. For example,

   (cons a (' (b c)))
gives (a b c) and
   (cons (' (a a)) (' ((b b) (c c))))
gives ((a a) (b b) (c c)). Note that in our first example we wrote a instead of (' a) because a evaluates to a unless we are inside a defined function in which we are using a as the name of an argument, in which case a gives us the value of that argument. Similarly,
   (cons a (cons b (cons c nil)))
gives us (a b c). This is called ``consing up a list,'' and could also be written
   (cons a (cons b (cons c ())))
or
   (cons (' a) (cons (' b) (cons (' c) (' ()))))

So now we have a way to break a list into pieces, and to put the pieces back together again.

Then there are two predicates for examining S-expressions, atom and equal =. Atom tells us if an S-expression is an atom or not, and equal tells us if two S-expressions are identical. For example.

   (atom (' a))
gives true,
   (atom (' (a b c)))
is false, and
   (atom (' ()))
   (atom nil)
are both true. Similarly,
   (= (' (a b c)) (' (a b c)))
and
   (= (' (a b c)) (' (a x c)))
give true and false, respectively.

Next there is the if-then-else pseudo-function if for using a predicate to choose between two expressions for a value. For example,

   (if true X Y)
   (if false X Y)
give X and Y, respectively. If-then-else is a pseudo-function, not a real function, because it only evaluates two of its three arguments. The unselected argument is not evaluated. The predicate is always evaluated. The other pseudo-function is quote, which never evaluates its one argument.

We've almost finished with the heart of pure LISP! We've seen quote ', car, cdr, cons, atom, equal =, and if-then-else if. What's missing is lambda, which is used for defining functions that are not supplied as primitive functions. For example, let's define the function that makes a list with its two arguments in reverse order:

   ((' (lambda (x y) (cons y (cons x nil)))) a b)
This gives us the value (b a). How does this work? Well, a defined function is a triple of the form
   (lambda (x y...) body)
consisting of a list of argument names followed by the body of the function, which is an expression which gives the value of the function. Most important, note that within the body of the function the argument names are bound to the actual values of the arguments. These bindings are temporary, they only apply within the function body. So within
   (cons y (cons x nil))
x is bound with a and y is bound with b.

Here we're literally giving the function definition, it's quoted, but we could also give an arbitrary expression whose value is the function definition. For example, if f is bound to

   (lambda (x y) (cons y (cons x nil)))
then
   (f a b)
gives (b a).

Now here is a real LISP program. I'll show you how to append two lists. For example, if we append

   (a b c)
and
   (d e f)
we'll get
   (a b c d e f)
Let's call this function F. So we need to bind F to the definition of this function. And how can we define it? Well, recursively, like this. To append two lists x and y, first look at x. If x is an atom, then x is the empty list, and the result is simply the second list y. Otherwise, the result is obtained by consing the first element of x with the result of appending the rest of x with y. So we want to define F to be
   (lambda (x y) (if (atom x) y (cons (car x) (F (cdr x) y))))
Here's how we do it:
   ((' (lambda (F) (F (' (a b c)) (' (d e f)))))
    (' (lambda (x y) (if (atom x) y (cons (car x) (F (cdr x) y)))))
   )
This operation occurs so frequently, that we provide the let-be-in macro[-operation] let which does this for us:
   (let (F x y) (if (atom x) y (cons (car x) (F (cdr x) y)))
    (F (' (a b c)) (' (d e f)))
   )
Let is just syntactic sugar, it's just a convenient abbreviation for the more complicated S-expression using two lambda's.

There's also a simpler version of let, which is used to assign a value to a variable. That's let x be v in e, which is written like this:

   (let x v e)
and expands to this:
   ((' (lambda (x) e)) v)

That's it! This is really everything essential in traditional LISP! For example, you don't need to have positive integers, you could define them as lists of bits and then program out addition, multiplication and exponentiation of binary numbers! And every expression that used an arithmetic operation would have to contain its definition, they would have to be repeated again and again.

So it would be very convenient to have a way of remembering function definitions permanently, and also to have primitive functions for doing decimal arithmetic. And there are far too many parentheses! So let's make our LISP more practical.

A more practical LISP

The first step in making this easier to use, is to omit the parentheses for primitive functions, which will always have a fixed number of arguments in my LISP. As my LISP interpreter reads an expression, it will insert the missing parentheses automatically. So now we have two notations for LISP expressions: the original S-expression notation, and a new meta notation, M-expressions, which are abbreviated expressions with missing parentheses and higher-level macro functions used as abbreviations for groups of primitive functions. [There's also a double-quote " escape mechanism for including an S-expression exactly as is within an M-expression.]

Here are the number of arguments which are understood: ', 1; car, 1; cdr, 1; cons, 2; atom, 1; =, 2; if, 3; lambda, 2; let, 3.

Also, to make life a little easier, we will make the quote function ' into a token delimiter like blank and parentheses, so we can always jam a quote against something without ambiguity. [Like quote, double-quote " is a delimiter, and can be jammed right up against something.]

So now we just write

   cons a '(b c)
to denote
   (cons a (' (b c)))
whose value is the list (a b c).

And our list append example simplifies to this:

   ('lambda (F) (F '(a b c) '(d e f))
    'lambda (x y) if atom x y
                            cons car x (F cdr x y)
   )
If we use the let-be-in macro, this expression gets even simpler. It becomes:
   let (F x y) if (atom x) y
                           cons car x (F cdr x y)
   (F '(a b c) '(d e f))

Two other convenient macros that are provided are cadr and caddr: they expand into car of cdr and into car of cdr of cdr, respectively. Thus

   car   '(a b c d)
   cadr  '(a b c d)
   caddr '(a b c d)
expand to
   (car           (' (a b c d)))
   (car (cdr      (' (a b c d))))
   (car (cdr (cdr (' (a b c d)))))
and evaluate to a, to b, and to c, respectively.

And let's throw in decimal numbers, unsigned integers, 0, 1, 2, ... And we'll provide two-argument addition +, subtraction -, multiplication *, and exponentiation ^, and extend equal = to also compare integers. E.g., the M-expression

   + * 2 3 * 4 5
denotes the S-expression
   (+ (* 2 3) (* 4 5))
whose value is 26. So we are using prefix notation instead of the usual infix notation for arithmetic operators. We also throw in less-than <, greater-than >, less-than-or-equal <=, and greater-than-or-equal >=. And two one-argument primitive functions for converting between base 2 and base 10: base10-to-2 and base2-to-10.

And let's also add define, with two arguments, which is just like let-be-in, except that it gives a permanent binding, not a temporary one, and never evaluates any of its arguments. Define is not really in the language; we should always use let-be-in, which is really lambda binding, but it is convenient to allow define at the highest level of the interpreter so that definitions don't have to be repeated within an interpreter run. Define only works if it is the top level function given to the interpreter, it cannot be inside another expression.

So now we have something that begins to look like a fairly realistic programming language. Here's how factorial looks. Here's 5 factorial; 5! = 5 by 4 by 3 by 2 by 1 = 120:

   define (factorial n)
   [if n = 0, then 1, otherwise n times (n-1)!]
      if = n 0 1 * n (factorial - n 1)
 
   [try it!]
   (factorial 5)
And the stuff in brackets are comments, which we had to have some way of including. When you run this, the LISP interpreter will first indicate that factorial is bound to
   (lambda (n) (if (= n 0) 1 (* n (factorial (- n 1)))))
and then that the value of (factorial 5) is 120.

At this point you should go to my LISP interpreter web page at one of these two URL's and try out all of this:

   http://www.cs.umaine.edu/~chaitin/unknowable/lisp.html
   http://www.cs.auckland.ac.nz/CDMTCS/chaitin/unknowable/lisp.html
Just type M-expressions into the upper window, click on run, and the corresponding S-expressions and their values will appear in the lower window.

Additional functions for AIT

Now let me show you some additional primitive functions that I've added, mostly in order to program my basic self-delimiting universal Turing machine in LISP, which we'll do in the next chapter.

We'll be dealing with very long bit strings, which we'll represent as very long lists of 0's and 1's. These will be the self-delimiting binary programs that we'll feed to my universal Turing machine, and the size of these binary programs will give us our complexity measure H(x).

I showed you how to append two lists, but having this as a defined function is too inefficient. So we add a primitive function append that gives us the combined list if it's given two lists. We'll use append to assemble long binary programs from pieces. And the pieces will usually be produced by another new primitive function, bits, that converts the character string representation of an S-expression into an ASCII bit string, eight bits per character, plus eight more bits for an unprintable UNIX newline control character at the end.

And we'll want to count how long the resulting bit strings are. So for that we add a primitive function length that returns the number of elements in a list. If this list is a bit string, a list of 0's and 1's, that will be the number of bits in the bit string. There's also size, which gives us the number of characters in the character string representation of an S-expression. That's very useful if we're looking at the LISP program-size complexity of x, which is the number of characters in the smallest S-expression that has value x, which we did in The Unknowable and The Limits of Mathematics, but which we won't be doing in this book. Here we're only interested in the real thing: in the size in bits of the smallest self-delimiting binary program for x.

Next there's an identity function, display, which has the side-effect of displaying its argument. This is normally used for debugging, but we'll also use it to output infinite sets instead of only producing one result, the value of an expression. In other words, display will be used to enable non-halting LISP expressions to generate infinite sets of S-expressions as output.

Next there's eval, which may be thought of as the inverse of quote. What it really does is enable us to run LISP code that we've generated somehow. The argument of eval is evaluated in a fresh environment, in which each atom is bound to itself and nil is bound to (). If you eval an unending LISP expression then you lose control, you never get a result back. The way to deal with this is called try, which performs a time-limited evaluation and captures any displays. It also has another very important role, which is to provide the LISP expression that it's running with binary data on the side.

Try has three arguments: a time limit α, an expression to be evaluated β, and a list of 0's and 1's γ. The number α is either a time limit for evaluating β, or it's no-time-limit indicating that there is no time limit. As the S-expression β is evaluated, the arguments of all its display's are captured, and β can read in its binary data γ: either bit by bit using the zero-argument primitive function read-bit, or a complete S-expression at a time using the zero-argument primitive function read-exp. Read-exp reads in eight-bit ASCII characters until it finds a UNIX newline, and returns the S-expression whose character string representation it has read. Read-bit and read-exp both fail if they run off the end of the binary data γ. In that case the (try α β γ) fails due to running out of data. It can also fail by running out of time.

The value that try returns is a triple. The first element will either be success or failure. The second element will either be the value of β if the try was a success, or it will be out-of-time if the time limit α was exceeded or out-of-data if β ran out of binary data. These are the only two possible failure modes, because otherwise my LISP has very permissive semantics and will always go ahead and do something. And the third element of the value of a try is the chronological list of captured display's.

Note that like eval, try evaluates its argument β in a fresh environment. However, evaluating the argument of eval may have the side effect of reading in part of the current binary data (the binary data in the immediately containing try), while the argument β of a try reads in data from its own binary data γ and cannot touch any other try's binary data. [Other side-effects of an eval: eval may also do display's, which try cannot.]

Finally, let's throw in another identity function debug that also displays its argument, but which is not seen by the official display/try mechanism in which display throws intermediate results and try catches them.

Try is the real difference between my LISP and a normal LISP; it's what I had to add to LISP to do algorithmic information theory. The fundamental semantic notion in normal LISP is evaluation, which maps an S-expression and the current bindings into its value. In my LISP the fundamental semantic notion of evaluation maps an S-expression and associated binary data plus the current bindings into its value; the argument of evaluation is a triple, not a pair.

And the key self-delimiting feature of read-bit and read-exp needed in order to implement algorithmic information theory properly is that they are not allowed to run off the end of the binary data γ and peacefully return an ``end-of-file'' indication. Instead they abort, and they abort the containing S-expressions with them, which percolates up to the immediately containing try.

Note that it is very important that try's and eval's can be nested to arbitrary depth; if two try's are nested the most constraining time limit is in effect. And how are these time limits measured? Well, roughly speaking, they're measured via the interpreter push-down stack depth, as the depth of nested subroutine calls or nested function definitions in the LISP evaluation. More precisely (thanks to R.M. Solovay), the time limit bounds the number of pending re-evaluations due to eval, to try, and to defined functions (lambda expressions). [Solovay pointed out to me that my original formulation of try in the first printing of my 1987 Cambridge University Press monograph was not quite right; subsequent printings corrected this.] The important point is that if there is a time limit, then a try must terminate, either with the correct value or with a failure indication. Thus it is always perfectly safe to do a time-limited try.

Well that's all there is, it's all you need to run algorithmic information theory on the computer! In the next chapter I'll show you how.

Exercises

  1. Play with the Java applet LISP interpreter; try out all the built-in primitive and macro functions.

  2. Define the functions for reversing the top level of a list, and for reversing all sublists too.

  3. Define the function flatten that removes all the inner parentheses from an S-expression, leaving only the outermost pair.

  4. Define the substitution function (subst e x r) that substitutes r for all occurrences of the atom x in the S-expression e.

  5. Define functions for sorting lists of positive integers into ascending order. For example, program bubble sort, and try dividing the list into two halves, sorting each half, and then merging the results.

  6. Program base-two addition, multiplication and exponentiation. Represent positive integers as lists of 0's and 1's. Does it pay to keep the bits in reverse order?

  7. Program elementary set theory for finite sets of S-expressions: membership, set inclusion, union, intersection, subtraction, cross product (set of pairs from two sets), and set of all subsets. A set is represented as a list without repetitions.

  8. Program intersection and union of infinite sets of S-expressions. You are given two non-halting LISP expressions that display the elements of the two sets. Use try to run both computations for time t, examine the elements that have been produced so far, then let t go to infinity. Avoid repeated output.

  9. Program LISP semantics in LISP. Represent bindings as a list of pairs, and define recursively the function evl with two arguments: an S-expression to be evaluated, and a list of bindings. Just handle quote, if-then-else, lambda, car, cdr, cons, atom, =, and display. Hint: Use ((nil ())) as the initial list of bindings, and make any unbound atom evaluate to itself. Put additional bindings at the front of the list, where they shadow any previous bindings. You'll need to use the double-quote " notation for including S-expressions in M-expressions. For example, "car is just the word car without any implicit parentheses or arguments.

  10. [Preview of next chapter!] Take a look at the run-utm-on p macro which expands to
       cadr try no-time-limit 'eval read-exp p
    
    and then to
       (car (cdr (try no-time-limit (' (eval (read-exp))) p)))
    
    Try to figure out what this does; it plays a key role in the next chapter. This macro is only provided in the Java applet version of my LISP interpreter. Fix the Mathematica and the C versions of the interpreter so that they also have run-utm-on. You can find the source code at these URL's:
       http://www.cs.umaine.edu/~chaitin/lisp.m
       http://www.cs.umaine.edu/~chaitin/lisp.c
    
       http://www.cs.auckland.ac.nz/CDMTCS/chaitin/lisp.m
       http://www.cs.auckland.ac.nz/CDMTCS/chaitin/lisp.c
    

  11. [Language improvements!] Add the two-argument primitive function not-equal != to the three versions of the interpreter. Also add the two-argument primitive function do that discards the value of its first argument and returns the value of its second argument. Then implement logical not !, and & and or | as macros:
       ! p
       & p q
       | p q
    
    expand to
       (if p false true)
       (if p q false)
       (if p true q)
    
    respectively.

LISP interpreter URL's

You can use our Java applet LISP interpreter at these URL's:
http://www.cs.umaine.edu/~chaitin/unknowable/lisp.html
http://www.cs.auckland.ac.nz/CDMTCS/chaitin/unknowable/lisp.html
The Java source code for this applet is at these URL's:
http://www.cs.umaine.edu/~chaitin/unknowable/lisp.java
http://www.cs.umaine.edu/~chaitin/unknowable/Sexp.java

http://www.cs.auckland.ac.nz/CDMTCS/chaitin/unknowable/lisp.java
http://www.cs.auckland.ac.nz/CDMTCS/chaitin/unknowable/Sexp.java

Built-in primitive and macro functions

Other LISP language elements