What Makes Lisp Great
Bill Clementson wrote about a very frequently asked question on comp.lang.lisp. He already has a link to the question, "What's so great about lisp?" so I won't repeat it here. What I particularly like is the answer to that question that Bill took the time to write about. I would like to throw in my one point three cents after taxes. Let me say ahead of time that I'm not the only one who thinks this. Read the thread.
The Troll FAQ and My Answer
From: "Kris Cipa" <kristinacipa@yahoo.co.nz>
Subject: What's so great about lisp?
Newsgroups: comp.lang.lisp,comp.lang.scheme
Date: 23 Sep 2005 03:40:26 -0700
Organization: http://groups.google.comWhat's so great about lisp?
I had to use lisp in college for a course, and it looked like a horribly primitive and useless contraption. We even had to use emacs to use it, in the 21 century!. I have avoided it ever since. However I find more and more people rhapsodizing about how cool Lisp is and what an advanced language it supposedly is. I just don't get it: I mean do those people claim that we have made no progress in all the years since the early days of computing when lisp was used? I'd like to know, what's the secret?
Regards
--
Kris.
My answer in one word: syntax.
Now let me elaborate. The very thing that people seem to hate at first sight when they see Lisp, the (parenthesis) is what makes the language so great. Sounds silly, doesn't it?
Lisp used to be spelled "LISP" for "LISt Processing". While modern Lisp has a large assortment of data structures and types, lists are still at the heart of the language. The source code is written as a tree of nested lists. The Lisp reader understands the syntax to read in the source text and convert it into List structure for the compiler. The compiler is loaded with functions for manipulating lists. What this leads to is the ability for the reader to perform arbitrary computations on the source code as it is read in and the compiler to perform arbitrary computations on the source at compile time. This power is not limited to the reader and the compiler. ANSI Common Lisp defines facilities for the programmer to implement arbitrarily complex code transformations. These are called Macros.
Macros are fairly pervasive throughout Common Lisp. A Macro doesn't look any different from a special form defined by the language or a function, primitive or user defined. Many of the Common Lisp defined features are in fact macros wrapping lower level expressions.
An Idiomatic Macro Example
While classes and functions in computer languages allow you to abstract concepts, Macros allow you to abstract syntax. Consider the following typical use case in some made up modern language that uses C like syntax and supports most of Lisp's features.
/*********************************************************************
Pseudo Language code for writing to a file
* The language has a C like syntax that most programmers are familiar
and comfortable with.
* The language has advanced features such as garbage collection and
exception handling.
* The language has anonymous functions and lexical closures.
* The language supports object oriented programming.
* The language supports most other features that are available to
Lisp. The only real difference is the syntax and lack of a macro
facility.
*********************************************************************/
function makeSequenceGenerator () {
int a = 1, b = 1;
function () {
int ret = a + b;
a = b;
b = ret;
ret;
};
}
function writeToFile (string file, int terms) {
func fib = makeSequenceGenerator();
fh = open(file, OUTPUT | SUPERSEDE);
try {
for (i = 0; i < terms; i++) {
int term = fib();
println (term);
}
}
catch error {
// Normally process the error. But we punt instead.
exit(-1);
}
finally {
if (fh) {
close (fh);
}
}
}
As you can see, the language supports the Fibonacci cliche as well as any language, including Lisp. Let's even pretend that it supports bignum arithmetic so that if you ask for 1000 terms you get the correct answer. What I want to draw your attention to is the try-catch-finally sequence. This is a major feature of modern exception handling. Lisp has something called the condition system that serves the same purpose. It's pretty sophisticated. The try-finally syntax is handled by the special form unwind-protect.
Let's see what an identically functioning Lisp program might look like when doing line by line translation.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; The same program in Lisp version 1
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defun make-sequence-generator ()
(let ((a 1)
(b 1))
(lambda () (shiftf a b (+ a b)))))
(defun write-to-file (file terms)
(let (fh)
(unwind-protect
(progn
(setf fh (open file
:direction :output
:if-exists :supersede))
(loop
with fib = (make-sequence-generator)
for i below terms do
(print (funcall fib) fh)))
(when fh
(close fh)))))
Like the finally clause, unwind-protect ensures that a file will be closed if an error occurs or not. This is such a common use case that Common Lisp defines a macro called with-open-file. OpenMCL defines the macro like so:
(defmacro with-open-file ((var . args) &body body &aux (stream (gensym))(done (gensym)))
"Use open to create a file stream to file named by filespec. Filespec is
the name of the file to be opened. Options are used as keyword arguments
to open."
`(let (,stream ,done)
(unwind-protect
(multiple-value-prog1
(let ((,var (setq ,stream (open ,@args))))
,@body)
(setq ,done t))
(when ,stream (close ,stream :abort (null ,done))))))
I won't go into the details of this macro. It does produce slightly different code than the Lisp program above when the macro is expanded. What is important is that the above program can now be written like this:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; The same program in Lisp version 2
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defun make-sequence-generator ()
(let ((a 1)
(b 1))
(lambda () (shiftf a b (+ a b)))))
(defun write-to-file (file terms)
(with-open-file (fh file :direction :output :if-exists :supersede)
(loop
with fib = (make-sequence-generator)
for i below terms do
(print (funcall fib) fh))))
Of course this is a trivial toy example. However, lines of code add up. Macros allow the kind of syntactic abstraction that can not be done with higher-order functions. This extra abstraction capability makes it that much easier for developing complex code. This is important because, in the end, a programmer's biggest challenge is managing complexity.
So Where's The Catch?
This is the catch. Catch is used for non-local transfer of control. Lisp has a rather powerful mechanism for dealing with errors called the condition system. Rather than mess up the explanation of what that is, I'll refer you instead to Kent Pitman's "Condition Handling in the Lisp Language Family". Kent Pitman worked on the X3J13 committee that created the ANSI Common Lisp standard. You can be sure he knows what he is talking about.
Emacs And Lisp In The 21st Century
The troll also complained about Emacs in the 21st century. It's not like Emacs has been standing still since it was first conceived. Just as a simple example, to look up the definition of with-open-file, I used a single key-chord: M-. (Meta-dot) with the cursor over the macro call. Emacs immediately located the file containing the definition and opened it, placing the cursor at the definition. Doesn't that sound just like a modern IDE to you?
You may also have noticed a little "Created With Emacs" icon at the lower left hand corner of my pages. That is no lie. Emacs is good for editing all sorts of text. It is unsurpassed at editing Lisp code in the free software world. Many would argue that it is unsurpassed for editing many computer languages' source code. I won't get into that debate.
So what about the advancement in programming languages over the years? That's easy. There has been advancement. Lisp hasn't stood still while other languages have been catching up. Not entirely anyway. The ANSI Common Lisp specification was finalized in 1994. That doesn't make ANSI Common Lisp very old now, does it? Scheme and other Lisp dialects are older. But the truth is, no general purpose programming language has managed to incorporate all of Lisp's features. Certainly some come close. But without those parenthesis they will have a difficult time creating macros as easily as in Lisp.
Lisp has also had time to mature. There is nothing like a stable foundation when it comes to building libraries. I know what you're going to say. You're going to say that libraries are a major weakness in Lisp. This is both true and false. It is true that libraries that people would like to have do not exist, causing functionality to differ from implementation to implementation when it goes beyond the ANSI specification. This is not really a problem unique to Lisp. Any language with multiple implementations on multiple platforms has the same issues. The other thing about libraries is that they are created on an as needed basis. Lisp has not been used the same way as Perl, PHP, Python, or Java. It should not come as a surprise that different sets of libraries are available.
Personally, I think Lisp has a bright future. There is a surprising number of free Lisp implementations considering the overall size of the Lisp "community". Also there appears to be a critical mass of users to encourage continued development. The more users there are, the more libraries there will be attracting more users.
Lisp is the language for the 21st Century.