Notes from the book “On Lisp” by Paul Graham

mmontoya
7 min readDec 8, 2017
  • The flexibility of Lisp has spawned a whole new style of programming. In Lisp, you can do much of your planning as you write the program.
  • Indeed, the greatest danger of Lisp is that it may spoil you: you won’t be able to go back to another language without always feeling that it doesn’t give you quite the flexibility you need.
  • In Lisp, you don’t just write your program down toward the language, you also build the language up toward your program. As you’re writing a program you may think “I wish Lisp had suchand-such an operator.” So you go and write it. Language and program evolve together.
  • Lisp tends naturally to have fewer operators. There are two ways to add new operators to Lisp: functions and macros. Macros are programs that write programs.
  • The thoughtful use of macros leads to programs which are marvels of clarity and elegance. No other language has anything like Lisp macros.
  • In Lisp, programs are data, but the implications of this fact take a while to sink in.
  • In fact, except for a small number of operators called special forms, the core of Lisp is a collection of Lisp functions.
  • Lisp supports one data type which may at first seem surprising: the function. It means that in Lisp we can do with functions all the things we expect to do with more familiar data types, like integers: create new ones at runtime, store them in variables and in structures, pass them as arguments to other functions, and return them as results.
  • A lambda-expression is a list with three parts: the symbol lambda, a parameter list, and a body of zero or more expressions.
  • Having functions as data objects means, among other things, that we can pass them as arguments to other functions. This possibility is partly responsible for the importance of bottom-up programming in Lisp.
  • One of the big selling points of object-oriented programming is that it makes programs extensible. This prospect excites less wonder in the Lisp world, where extensibility has always been taken for granted.
  • Because Common Lisp is lexically scoped, when we define a function containing free variables, the system must save copies of the bindings of those variables at the time the function was defined. Such a combination of a function and a set of variable bindings is called a closure. Closures are functions with local state.
  • Tail-recursion is desirable because many Common Lisp compilers can transform tail-recursive functions into loops. Many Common Lisp compilers can do tail-recursion optimization, but not all of them do it by default.
  • It cannot be overemphasized how important it is that Lisp programs can write Lisp programs, especially since this fact is so often overlooked. Even experienced Lisp users rarely realize the advantages they derive from this feature of the language.
  • The previous chapter explained how Lisp and Lisp programs are both built out of a single raw material: the function.
  • Functional programming means writing programs which work by returning values instead of by performing side-effects. Side-effects include destructive changes to objects and assignments to variables.
  • Having functional programming as an ideal doesn’t imply that programs should never have sideeffects. It just means that they should have no more than necessary. It may take time to develop this habit.
  • In other languages, one of the most common causes of side-effects is the need for a function to return multiple values. If functions can only return one value, they have to “return” the rest by altering their parameters. Fortunately, this isn’t necessary in Common Lisp, because any function can return multiple values.
  • The aims of functional programming may show more clearly when contrasted with those of the more common approach, imperative programming. A functional program tells you what it wants; an imperative program tells you what to do.
  • What skates are to ice, functional programming is to Lisp. Together the two allow you to travel more gracefully, with less effort. One of the obstacles to learning Lisp as a second language is learning to program in a functional style.
  • The trick is to realize that an imperative program is a functional program turned inside-out. To find the functional program implicit in our imperative one, we just turn it outside-in.
  • Instead of treating all side-effects as equally bad, it would be helpful if we had some way of distinguishing between such cases. Informally, we could say that it’s harmless for a function to modify something that no one else owns.
  • When you’re writing functional code, you can narrow your focus: you only need consider the functions that call, or are called by, the one you’re writing. (Cognitve load).
  • In Lisp it is comparatively easy to debug programs. A lot of information is available at runtime, which helps in tracing the causes of errors. But even more important is the ease with which you can test programs.
  • Programs written in the functional style can be understood one function at a time, and from the point of view of the reader this is its main advantage.
  • Experienced Lisp programmers actually design their programs to be easy to test:
  1. They try to segregate side-effects in a few functions, allowing the greater part of the program to be written in a purely functional style.
  2. If a function must perform side-effects, they try at least to give it a functional interface.
  3. They give each function a single, well-defined purpose.
  • In Lisp, as in any language, development is a cycle of writing and testing. But in Lisp the cycle is very short: single functions, or even parts of functions.
  • Introductory programming courses teach early on that abstraction leads to less duplication of effort. One of the first lessons is: don’t wire in behavior.
  • Bottom-up programming makes what would otherwise be a large program look like a small, simple one.
  • An orthogonal language is one in which you can express a lot by combining a small number of operators in a lot of different ways. Toy blocks are very orthogonal; a plastic model kit is hardly orthogonal at all. The setf macro also improves Lisp’s orthogonality.
  • If some function is expensive to compute, and we expect sometimes to make the same call more than once, then it pays to memoize: to cache the return values of all the previous calls, and each time the function is about to be called, to look first in the cache to see if the value is already known.
  • There is another recursive pattern commonly found in Lisp programs: recursion on subtrees. This pattern is seen in cases where you begin with a possibly nested list, and want to recurse down both its car and its cdr.
  • The Lisp list is a versatile structure. Lists can represent, among other things, sequences, sets, mappings, arrays, and trees.
  • Generally, data structures are used to represent. An array could represent a geometric transformation; a tree could represent a hierarchy of command; a graph could represent a rail network.
  • By using closures to represent something we would otherwise represent with static data structures, we can often expect substantial improvements in elegance and efficiency.
  • Closures have three useful properties: they are active, they have local state, and we can make multiple instances of them.
  • Many programs involving networks can be implemented by compiling the nodes into closures. Closures are data objects, and they can be used to represent things just as structures can.
  • The definition of a macro is essentially a function that generates Lisp code — a program that writes programs. The step of building the new expression is called macroexpansion. After macroexpansion comes a second step, evaluation.
  • Many of the difficulties you might encounter with macros can be avoided by maintaining a sharp distinction between macroexpansion and evaluation.
  • Backquote is a special version of quote which can be used to create templates for Lisp expressions. One of the most common uses of backquote is in macro definitions.
  • Destructuring is a generalization of the sort of assignment done by function calls. Used sparingly, parameter list destructuring can result in clearer code.
  • Most of the time there is a clear distinction between the cases which call for macros and those which don’t. By default we should use functions: it is inelegant to use a macro where a function would do. We should use macros only where they bring us some specific advantage.
  • As destructuring is a generalization of assignment, pattern-matching is a generalization of destructuring. The term “pattern-matching” has many senses. In this context, it means comparing two structures, possibly containing variables, to see if there is some way of assigning values to the variables which makes the two equal.
  • A continuation is a program frozen in action: a single functional object containing the state of a computation. When the object is evaluated, the stored computation is restarted where it left off. In solving certain types of problems it can be a great help to be able to save the state of a program and restart it later. In multiprocessing, for example, a continuation conveniently represents a suspended process.
  • Programming languages save us from being swamped by a mass of detail. Lisp is a good language because it handles so many details itself, enabling programmers to make the most of their limited tolerance for complexity. This chapter describes how macros can make Lisp handle another important class of details: the details of transforming a nondeterministic algorithm into a deterministic one.
  • It is possible to write programs bottom-up in any language, but Lisp is the most natural vehicle for this style of programming. In Lisp, bottom-up design is not a special technique reserved for unusually large or difficult programs. Any substantial program will be written partly in this style. Lisp was meant from the start to be an extensible language.

--

--