Why I like Lisp
Introduction
I've recently started attempting to learn the Lisp programming language. My previous attempts always aborted at the wierd (to me) syntax, and the wierd (again, to me) programming style.
But, I have to say now that I've finally got over my parenthical prejudices, I'm bloody well impressed! The power of lisp is difficult to explain to someone who does not actually program with the language, but I'm going to try and give a single simplistic example to try and illustrate the power of lisp.
The language versus the library
First off, even though I do not particularly like arguments that go "Yeah, but can your language do this?", I am nevertheless going to ask "Can your language do this?".
The this in question is the following:
Assume that your favourite (and by implication, your most powerfull) language does not support a certain bit of functionality ... say the ubiquitous for loop, although YFL (Your Favourite Language) does indeed have a while loop.
So, how hard is it to add a for loop to YFL? Can you do it without the sources to rebuild the language? Can you do it in a manner that makes it transparent to the language user?
In lisp, it is trivially easy to add into the language new constructs that expand the language. If there is no for loop, then you can add one in, and it will look, to the reader, as if it is part of the language itself.
Now, I'm not talking about adding a new function to the language, which can be done in every language you'd care to name, but adding a new language construct.
That's a big win, right there. Theres no need to even go further - in most other languages you would need to modify the source code for the language implementation whilst in lisp you can merely use the language as given and add in your more powerful constructs.
Example: Adding a language feature to lisp
For a concrete example, lets talk about reflection. You know, the ability for a class or complex piece of data to make available at runtime all the elements that make up said class or piece of complex data[1].
Lets say, that in YFL, you can declare something along the following lines in my easy-to-understand pseudocode:
class Human inherits from Mammal {
Name;
Age;
DoB;
};
You can easily come up with the analogous definition in C#, C++, Java
or any of the other popular languages. In lisp, such code
can be expressed by:
(defclass Human (Mammal)
((Name)
(Age)
(DoB)))
Of course, there are other things as well, such as setting
default values for each element which is done as follows:
(defclass Human (Mammal)
((Name :initform "Default Name")
(Age :initform 25)
(DoB :initform "1-Aug-1971")))
In addition, to ":initform", there are a few other keywords
that one can place next to the element, such as ":initarg"
and ":accessor", and they are placed in much the same way
as ":initform" above. The value of an element is typically
retrieved with "(slot-value ...)":
(defvar foo (make-instance 'Human)) (slot-value foo 'Name)The above will presumably return the value that is stored in the element (called slots) "Name" in object "foo".
Now take YFL, and try to change it so that every class that you define a certain way automatically has one additional element - an element to store the names of all the other elements.
What I mean is this, after I instantiate a "Human" class, I would like my code to be able to get the name (in whatever form) of every single element in that class; example:
Human foo;
print ("the names of all the elements in foo is");
print (getAllTheNames (foo));
if (foo.HasElement ("Name")) then
print ("Name is an element of foo");
end;
I'll even make it easy for you, I'll let you define a base
class which all the other classes have to inherit from in order
to have this functionality.
I suppose you can do it; in C or C++, for example, you can mess with the preprocessor until you get it working. Possible.
Totally impossible in Java without making the syntax of the call very ugly (I know, RMI needs reflection, but its ugly and cluttered with keywords and sequence of function calls to make it work). The pseudocode above has no equivalent in most other languages without writing bags and bags of supporting code.
In lisp, however, the following macro does exactly that:
(defmacro def-rclass (name base slotlist)
`(defclass ,name ,base
((slots
:accessor slots
:initform (let ((slotnames (list )))
(dolist (slot ',slotlist)
(setf slotnames (append slotnames (list (car slot)))))
(dolist (b ',base)
(setf slotnames (append slotnames (slots (make-instance b)))))
slotnames)
) ,@slotlist)))
And that, I kid you not, is not the best implementation of reflection in lisp
(I am after all only just learning the language, an expert will almost
surely do better, especially in the appending to a list bits). The usage
is almost identical to the standard usage:
(def-rclass Human (Mammal)
((Name)
(Age)
(DoB)))
Everything but the defclass keyword stays the same. Inheritance
is specified the same as before, and multiple-inheritance works
exactly the same as well (since we are not working with a base-class
at all in the new definition, MI doesn't get affected at all). The
keywords ":initform" and friends also work unchanged.
The list of element names (like "Name" "Age" and "DoB") still exists and can be accessed as before, but there is one new element that is created and modified invisibly; slots.
slots is an extra element that will be present in every single object instantiated from a class defined with our new def-rclass keyword. The contents of this element is simply a list all the other elements.
This:
(def-rclass Human (Mammal)
((Name)
(Age)
(DoB)))
gets turned into this:
(defclass Human (Mammal)
((slots :initform '(Name Age DoB))
(Name)
(Age)
(DoB)))
by our macro def-rclass. But thats not all, you see
def-rclass does not mess with the standard element
values, so this:
(def-rclass Human (Mammal)
((Name :initform "NoNameBrand" :accessor m-name :initarg :name)
(Age)
(DoB)))
gets turned into this:
(defclass Human (Mammal)
((slots :initform '(Name Age DoB))
(Name :initform "NoNameBrand" :accessor m-name :initarg :name)
(Age)
(DoB)))
so that the basic class definition is intact as if one had written
it natively using plain old defclass. We can even get the
inheritance and multiple-inheritance (with the order) correct, because
this:
(def-rclass Human (Endoskeleton Primate Hairy)
((Name)
(Age)
(DoB)))
gets turned into this:
(defclass Human (Endoskeleton Primate)
((slots :initform '(Name Age DoB))
(Name)
(Age)
(DoB)))
Impressive, huh? But wait, theres even more! Lets say you have
more than one class, and that one inherits from the other like
so:
(defclass Endoskeleton ()
((BoneType)
(BoneCount)))
(defclass Primate ()
((Weight)
(Height)))
(defclass Human (Endoskeleton Primate)
((Name)
(Age)
(DoB)))
Then, instead of using defclass you use def-rclass
to define all three classes. The final class Human will have
the element slots set to '(Name Age DoB BoneType BoneCount Weight Height)). That is, the following:
(def-rclass Endoskeleton ()
((BoneType)
(BoneCount)))
(def-rclass Primate ()
((Weight)
(Height)))
(def-rclass Human (Endoskeleton Primate)
((Name)
(Age)
(DoB)))
will get turned into this:
(defclass Endoskeleton ()
((slots :initform '(BoneType BoneCount))
(BoneType)
(BoneCount)))
(defclass Primate ()
((slots :initform '(Weight Height))
(Weight)
(Height)))
(defclass Human (Endoskeleton Primate)
((slots :initform '(Name Age DoB BoneType BoneCount Weight Height))
(Name)
(Age)
(DoB)))
so that you can have access to the name of every element, whether
or not it's been inherited or defined locally in that class. I'm
struggling to see how to implement this in other languages.
I implemented this while still learning about CLOS[2]; it simply occurred to me to do this in order to save all my objects to files and then read them back in a platform independent manner. I saved the objects simply as "name=value" pairs, where the "name" was the slotname and the "value" was the value of that slot.
This is not to show how to do reflection in lisp, but to illustrate the possibilities that the programmer has to solve his problem. There are many other powerful aspects of lisp such as the simple syntax (which will take all of 60 minutes to pick up and remains consistent throughout the language) or the ability to have anonymous functions (which are getting popular in other languages now) or even the full-featured error handling system (which makes exception throwing and catching in other languages look like a toy system) but this example shows best, I think, the power of having a programmable programming language.
Notes:
[1]
The python readers don't have to sit this one out; pretend that the python dir function did not exist, and you are trying to add it in, like the way I asked how hard would be to add for to the language had it not existed. The point is not to show how reflection and introspection can be added to a class, but to show that it can, in fact, be done with ease without any primitive reflection or introspection functions available in Lisp.
[2]The Common Lisp Object System, possibly the most powerful object system I've had the pleasure of using.