Influential Programming Languages, Part 4: Lisp

By David Chisnall

Date: Jan 12, 2011

Article is provided courtesy of Addison-Wesley Professional.

Return to the article

Understanding modern programming languages is much easier when you know about the languages that influenced their design. David Chisnall looks at some languages that have shaped the modern computing landscape. The series concludes by considering Lisp, arguably the most influential programming language of all time.

In the first three parts of this series (ALGOL, Simula, and Smalltalk), we've tracked the evolution of structured programming into object-oriented programming. Occasionally along the way, I've mentioned that languages inherited a feature or two from Lisp. We're now going to retrace our steps to 1958, to one of the first-ever high-level languages, and one which still provides a lot of inspiration to language designers: Lisp.

Garbage Collection

Lisp was the very first programming language to provide automatic garbage collection, based on a stop-the-world mark-and-sweep model. Periodically, the collector would interrupt the program, stop its running, follow every pointer from a small set of roots, and then collect all memory that it hadn't managed to reach.

By modern standards, this design is very primitive; modern Lisp implementations use concurrent generational garbage collection, as do other languages such as Java. For the time, however, that first garbage collection approach was innovative. The programmer didn't have to think about memory allocation; he just had to put up with occasional (brief) pauses.

It's interesting to note that garbage collection is seen as a fairly heavy feature in modern languages, when the original Lisp implementation ran on a machine that only supported about 30,000 words of memory.

Implicit Types

Lisp originally supported two types: lists and atoms. In fact, lists were really pairs, but the second element of a pair could also be a pair, leading to arbitrarily long sequences. Lisp evolved other types later, but they never have to be annotated explicitly. You can always query a Lisp object to find out its type and try manipulating it.

This kind of dynamic typing is common in modern scripting languages. In Lisp, it was quite novel. Earlier programming languages often omitted explicit typing. Most notably, assembly languages rarely tag memory locations with type information, but these languages required that the user always keep track of the type of every memory location or register. With Lisp, the language runtime environment kept track and could always be interrogated if the user forgot. This facility was very important for implementing garbage collection; if the environment didn't know what was an atom and what was a pointer, it couldn't trace the memory.

Interactive Environment

When Lisp was created, writing a program in an assembly language typically involved typing onto tape or punch cards and feeding them into the computer along with an assembler program, getting the binary out, possibly running it through a linker, and then running it. Fortran programs involved a similar process, with the compiler replacing the interpreter.

Lisp introduced the idea of the read-evaluate-print loop (REPL). The environment read a line of Lisp code, evaluated it, printed the result, and then looped back to the start of the process. This change allowed for incremental development, where you gradually wrote a function, tried calling it, tested that it worked, and then moved on to the next bit of code. This system made development significantly faster. Compiling a Fortran program on a machine of the time could take minutes if you were lucky, hours if you weren't. If you encountered a bug, you had to return to the source code, modify it, and redo the entire process from the start.

A Lisp programmer, in contrast, got immediate feedback if he'd done something wrong. More importantly, he could easily see where he'd done something wrong, because he could test each part of the program in turn.

This interactive structure is now common in interpreted languages. Python, Erlang, and so on all provide similar interfaces. The most interesting thing about the Lisp version is that it implemented incremental compilation very early on. In 1962, the MIT implementation of Lisp would compile expressions that were entered in the REPL mode.

Closures and Metaprogramming

Closure is a buzzword in a lot of modern languages, but closures have been in Lisp for a very long time. Because they're simple to create, they're used all over the place in Lisp programs. Even some of the syntax is inherited by other languages. Lisp uses the lambda keyword to define closures—a little bit of syntax picked up by Python, among others. Closures, functions, and macros in Lisp are all defined in almost the same way.

Macros and higher-order functions are very common in Lisp programs. It's been claimed that the correct way to write a program in Lisp is to describe the problem in a domain-specific language and then write an interpreter for that language.

Part of the reason that metaprogramming in Lisp is so common is the homogeneous syntax. In Lisp, programs are represented in S-expressions, which are lists of lists and atoms—the primitive types that Lisp is designed for processing.

Lisp macros are programs that take a set of S-expressions (the program) and produce another set. Other languages have attempted to produce similar metaprogramming tools, such as the C preprocessor and C++ templates, but they're far less flexible. Lisp macros can perform any arbitrary transform on the source code, and they're inherently easy to write because the source code for a Lisp program is written in a form that's trivial to parse.

Detractors claim that LISP stands for "Lots of Irritating and Superfluous Parentheses," and when looking at some Lisp code it's easy to see why. A Lisp expression is either an atom or a list of atoms in brackets ( [ ] ). For example, (+ 12 x) is an expression that adds 12 to the variable x . The first element in the list tells the Lisp interpreter what to do with the rest of the list. To define a function, you'd begin the list with defun , followed by a list of the arguments, and then followed by the expressions that make up the function body.

The extensive metaprogramming capabilities of Lisp lead to very concise source code. It's possible, for instance, to implement almost all of the design patterns in the Gang of Four book in terms of Lisp macros. Rather than implementing the pattern each time you use it, you just invoke the correct macro and get it for free.

Metacircular Evaluation

One test that I've seen proposed for determining how expressive a language is: How easy is it to write a metacircular evaluator in that language? In other words, how easily can you write an interpreter for the language, using the language itself?

In Lisp, this task is very easy. From first principles, it's actually easy because Lisp provides tools for reading S-expressions, and a Lisp interpreter just has to dispatch based on the first element in each expression. More importantly, Lisp contains an eval keyword, which is effectively a function that takes some Lisp code as an argument and runs it.

eval crops up in a lot of languages. It's probably most famous for the security holes that it has caused in poorly written PHP applications, but in Lisp eval is very powerful. In a lot of Lisp implementations, it drives the incremental compiler, rather than just an interpreter, so you can use it to add compiled functions to a program.

It's also used for metaprogramming in a lot of Lisp code. You can construct some S-expressions (trivially—they're just lists, which are the simplest compound data type in Lisp), and then run them as Lisp code. This capability makes Lisp a very popular language for writing interpreters; if you construct Lisp code from your new language and eval it, you often get to take advantage of the Lisp implementation's optimizing compiler.

Stack Computing

Lisp was sufficiently popular, for a time, that people designed machines explicitly for running Lisp code. These machines used a stack-based architecture, rather than one with explicit registers. Stack-based machines fell out of favor because it was hard to extract instruction-level parallelism from them, meaning that they didn't take advantage of techniques such as pipelining and superscalar architectures.

Note These machines provided very dense instruction encodings, though, so it would be interesting to see whether they could be revived now for massively parallel code, with a large number of threads per core and a large number of cores per die.

Stack-based machines live on in virtual machines. Both the Java and .NET virtual machines use a stack-based architecture, inspired (indirectly) by Lisp machines.

Other Paradigms

Lisp wasn't the first language to support object orientation, but it has one of the most comprehensive object systems imaginable. The Common Lisp Object System (CLOS) implements object orientation on top of Lisp. It's not the only way of supporting object orientation with Lisp; the metaprogramming facilities of Lisp have been used to implement pretty much every model of object orientation that you'll find.

One of the interesting features of CLOS is that it supports dispatch based on any of the arguments of a method. In most object-oriented languages, the method is looked up based on its name and the class of the receiver. In CLOS, it can be based on any of the arguments, which makes it very hard to implement CLOS in a way that runs quickly, but it's also very flexible.

Lisp is often called a "functional programming language." This is incorrect—Lisp functions can have side effects, so they're not mathematical functions. It is possible to implement a lot of functional programming patterns in Lisp, however, and Lisp provided a lot of the inspiration for pure functional languages.

If you learn a functional language today, the map and fold operations are among the first things that you'll see. They're given as examples of the power of higher-order functions; they take a list and a function as arguments, and then they apply the function to the values in the list. These operations come from Lisp.

Lisp was also the inspiration for a number of object-oriented languages, most notably Smalltalk and Io. The simplicity of the specification of the first version of Lisp was something that Smalltalk aimed to capture. The entire language was specified in detail in a single paper. Lisp lost this simplicity later; the Common Lisp specification is a very long document, but you can think of most of this stuff as part of the standard library, rather than the language. The core language remains simple, at least conceptually—the implementations are fairly complex.

Longevity

Lisp was created in 1958, and in 2010 people are still using it to create new software. Unlike Fortran, Lisp hasn't changed radically since its inception. Programmers using Lisp in the early 1960s would still recognize the language today, although they'd probably notice that it now has a lot more convenient stuff built in.

Pretty much every high-level programming language inherits something from Lisp. It's easy to be influential when you're first, but it's impressive how many features of modern programming languages were present in Lisp back when the idea of a high-level programming language was still new.

Fortran, which predates Lisp slightly, is still in use today, but a modern Fortran program looks nothing like a Fortran program from 1957. I was at a talk a few years ago where the speaker said, "I don't know what characteristics the programming language used in HPC [high-performance computing] in 20 years will have, but I know that it will be called Fortran." He was probably quite right; the Fortran name is applied to a whole family of languages that are only superficially similar. In contrast, Lisp implementations all share the same core semantics.

Wrapping Up

You may never use any of the languages that we've discussed in this series. Even so, you almost certainly will use a language that inherits some of their characteristics. Spending some time learning where these concepts originated can help you to understand why they're in the languages that you do use.