Tutorial on Good Lisp Programming style. (1993)
What is good style?
Good programming style
Elegance is not optional
Good style in any language leads to programs that are:
- Easy to develop/debug
It also helps correctness, robustness, compability. Our maxims of good style are:
- Be explicit
- Be specific
- Be concise
- Be consistent
- Be helpful (anticipate the reader’s needs)
- Be conventional (don’t be obscure)
- Build abstractions at a usable level
- Allow tools to interact (referential transperency)
What to believe
Don’t believe everything we tell you. Worry less about what to believe and more about why. Know where your “style rules” come from:
- Religion, Good vs Evil “This way is better.”
- Philosophy “This is consistent with other things.”
- Robustness, Liability, Safety, Ethics “I’ll put in redundant checks to avoid something horrible.”
- Legality “Our lawyers say do it this way.”
- Personality, Opinion “I like it this way.”
- Compability “Another tool expect this way.”
- Portability “Other compilers prefer this way.”
- Cooperatin, Convention “It has to be done some uniform way, so we agreed on this one.”
- Habit, Tradition “We’ve always done it this way.”
- Ability “My programmers aren’t sophisticated enough.”
- Memory “Knowing how I would do it means I don’t have to remember how I did do it.”
- Supersticion “I’m scared to do it differently.”
- Practicality “This makes other things easier.”
It’s all about communication
Expression + Undestanding = Commincation
Programs communicate with:
- Human readers
- Text editors
- Users of the program
Know the context
When reading code
Know who wrote it and when.
When writing code
Annotate it with comments, sign and date your comments.
Some things to notice:
- People’s style change over time
- The same person at different times can seem like a different person
- Sometimes that person is you
Value systems are not absolute
Style rules cannot be viewed in isolation. They often overlap in conflicting ways.
The fact that style rules conflict with one another reflect the natural fact that real-world goals conflict. A good programmer makes trade-offs in programming style that reflect underlying priority choices among various major goals:
- Efficient(coding, space, speed, …)
- Easy to develop/debug
Why good style is good
Good style helps build the current program, and the next one:
- Organizes a program, relieving human memory needs
- Encourages modular, reusable parts
Style is not just added at the end. it plays a part in:
- Organization of the program into files
- Top-level design, structure and layout of each file
- Decompositon into modules and components
- Data-structure choice
- Individual function design/implementation
- Naming, formatting and documenting standards
Why style is practical: memory
Good style replaces the need for great memory:
- Make sure any lines are self-explanatory. Also called “referential transperency”. Package complexity into objects and abstractions; not global variables/dependencies
- Make it “fractally” self-organizing all the way up/down
- Say what you mean
- Mean what you say
Why style is practical: reuse
Structured programming encourages modules that meet specifications and can be reused within the bounds of that specification.
Stratified design encourages modules with commonly-needed functionality, which can be reused even when teh specificationi changes, or in another program.
You should aim to reuse:
- Data types
- Control abstractions
- Interface abstractions (packages, modules)
- Syntactic abstrations (macros and whole languages)
Say what you mean
Say what you mean in data (be sepcific, concise):
- Use data abstractions
- Define langauges for data, if needed
- Choose names wisely
Say what you mean in code (be concise, conventional):
- Define interfaces clearly
- Use macrso and languages appropriately
- Use built-in functions
- Creater your own abstractions
- Don’ do it twice if you can do it once
In annotations (be explicit, helpful):
- Use appropiate detail for comments
- Documentation strings are better tha comments
- Say what it is for, not just what it does
- Declarations and assertions
Optional and keyword arguments
If you have to loop up the default value, you need to supply it. You should only take teh default if you truly believe you don’t care or if you’re sure the default is well-understood and well-accepted by all.
If you know type information, declare it. Don’t do what some people do and only declare things you know the compiler will use. Compilers change, and you want your program to naturally take advantage of those changes without the need for ongoing intervention.
Also, declarations are for communication with human readers too.
If you’re thinking of something useful that others might want to know when they read your code and that might not be instantly apparent to them, make it a comment.
Be as specific as your data abstractions warrant, but no more.
Thest for the simplest case. If you make the same test in two places, there must be an easier way.
- Bad verbose, convoluted
- Good conventional, consistent
Documetnation should be organized around tasks the user needs to do, not around what your program happens to provied.
Build your own functionality to parallel existing features. Obey naming conventions. Use built-in functionality when possible:
- Conventional reader will know what you mean
- Concise reader doesn’t have to parse the code
- Efficient has been worked on heavily
Be consisten about which operators you use in neutral cases so that it is apparent when you’re doing something unusual
Choose the right language
Choose the appropriate language, and use appropriate features int he language you choose.
Dynamic is good for:
- Exploratory programming
- Rapid prototyping
- Minimizng time-to-market
- Single-programer (or single-digit team) project
- Source-to-source or data-to-data transformation
- Compilers and other translators
- Probelm-specific languages (DSL)
- Dynamic dispatch and creation (compiler avilable at run-time)
- Tight integration of modules in one image (as opposed to Unix’s character)
- High degree of interaction (read-eval-print, CLIM)
- user-extensible applications
Dynamic is bad for:
- Persistent storage (data base)
- Maximizing resouce use on small machines
- Projects with hundred of programmers
- Close communication with foreign code
- Delivering small-image applicatoins
- Real-time control
Understanding conditions vs errors
Learn the difference between errors and conditions. All errors are conditions; not all conditions are errors.
Distinguis three concepts:
- Signaling a condition, Detecting that something unusual has happened.
- Providing a restart, Establishing one of possibly several options for continuing.
- Handling a condition, selecting how to proceed form available options.
Pick a level of error detection and handling that matches your intent. Usually you don’t want to let bad data go by, but in many cases you also don’t want to be in the debugger for inconsequential reasons.
Strike a balance between tolerance and pickiness that is appropiate to your apllication.
Write good error messages
- Use full sentences in error messages
- No “error” or “;;” prefix. The system will supply such a prefix if needed.
- Do not begin an error message with a request for a fresh line.
- As with other format strings, don’t use embedded tab characters.
- Don’t mention the consequences in the error message. Just describe the situation itself.
- Don’t presuppose the debugger’s user interface in describing how to continue. This may cause portablity problems since different implementations use differnet interfaces.
- Specify enough detail in the message to ditnguish it from other errors, and if you can, enough to help you debug the problem later if it happens
All programming languages allow the programmer to define abstractions. All modern languages provide support for:
- Data abstraction (abstract data types)
- Functional abstraction (functions, procedures)
Lisp and other languages with closures support:
- Control abstraction (defining iterators and otehr new flow of control constructs)
Also Lisp is unique in the degree which it supports:
- Synctatic abstraction (macros, whole new langauges)
Design: where style begins
- Data abstraction: classes, structures, deftype
- Functional abstraction: functions, methods
- Interface abstraction: packages, closures
- Object-Oriented; CLOS, closures
- Stratified design: closures, all of above
- Delayed decisions: run-time dispatch
- Strive for simple designs.
- Break the problem int parts. Desgin useful subparts (stratified). Be opportuinistic. Use existing tools.
- Determine dependecies. Re-modularize to reduce rependecies. Design most dependent parts first.
We will cover the following kinds of abstraction:
- Data abstraction
- Functional abstraction
- Control abstraction
- Syntactic abstraction
Write code in terms of the problem’s data types, not the types that happen to be in the implementation.
- Use defstruct or defclass for record types
- Use inline functions as aliases (not macros)
- Use deftype
- Use declarations and :type slots for efficiency and/or documentation
- Variable names give informal type information
Every function should have:
- A single specific purpose
- If possible, a generally useful purpose
- A meaningful name
- A structure that is simple to understand
- An interface that is simple yet general enough
- As few dependencies as possible
- A documentation string
Most algorithms can be characterized as:
- Searching (some find find-if mismatch)
- Sorting (sort merge remove-duplicates)
- Filtering (remove remove-if mapcan)
- Mapping (map mapcar mapc)
- Combining (reduce mapcan)
- Counting (count count-if)
These functions abstract common control patterns. Code that use them is:
- Easy to understand
- Often reusable
- Usually efficent
Avoid complicated lambda expressions
When a higher-order function would need a complicated lambda expression, consider alternatives:
- dolist or loop
- generate an intermediate (garbage) sequence
- Macros or read macros
- local function
- Specific: makes it clear where function is used
- Doesn’t clutter up global name space
- Local variables needn’t be arguments
Syntactic abstraction defines a new language that is appropiate to the problem.
This is a problem-oriented (as opposed to code-oriented) approach.
An interpreter for simplifying
Write an interpreter o a compiler to simplify your language.
- Simplification rules are easy to write
- Control flow is abstracted away (mostly)
- It is easy to verify the rules are correct
Use macros appropriately
The design of macros:
- Decide if a macro is really necessary
- Pick a clear, consistent syntax for the macro
- Figure out the right expansioin
- Use defmacro and ‘ to implement the mapping
- In most cases also provide a functional interface
Things to think about:
- Don’t use a macro where a function would suffice
- Make sure nothing is done at expansion time (mostly)
- Evalute args left-to-right, once each (if at all)
- Don’t clash with user names
Macros for control structures
Good to use if it fills a hole in orthogonality of your lang.
- Iterates over a common data type
- Follows established sytnax
- Obeys declarations, returns
- Extends established syntax with keyboards
Programming in the large
Be aware of stages of software development:
- Gathering requirements
- Component design
These can overlap. The point of exploratory programming is to minimize component design time, getting quickly to implmentation in order to decide if the architecture and requirements are right.
Know how to put together a large program:
- Using packages
- Using defsystem
- Separating source code into files
- Documentation in the large
- Error handling
Using comments effectively
Use comments to/for:
- Explain philosophy. Don’t just dcoument details; also document philosphy, motivation, and metaphors that provide a framework for understanding the overall structure of the code.
- Offer examples. Sometimes an example is worth a pile of documentation.
- Have conversations with other developers!. In a collaborative project, you can sometimes ask a question just by putting it in the source. You may come back to find it answered. Leave the question and the answer for others who might later wonder too.
- Maintain your “to do” list. Put a special marker on comments that you want to return to later.
Documentation: say what you mean
- Describe the purpose and structure of system
- Describe each file
- Describe each package
- Documentation strings for all functions
- Consider automatic tools
- Make code, not comments
Make your program run well in the environment(s) you use.
But be aware that you or someone else may want to use it in another environment someday.
Mean what you say
- Don’t mislead the reader. Anticipate reader’s misunderstandings
- Use the right level of specificity
- Be careful with declarations. Incorrect declaratoins can break code
- One-to-one correspondence
Expect the unexpected
Read other people’s code