Lisp Style
No Anonymous Constants
The following stylistic principle applies to virtually every
modern programming language
No Anonymous Constants
An anonymous constant is any unnamed number, string, character, or
quoted expression in code. Here are some examples:
- (update-due-date n 30)
- (eql ch #\q)
- (change-direction dir 1 -1)
The problem is not knowing what the constants are, it's knowing
what they mean. An anonymous constant has no name to tell you what
it's referring to. Does the constant 12 refer to "number of
eggs in a carton," "number of apostles," "number of characters
allowed in a last name," or what?
There are two problems with anonymous constants:
- They are hard to read.
- Therefore, they are hard to change.
The meaning of an anonymous constant has to be inferred from the
code context, and that context may require reading the entire
program. Furthermore, constants have a way of not staying constant.
There are egg cartons now that hold 18 eggs. To fix a program
calculating egg prices, written with anonymous constants, we would
have to find every occurrence of 12 and look at the context to see if
that 12 refers to carton size.
Defining Constants
The first and most obvious thing to do with anonymous constants is
name them. In Common Lisp, we do that with defconstant,
e.g.,
(defconstant carton-size 12
"Number of eggs in one carton.")
This special form is like defvar except that it tells
Lisp that the variable being defined is supposed to be a constant.
That means that
- Attempts to assign a new value to carton-size should
cause an error.
- The compiler can assume carton-size won't change and
therefore, in compiled code, replace references to the variable
carton-size with a more efficient reference to the
constant 12.
Here's how our example code with anonymous constants might look
with named constants:
- (update-due-date n default-month-length)
- (eql ch quit-char)
- (change-direction dir x-right y-down)
Common Lisp has a number of pre-defined named constants. The two
best known are T and NIL. Other constants include
pi and lambda-list-keywords.
Naming Constants
By convention, we put stars (asterisks) around the names of global
variables declared with defvar, defparameter, or
special. This is done because global special variables
behave very differently than normal lexical variables:
- They are slower to access.
- Changes to how they are assigned in one piece of code affects
what happens in code arbitrarily far away.
Neither of these points applies to constants:
- They can be compiled away, so they're more efficient than even
lexical variables.
- They can't be re-assigned in code.
For this reason, constant names are not starred. This is true for
built-in constants, such as nil, t, and
pi, and should be true for your own constants.
Use Short Well-Named
Functions Freely
Most novice programmers and many experienced hackers define far
too few functions. If you look at my code, even the code I generate
on the fly in class, you'll see that I rarely write functions longer
than 6 lines of Lisp code. Anything longer gets broken up into
subfunctions. Why? Because code appropriately divided into many short
functions is:
- self-documenting (because function names say what code is
doing, if the names are right)
- easily skimmed (because you can ignore complex control
structures and just read the function names, if the names are
good)
- easily modified (because change points are easily identified
and change effects are localized, if the functions have single
tasks and no side effects)
- easily reused (because functions can be moved directly into
other code, or a shared library, if free of side-effects and
global variables)
Of course, these advantages hold only if the parenthetical
conditions hold: functions needs to be
well-named,
single-tasked, and side-effect free.
For a very compatible view of functions from an experienced C++
programmer and manager, see Steve McConnel's Best Practices column,
"Why You Should use Routines...Routlinely" in IEEE Software,
July/August 1998.
Accessor Functions
There are two ways to communicate global information.
- use global variables, e.g., *line-width*
- use accessor functions, e.g., a reader like
(line-width) and a writer like (set-line-width
...) or (setf (line-width) ...)
In the simplest situations, it's trivial to define a reader and
writer given a global variable.
(defvar *line-width* 72
"Line width for display functions.")
(defun line-width () *line-width*)
(defun set-line-width (n)
(setq *line-width* n))
To enable (setf (line-width) ...) in Common Lisp 1 or 2,
add
(defsetf line-width set-line-width)
In Common Lisp 2, you can replace set-line-width and
defsetf with
(defun (setf line-width) (n)
(setq *line-width* n))
10 Reasons Why Accessors are Better than Globals
- Readability
- Global variables should always have names with stars, e.g.,
*current-color*, to clearly document their special
status (pun intended). But starred variables clutter up code
badly. [When someone hands experienced programmers a page of
code littered with starred variables, their
first reaction is "Asterisks! The gall!" (pun intended but
obscure)] Accessor functions however are just regular
functions and need no such special naming.
- Read-only access
- Often there is global information that the user should be able
to access, but not change. By exporting only a reader function,
you can prevent users from changing information that can not or
should not be changed.
- Write-only access
- While less common, sometimes there is information that needs
to be specified that should not be readable, by some users at
least. (set-password ...) comes to mind, here.
- Uniform Access to Non-Variables
- There's more to a computer than CPU and memory. There are
clocks, I/O ports, and so on. (current-time) and
(set-current-time ...) offer an easy to understand way to
access such information, despite the internal details.
- Localized Values
- Consider line width. When we set it to 50 characters, do we
mean that to apply to all output, including output to files, or
just to output to the screen or to some window on the screen? With
accessors, it's easy to extend the calling format to allow values
to be attached to local contexts, e.g., (set-line-width
stream value).
- Safe Assignment
- Consider line width again. What happens if someone says
(setq *line-width* nil)? Chances are nothing happens
until later when printing is attempted, at which point an error
occurs. With an accessor like set-line-width, bad values
can be caught and prevented at assignment time.
- Simplified Assignment
- Consider line width once more. With an accessor like
set-line-width, we can extend the values it accepts to
include things like named standardized values, such as
(set-line-width :wide).
- Assignment by Example
- Consider date formats. There are many ways dates can be
printed: full month names vs. abbreviated month names versus
digital months, two-digit vs. four-digit years, month-day-year vs.
day-month-year, hyphens vs slashes vs. spaces and commas, etc. A
clever way of making this complex combination of choices simple to
specify is to allow the user to given an example date,
e.g., (set-date-format "2/10/95").
Another use of this would be to set a default pathname for files
by giving the full pathname for one file, from which the
default information could be extracted.
- Assignment by Parts
- Consider default pathnames, e.g., the default pathname for
module binary files. Even though it makes sense to store this
internally as one pathname, it makes more sense to allow the user
to modify pieces of it without worrying about the other parts,
e.g., (set-module-binary-pathname :directory "Lisp:").
- Traceable Access
- You can't easily trace when someone gets or sets the value of
a global variable. No problem with accessor functions.
Comments?
Send mail to Chris
Riesbeck.