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 in Lisp
In Common Lisp, we name constants 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.
Unlike global variables defined with defvar and defparameter, constant names are not starred. This is true for built-in constants, such as nil, t Stars (asterisks) are put on global variables to highlight them because
- They are slower to access.
- They re-bind differently in forms LET
- Changes to their value can affect code arbitrarily far away.
None of these points applies to constants, because they can't be rebound.
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...Routinely in IEEE Software, July/August 1998.
Export Accessor Functions, Not Variables
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.