BlogHome

style

2020-12-03

Tutorial on Good Lisp Programming style. (1993)

Peter Norvig.

What is good style?

Good programming style

Elegance is not optional

Good style in any language leads to programs that are:

  • Understandable
  • Reusable
  • Extensible
  • Efficient
  • 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
  • Compilers
  • Text editors
  • Tools
  • 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:

  • Understandable
  • Reusable
  • Extensible
  • 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
  • Functions
  • 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

Be explicit

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.

Declarations

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.

Comments

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 specific

Be as specific as your data abstractions warrant, but no more.

Be concise

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

Be helpful

Documetnation should be organized around tasks the user needs to do, not around what your program happens to provied.

Be conventional

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 consistent

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

{: data-content="Tips on built-in functionality"}

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.

Error detection

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

Abstraction

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

Approaches:

  • 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

Desgin: decomposition

  • 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

Data 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

Functional abstraction

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

Control abstraction

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:

  • Concise
  • Self-documenting
  • 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
  • Series
  • 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

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
  • Architecture
  • Component design
  • Implementation
  • Debugging
  • Tuning

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
  • Portability
  • 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

Portability

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

Miscellaneous

  • Expect the unexpected
  • Read other people's code
  • Prototype