A Guide to Formatting Lisp

My notes on formatting Lisp code — as a professional designer who is just starting with Lisp and trying to make sense of it all

Ashok Khanna
15 min readOct 25, 2020

Credits: Google Common Lisp Style Guide, Emacs 21.2 Manual, Strandh’s Tutorial on Indentation, Rainer Joswig, ACow_Adonis, following Reddit users: defunkydrummer, lispm, KaranasToll, kazkylheku, theangreymacsshibe, xach, zulu-inoe

Lisp is infamous for its reliance on parentheses ( ) to delimit code and is notorious for being difficult to read because of this. As many experienced lisp programmers have pointed out, and as this article will hopefully show, properly indented code makes reading lisp much easier and the parentheses disappear into the background.

In this article, we will discuss some basic concepts for formatting Lisp code and I will share with you my personal notes, as a professional designer of presentations and reports. I hope you find it useful and that it makes it easier for you to read, write and enjoy lisp code.

P.S. I have written a guidebook and cheat sheet for Common Lisp on GitHub — check it out if you are interested in some introductory materials for learning Common Lisp!

1: Executive Summary

Lisp code should be formatted according to the following guidelines.

Indenting

Indent your code the way a properly configured GNU Emacs does. In practise, this means relying on a lisp editor (such as Emacs) that indents code automatically.

  • Function Arguments: Aligned with the first argument; if the first argument is on its own line, it is aligned with the function name
  • Body of Forms: Indented two spaces
  • Distinguished (“special”) Arguments: Indented four spaces

Examples

;; Align arguments to a function with the first argument:
(my-function arg-one
arg-two
arg-three)
;; Or under the function name:
(my-function
arg-one
arg-two
arg-three)
;; Body should be nested two spaces:
(when something
(do-this)
(and-this)
(and-also-this))
;; Distinguished forms should be nested four spaces:
(with-slots (a b)
distinguished-form
(print a)
(print b))

Line Spacing / Newlines

When an lisp form does not fit on one line, consider inserting newlines between the arguments so that each one is on a separate line. However, do not insert newlines in a way that makes it hard to tell how many arguments the function takes or where an argument form starts and ends.

;; Bad:
(do-something first-argument second-argument (lambda (x)
(frob x)) fourth-argument last-argument)
;; Good:
(do-something first-argument
second-argument
#'(lambda (x) (frob x))
fourth-argument
last-argument)

Spacing of Parentheses

Always space elements like this: (+ (1 2) 3) and never like this (+(1 2)3) or (+ ( 1 2 ) 3).

Do not put ending parentheses on newlines

Always put ending parentheses on the last line, and not seperately on a newline. Lisp programmers read code by indentation and not by parentheses so we do not need to allocate extra lines for closing parentheses.

;; Good:
(defun our-equal (x y)
(or (eql x y)
(and (consp x)
(consp y)
(our-equal (car x) (car y))
(our-equal (cdr x) (cdr y)))))
;; Bad:
(defun our-equal (x y)
(or (eql x y)
(and (consp x)
(consp y)
(our-equal (car x) (car y))
(our-equal (cdr x) (cdr y))
)
)
)

Vertical lines between top-line forms

We should keep one blank line between top-level forms, unless they relate to definitions that are related to each other, e.g.:

;; Good:(defun function-one
...)
(defun function-two
...)
;; Bad:(defun function-one
...)
(defun function-two
...)
;; This is okay:(defvar font "Arial")
(defvar bg-color "Black")
(defvar text-color "White")
(defvar font-size "12pt")

Formatting Comments

Comments in Lisp are written with four levels of depth and are written before the code they are discussing (with the exception of margin comments):

  1. We use four semi-colons ;;;; for file headers and important comments that apply to large sections of code
  2. We use three semi-colons ;;; for one or a small group of top-level forms
  3. We use two semi-colons ;; for code fragments (multiple lines of code) within a top-level form. They are indented at the same indentation level as the code
  4. We use one semi-colon ; for margin comments, written on the same line and after the code itself. You should try to vertically align consecutive related end-of-line comments.
;;;; project-euler.lisp
;;;; File-level comments or comments for large sections of code.

;;; Problems are described in more detail here: https://projecteuler.net/

;;; Divisibility
;;; Comments that describe a group of definitions.

(defun divisorp (d n)
(zerop (mod n d)))

(defun proper-divisors (n)
...)

(defun divisors (n)
(cons n (proper-divisors n)))

;;; Prime numbers

(defun small-prime-number-p (n)
(cond ((or (< n 2))
nil)
((= n 2) ; parenthetical remark here
t) ; continuation of the remark
((divisorp 2 n)
nil) ; different remark
;; Comment that applies to a section of code.
(t
(loop for i from 3 upto (sqrt n) by 2
never (divisorp i n)))))

2: Contents

The structure of the article is as follows:

  1. Part 1 — Whitespace Concepts: A brief conceptual discussion on the use of whitespace in code formatting (specifically indentation and newlines)
  2. Part 2 — Basic Indentation: Introducing you to the basic use of indentation in lisp code to reflect nesting
  3. Part 3 — Basic Newlines: Introducing you to the basic use of newlines in lisp code to breakup complicated expressions
  4. Part 4 — Special Indentation: A discussion of formatting function calls, function definitions and special forms and the use of special indentation here. A complicated section, but the most important.
  5. Part 5 — Concluding Remarks: Summarising the guidelines discussed in parts 1 to 4 above, introducing the pretty printer and finally wishing you good luck :)

Note: Indenting & formatting are two terms that are often used interchangeably. To clarify on these terms, indentation refers to horizontal space on a line, whilst formatting is a more broader concept that covers indentation, use of newlines, colors, font sizes, font weights and so forth. For our purposes, we will restrict formatting to indentation plus newlines.

Inspiration for this Article

The inspiration for this article came from the below post by ACow_Adonis on Hacker News:

You usually start out learning lisp like other languages: you’re just trying to learn the syntax of various commands and the quirks of how everything fits together of the concepts (if you’ve learnt another language) that you probably already know.

Then one day, you wake up, and you look at code, and some switch you weren’t even aware of has flicked in your brain, and it all looks different. Mature lispers read/structure code subconsciously using both symbols and the indentation style to convey real information about the structure of a program, which is why you can really mess with their heads if you mess yours up too much. But there’s a difference between being told you can do it this way, and the practice that actually makes you do it, because its not a conscious thing.

The mental effect this has on you is not unlike that scene towards the end of the matrix where Neo looks down the hallway and his perception of the thing he’s been interacting with this entire time just fundamentally changes. Its no longer writing code, that is to say, strings of text. Its interacting with and perceiving the very structure of the programs themselves.

The funny thing is, they always say they’re Neo in the matrix, which i understand is cooler, partly because that final scene captures the phenomenon of what it feels like, but its arguably summed up in a more lackadaisical way by the character Cypher in one of his quotes from the same movie: “You get used to it, though. Your brain does the translating. I don’t even see the code. All I see is blonde, brunette, redhead. Hey uh, you want a drink?”

3: Whitespace Concepts

The key to well-formatted code is whitespace. More whitespace between objects implies less relation between them, whilst the closer objects are visually (i.e. less whitespace), the more closely they are assumed to be related.

Horizontal whitespace is achieved through indentation and vertical whitespace is achieved through newlines.

  • Indentation: In most languages, we read left to right and thus indented code naturally implies a level of nesting, whilst non-indented code implies no nesting. When you consider the analogy of using bullet points (same level) and sub bullet points (nested) in text, you can see the obvious and near universal meaning implied by indentation.
  • Newlines: We read top to bottom and paragraphs are used to divide content (text within a paragraph is more related to each other than text in the preceding or succeeding paragraph). Thus, we expect headers to precede bodies of content and items on the same line are assumed to be more closely related than items on seperate lines.
;; Indented code implies a nesting structure:(when something
(do-this)
(and-this)
(and-also-this))
;; Non-indented code implies no nesting (each expression is sequentially evaluated after the last):(do-this)
(and-this)
(and-also-this)
;; We generally expect headers to precede bodies (body in bold in below example):(function-header)
(do-this)
(and-this)
(and-also-this)
; In contrast, this looks more strange:

(do-this)
(also-this)
(and-this)

(function-header)
;; Presence or absence of newlines help imply level of relatedness between items:; The forms are more related here:(function form-1 form-2 form-3); The forms are less related here:(function form-1

form-2
form-3)

4: Basic Indentation

In Lisp, the form header (a vague term, but which covers its name, the paremeters it accepts and other initialisation information) precedes the body of the form, in line with most languages and our natural expectations.

The first general rule we want to apply is that the body should be indented two spaces under the header. Thus, we can use indentation to visually seperate the header and body of a lisp form (this seperation is reinforced by our expectations that the first part of a form is its header and the second part is its body). This also visually helps us identify where and how lexical scoping and closures apply.

In a similar manner, the bodies of subforms should also be indented two spaces under their headers, to create a level of nesting, whilst code at the same nesting level should have the same level of indenting (implying each expression is consecutively evaluated after the last).

;; Good: Clear distinction between function and body:(defun my-function (parameter)
(body-statement-1)
(body-statement-2)
(body-statement-3))
;; Good: when something is the header and do-this is its body. do-this occurs only when 'something' is true, the indent reflects this nesting / closure(when something
(do-this))
;; Bad: We have to rely on the additional parentheses at the end of the second line to realise that do-this is a part of the when construct(when something
(do-this))
;;; Examples with subforms
;; Bad: Difficult to see that only do-only-this is evaluated when something-else is true:(when something
(do-this)
(when something-else
(do-also-this))
(and-also-this))
;; Good: Much clearer :)(when something
(do-this)
(when something-else
(do-also-this))
(and-also-this))

As a side remark — The general principle across this article is that all of our formatting guidelines apply recursively to subforms. This creates consistency in our code and makes it much easier to write and read.

5: Basic Newlines

Generally, we write sentences on one line and uses newlines to split paragraphs. Similarly, as a starting point, readers may expect a lisp form (the lisp equivalent to a sentence in English) to be on one line.

The below looks good:

(+ 1 2)(+ (+ 1 2) (* 4 5))

This works because these forms are simple. However we can already see that the second would arguably be more readable as follows:

(+ (+ 1 2)
(* 4 5))

Splitting across lines gives the brain some space to rest and focus on each individual component. This is illustrated better with the below examples:

;; Easier to follow:
(cons (copy-tree (car tree-input))
(copy-tree (cdr tree-input)))
;; Slightly harder to follow:
(cons (copy-tree (car tree-input)) (copy-tree (cdr tree-input)))

Thus in conclusion, where a form is simple, keeping them on one line is best, but when they are complicated, we can use newlines to improve readability.

There is no hard and fast rule on when to split a form across multiple lines because there are two competing implications at play:

  1. Keeping the form elements on one line signals that they are related to each other and allows them to flow nicely from one to the next. However, too much information on one line can be overwhelming
  2. Splitting the form across lines gives the brain more time to process each element. However, it also disjoints the thought process momentarily so should not be used when the elements are easier to understand

Thus, the correct answer on when to split the form across lines depends very much on the complexity of the form in question. That said, in general, lisp is a bit harder to read than other languages, so you may find yourself preferring to split forms more frequently.

When we split forms across lines, ensure the elements are aligned against each other to reflect that they are at the same level of nesting:

;; Arguments are aligned after splitting the form with a newline:(cons (copy-tree (car tree-input))
(copy-tree (cdr tree-input)))
;; A more complicated example, but note how the bolded items are aligned within the let form:(let* ((symbols (mapcar #'compute-symbol l))
(spaces (mapcar #'compute-space symbols (cdr symbols))))
(when (verify-spacing symbols spaces)
(make-spacing permanent spaces)))

6: Special Indentation

We will now build upon the above remarks to cover the various scenarios that we can encounter — function calls, function definitions and special forms, and introduce a second form of “special” indentation (four spaces) that plays an important role in formatting lisp code.

Simple function calls

In mathematics and other programming languages, the function name and its parameters are kept on the same line, so as a starting point we will follow this rule. That said, we will split arguments across lines where it makes sense (see Part 3 above).This gives us two approaches that we apply depending on what we feel works better:

(i) Function call on one line:

(function-name parameter-1 parameter-2 etc)

(ii) Function call across lines: As noted at the end of the previous section — always line up arguments when splitting across lines:

(function-name parameter-1
parameter-2
parameter-3)

This function indentation (which inevitable is always more than the standard two spaces due to the length of the function-name) visually separates the function parameters and prevents them from being considered as nested elements:

;; Standard indentation in this case is confusing as the reader may interpret the parameters to be part of the body of the function and not a parameter to the function:(function-name parameter-1
parameter-2
parameter-3)

Function Definitions

Let’s expand on these concepts and consider how to format code when we define our own functions. A function definition has four parts — its name, its parameters, its document string and the body of the function. Consider the below example:

(defun hello (name &key happy)
"If `happy' is `t', print a smiley"
(format t "hello ~a " name)
(when happy
(format t ":)~&")))

Note how we indent the body (in bold above) — reverting back to our earlier discussion, this makes sense as the body is nested within the function and is in a lexical closure here.

If we want to split the function’s parameters on multiple lines, we can do that as well — aligning the elements as they are at the same level of nesting. For example:

(defun hello (name
&key happy)
"If `happy' is `t', print a smiley"
(format t "hello ~a " name)
(when happy
(format t ":)~&")))

The above version has greater whitespace in our code, it’s quite good in visually splitting the function header from its body. I recommend it in many instances (but perhaps not the above).

Special Forms

Special forms follow their own structure, and lisp editors apply a somewhat complex set of rules to format them. For the purposes of our discussion, we will simplify to a conceptual level that covers most cases:

  • The first n arguments (varies for each form in question) of the form are its distinguished arguments (i.e. “header” of the form) and the rest are considered the body of the form
  • If the argument is part of the body, the line is indented two spaces as per our earlier discussion, to reflect nesting and lexical closure and convey that the forms in the body are evaluated in order
  • If the argument is distinguished, the line is indented four spaces to avoid confusion with both body nesting (indented two spaces) and alignment of parameters for functions (alignment under the first parameter)

Simple Examples of Body Nesting & Sub Nesting

;; Example 1 (body in bold):(when (= (f x) 4)
(setf *level-number* 0)
(unless *do-not-reinitialize*
(reinitialize-global-information x)
(reinitialize-local-information))
(top-level x))

;; Example 2 (body in bold):(let* ((symbols (mapcar #'compute-symbol l))
(spaces (mapcar #'compute-space symbols (cdr symbols))))
(when (verify-spacing symbols spaces)
(make-spacing permanent spaces)))

Examples with “distinguished” arguments

Standard (two space) indentation is not valid for certain special forms, such as if and do forms. Taking the if form as an example:

  • The three arguments of an if form (condition, argument-if-true and argument-if-false) are not evaluated consecutively
  • These are called "distinguished" arguments as each play as specific role as defined by the form’s specific syntax
  • They are not standard body forms evaluated one after the other

To prevent distinguished arguments from being mistaken as part of the form’s body, we rely on the special indentation of four spaces:

;; Wrong: With "standard" indentation, a reader may incorrectly assume that the last two expressions in an if form are consecutively applied after one another:(if (< (g x) 2)     
(top-level x)
(h y))
;; Correct: Greater indentation (special indentation of four spaces) will visually implies there is something "special" about this form:(if (< (g x) 2)
(top-level x)
(h y))

;; Note:
This looks similar to function indenting, but that is purely by chance as there is four spaces from the start to the first argument

The “Do” Loop — Another Example

To close off this section, we will consider another example — the do loop.

;; Definition of a do loop:(do (variable-definition*)
(end-test-form result-form*)
statement*)
;; Good: Confusing, but with practice, it will be easier to read as the various arguments are well identified in the below through appropriate indentation:(do ((i 1 (1+ i))
(j (length l) (/ j 2)))
((= j 0) i)
(iterate i j)
(when (= (f x) 4)
(setf *level-number* 0)
(top-level x)))

;; Bad: Notice when we do not apply special indentation, it becomes very confusing - there is no meaningful differentiation between the various arguments:
(do ((i 1 (1+ i))
(j (length l) (/ j 2)))
((= j 0) i)
(iterate i j)
(when (= (f x) 4)
(setf *level-number* 0)
(top-level x)))

If this is confusing to you, don’t worry, its very confusing to me!

The fact is that special forms and macro have unique and complex structures and the magic of indentation and newlines, whilst helpful, is not a panacea for complex code. But, at least, we made our lives slightly easier. The more we use these forms and learn their idiosyncrasies, the better we will remember their structure and the easier it will get to read their code.

7: Concluding Remarks

We covered a lot of concepts above, and hopefully you can start to understand why we format lisp code in a certain way. Over time, indenting will become second nature as you get used to seeing the same visual shapes for your code. We can summarise this article with the following.

  1. Function arguments are aligned under their first argument
  2. Use standard indentation (two spaces) to reflect nesting, e.g. in the “body” of a form or a closure
  3. Utilise special indentation (four spaces) for distinguished arguments where there is a special structure at play
  4. Seperate arguments in a form with a newline when they are a bit more complicated, aligning these arguments against each other (just like in a function call)

Pretty Printer

Another item I want to briefly touch is Lisp’s inbuilt pretty printer which automatically indents and formats code passed to it. Whilst not perfect (e.g. pretty printers struggle with complex loop forms), and whilst manually formatted code may look better in many cases, it is useful to get familiar with automatic formatting as it can be a shortcut way to standardise somebody else’s code that you have received on your desk.

Here are some relevant examples (credit — xach from Reddit’s r/lisp):

;; Simple Example - no indents or newlines in the argument passed to pretty print:(pprint '(defun our-equal (x y) (or (eql x y) (and (consp x)       (consp y) (our-equal (car x) (car y)) (our-equal (cdr x) (cdr y))))));; Output - automatic formatting applied:(DEFUN OUR-EQUAL (X Y)
(OR (EQL X Y)
(AND (CONSP X) (CONSP Y) (OUR-EQUAL (CAR X) (CAR Y))
(OUR-EQUAL (CDR X) (CDR Y)))))
;; However, we cannot directly apply pretty print on the our-equal function:(pprint our-equal)
=> Error
;; We can sometimes do this via function-lambda-expression:(pprint (nth-value 0 (function-lambda-expression #’our-equal)))
=> works as expected
;; Importantly, we can read lisp code from file and produce pretty print output (the below is an example on my Mac):(pprint (read (open “/Users/ashokkhanna/lisp/test-pprint.lisp”)))

Note: function-lambda-expression does not work always as the saving of the source code at the symbol is not mandated by the CL standard, so any implementation may choose not to do that (this also has performance implications).

Finally...Trust your Editor

In general, don’t get frightened by the above rules. Rely on your editor to correctly indent your code.

To close off this article, below are some common shortcuts in SLIME/EMACS for indenting:

  • C-M-\ (alternatively M-x indent-region) on a selection of code will correctly indent the whole selection
  • C-M-q will reindent all the lines within that parenthetical grouping (On my Mac, I had to do CTRL+ALT+q as CTRL+ESC+q did not work.)
  • C-u TAB will shift an entire parenthetical grouping rigidly sideways so that its first line is properly indented
  • M-x indent-code-rigidly will shift all the lines in the region rigidly sideways, but do not alter lines that start inside comments and strings

8: Good luck!

I hope you find the above helpful. Good luck and drop me a message if you have any comments or feedback, or just want to chat. Hopefully I will see you in the Matrix soon.

--

--

Ashok Khanna
Ashok Khanna

Written by Ashok Khanna

Masters in Quantitative Finance. Writing Computer Science articles and notes on topics that interest me, with a tendency towards writing about Lisp & Swift

Responses (2)

Write a response