An emphasis is placed on techniques that tend to reduce bugs, improve program maintenance, and are visually pleasing. In some cases the only goal of a particular programming style recommendation is the visual aspect. For these recommendations, there may well be reasonable competing techniques. However, we believe that it is crucial that the software we deliver have a certain uniformity in style, both for the benefit of the customer and for the benefit of those who would maintain your software.
This is probably a good time to talk a bit about programming philosophy. The goal of this style guide is not to be a workaid for code reviews. To the contrary, we believe that detailed, formal code walkthroughs do not typically generate much benefit. (In fact, experience teaches us that there usually should be only one of the following two actions from a code review: "Continue Programming" or "Start Over From Scratch".)
The main goal of this style guide is instead a preemptive strike against non-uniform techniques, but more as a teaching tool than as a compliance tool. It should be read once, before writing any software, and then referred to periodically when questions come up. I also expect that this guide will contain errors and omissions. And it's expected that we might develop some new style techniques as we experiment with new ideas or programming language idioms. This guide is not so much a bible, as it is a collection of what we know works well and looks good.
A good name for what this guide is promoting is "the Local Idiom"; that is, software written in the 7B Software style. We know this isn't the only way to do it. It's the way that we do it.
Some of the material in this guide are targeted more for newer programmers,
so please don't be insulted if a lot of this is old news to you.
This is a problem that can very easily afflict a new Tcl/Tk programmer,
because it is extremely easy in that language to just try things out,
re-test, and stop coding when the GUI looks right.
This is never a good idea. It is OK to do code experimentation when
you're trying to understand how some widget works. But it is not OK
to quit before you actually understand, and commit the
experimental code to the CVS tree.
This is not a good idea. It violates the
First Rule of Reliability:
Do not have a single point of failure. In this case, of course,
the single point of failure is the programmer.
Earlier, I said that I'm not a big believer in formal code reviews. You don't
need code reviews to avoid single points of failure. All you really need
are two things:
The tester keeps the programmer honest by worrying about usability
issues that the programmer neglects. The 2nd programmer helps by
periodically taking a look at, or using, the 1st programmer's code.
In this way both programmers learn techniques from each other; a 2nd
programmer also can more easily detect trouble spots in the 1st programmer's
code, by doing an occasional casual reading.
I sometimes refer to this concept as The Program Who Knew Too Much.
We want to avoid writing a routine whose correct operation depends
upon intimate knowledge of a different routine.
It is probably not an exaggeration to say much of what we refer to as
"software refactoring" actually refers to removing duplications in
program logic, thus effectively "factoring" a shared
concept into a single routine.
This rule is important, but it is not necessary to follow it slavishly.
There are some places where insisting on a SPOT is going overboard.
A good example of this is the Tcl/Tk widget name. It is
usually OK to refer to a Tcl/Tk widget by name
(for example: .dialog.listbox) throughout the program, rather than
referring to the name by some variable abstraction.
In theory, this is a duplication; in practice,
creating variables to hold widget names is usually not worth the effort, and
the cure of variable namespace pollution is worse than the disease
of duplication.
Bottom line: Good judgment is required to know when a SPOT violation should
be corrected.
Many ideas for doing "programming in the large" came out of this period
of time. Included in these ideas were:
Egoless Programming,
Object Oriented Programming,
Institutionalized Feedback Loops and Reviews,
Best Current Practices,
Integrated Development Environments,
and some others that I've probably forgotten.
The lesson from looking back on this time was that none of these
ideas actually yield better software.
Instead, experience shows that the way to create the best software is
by following these guidelines:
That last item sounds as if it might be an endorsement of the
Integrated Development Environment, but in fact it's just the opposite.
I have yet to see an IDE that allows the programmer to change and
keep track of software; instead, the IDE itself keeps track.
If you're the programmer, that means that you have to keep track of
both the program and the IDE in order to have any confidence
that what you're writing is in fact what is being generated.
The takeaway from all of this is: To write excellent software,
the programmer must be in control at all times.
(By the way, if you're wondering what Egoless Programming
refers to, it is the concept
that you should not invest any emotion or ownership in the programs you write;
that instead you should trust the team concept to produce high quality
software. Experience has taught us, contrariwise,
that the best programs come from the programmers who care the most.
The fact that nobody remembers Egoless Programming
is testament to its usefulness.)
The whole concept of style is difficult to deal with because of
several factors, the most important of which is probably programmer
autonomy. After all, if the right philosophy is to create an environment
where the programmer is in control, then how is it that any
technique mandating an issue of pure style is defensible?
There are three reasons why pure style issues should be fair game for
a Guide such as this.
Some pure style issues that are described later in this Guide are
rather arbitrary. (The indentation and comment styles are good
examples of this.) We like the styles promoted here, but we realize there may
well be other styles that are equally defensible. I can come
up with reasons why, for instance, a 2-space indentation style is
"better" than a 4-space one; but you might reasonably disagree.
Things like this basically come down to uniformity: We've picked
a style, so let's stay with it.
Now that we've dealt with the five hundred pound elephant in the room,
let's move on to some particular techniques.
Example:
file porridge.tcl can have at most one namespace,
and the namespace's name
is porridge.
Rule.
The file should contain some summary comment information at the top
describing the goal of the namespace.
Example: in file porridge.tcl:
Rule.
Use namespace variables rather than global variables.
Rule.
If a namespace variable is referenced by another source file, then the
namespace variable should be defined at the top of the file.
The namespace variable definition should have a useful comment
(see Comments section below).
Example:
Example:
don't do this:
Rule.
If the Tcl procedure is called from outside of the file, its name
should start with an alpha character (a-z). If it is only referenced
internally within the file, its name should start with an underscore
(_). Organize the procedures so that all of the externally callable
ones are at the top of the file and all of the "for internal use only"
ones are at the bottom. And add a comment where the demarcation point
is, between the external and internal procs.
Example: In this example, ::porridge::warmup
is callable from outside the module and ::porridge::_too_hot
is only called from within the module.
Example:
Rule.
Add 4 spaces for line continuations. Avoid lines that are more than 80
characters.
Example:
Rule.
Procedures start at the beginning of the line. Don't indent the
procedure, even though it's in a namespace. (All of the above examples
use that convention.)
Brief note. We know that there are lots of ways to indent.
This is how we do it. Yours may be better. We know that and we
respect you as a person and a programmer. Do it our way anyway. Please!
To make this work, you should design your module such that all
of the module's namespace variables are initialized once,
when the program starts up, and not each time the module is sourced.
Example:
Don't do this: This will cause the ::porridge::hot variable
to be reset to "just right" each time the module is sourced.
Rule.
A comment should be indented on the same level as the code it's
commenting on. The comment's form should be:
The comment form is to be followed both for comments for procedures
(as in the above example) and for comments within procedure bodies.
Let's move on to the problem of when to comment.
There are two basic types of comments:
Rule.
Description comments should be kept to a minimum.
Description
comments are useful in describing large swaths of code, or for describing
variables that are effectively global in scope. So for that reason, all
namespaces
should have a descriptive comment, and all namespace variables that
are referenced external to the module should have a descriptive comment.
And most procedures should have a descriptive "prologue comment" as well.
Conversely, most procedure bodies should not have any
description comments. The exception to this is a procedure body that
is (necessarily) too long for a programmer to be able to quickly understand
a subpart of the procedure body at a glance.
If you feel the need to add a description comment within a procedure body,
you should ask yourself if the procedure needs to be split into
two or more procedures, or whether it is unnecessarily long, or
whether it should be rewritten entirely.
Rule.
Explanatory comments should be used whenever it is likely that
a programmer will wonder why something is being done.
This type of
comment is used when some clever programming trick accomplishes a task,
or when some unapparent edge case is being handled.
Example. This example provides examples of both
description comments (namespace, namespace variable, and procedure)
as well as an explanation comment in the _see_a_bear procedure.
In the _see_a_bear procedure, there is a brief description comment
that describes the procedure, and there is a brief explanation comment
that makes it clear why the code is checking for small bears that are neither
moving nor sleeping. The assumption is that it would have been difficult
for the programmer to figure out the motivation behind doing that check
without such an explanation.
Otherwise, there are description comments where they help. There is
no description comment for the procedure _call_911,
though it would have been reasonable to supply one.
This goes for if-expressions as well!
Example.
Example:
Example:
The reason for this is twofold:
There are several tools at your disposal to make this happen:
the "Tcl Console" module console.tcl lets you embed
a tcl shell in your program; and the "Tcl Editor" module ped.tcl
lets you inspect and make changes to running Tcl code.
A very good example of a typical use is when the program must open
a connection to a socket service. The program has to open the
socket, wait for the socket to have data on it before reading it,
wait for the socket to become writable before writing it,
and handle the case where the remote server terminates the connection.
Plus, usually there is other housekeeping associated with the socket
having to do with the service that is actually being provided by
the connection.
The best way to handle this in Tcl is to create a namespace variable
whose name is generated on the fly from the (unique) socket identifier.
Example:
Watch out: Socket names are reused by Tcl. So if
you close a socket and then open a new socket, it is quite likely
that the new socket's name will be the same as the old one. So
be careful to clean up when closing a socket! (Such cleanup
is also required to avoid leaking memory, which would not cause the
same behavior that accidental reuse of a name would, but which could
cause serious problems in a long-running application.)
Also note: In the above code,
I could have created a separate proc called _close that
handles both the closing of the socket and the removal of the namespace
variable.
If you hate something in this guide, please let me know. We'll
figure something out. (If you hate
the idea of this guide, I'm not sure I can help though.)
2.0 Some Programming Concepts
Before I jump into coding techniques,
I want to talk a bit about some basic concepts, because they affect
how the code is written in a general way
(though they aren't coding techniques per se).
2.1 Avoid committing experimental code for things you don't understand.
(By commit I refer to publishing source code in the source
code repository.)
2.2 Every job is a 2-person job.
I've seen lots of projects where one person was basically in charge
of writing software, writing the unit test, writing the interface
guide, doing the testing, and writing the installation procedure.
2.3 Single Point of Truth (SPOT).
An important concept in programming is the SPOT, or Single Point of Truth.
The idea is that software should be organized so that there is not a lot
of duplication of data or of program text.
2.4 The Fallacy of Egoless Programming, and Other Tales.
Back in the 1980's (yes, people wrote programs then), there was a lot of
effort expended to figure out how to manage large software projects,
because that was basically the
first time that computing platforms existed that were able to support
such large projects.
3.0 Tcl/Tk Techniques
OK, enough on philosophies. It's time to jump into techniques for
the Tcl/Tk language. But first, a few words about coding style.
3.1 Tcl Source Files.
Rule.
A file containing Tcl code can contain no more than one namespace.
The namespace should be the same as the file's basename.
# This module provides methods to change the temperature of the porridge, and
# to check its current temperature.
#
namespace eval porridge {
...
}
3.2 Namespaces.
Rule.
Use namespaces for any program that needs more than one Tcl source file.
namespace eval porridge {
variable gval ;# The Goldilox setting of the porridge. May be one of:
;# "too hot", "too cold", "just right"
...
}
3.3 Procedure Naming Conventions.
Rule.
The name of a tcl proc should be fairly terse but sufficiently
evocative to be a reminder of what it is doing. Don't use the
nerdProgrammer naming style that looksLikeThis.
# This proc will make the porridge warmer by $deg degrees.
#
proc warmup {deg} {
...
}
# This proc will make the porridge warmer by $deg degrees.
#
proc makePorridgeWarmerBy {deg} {
...
}
namespace eval porridge {
...
proc warmup {deg} {
...
if {[_too_hot $deg]} {error "fire hazard"}
}
...
#
# All procedures below this point are for the internal use of this
# module only. Do not call them from outside this module.
#
# This proc returns TRUE if the porridge temperature in $deg
# is hot enough to set fire to the house.
#
proc _too_hot {deg} {
return [expr {$deg>5000}]
}
}
3.4 Indentation.
Rule.
Add 2 spaces for bodies of while, if, for, proc, etc.
This goes for both of the bodies of the if statement, of course.
proc warmup {deg} {
...
if {[_see_a_bear]} {
_call_911
_find_pistol
} elseif {[_see_a_rabbit]} {
_marvel_at_tail
} else {
_check_stove
}
...
}
proc _call_911 {} {
if {[catch {set pcall [::phone::call 911]} msg]} {
tk_messageBox -icon info type ok -message \
"Oh my. 911 service is out in the Hundred Acre Wood. Perhaps you\
should consider finding your pistol."
return
}
...
}
3.5 Dyanmic Reloadability.
One of the really nice features of Tcl is its ability to re-read
procedures at runtime. This can be a way to do rapid development
and prototyping: run your program, find a bug, make a change to
the source file, and re-source the new source file.
namespace eval porridge {
variable gval ;# The Goldilox setting of the porridge. May be one of:
;# "too hot", "too cold", "just right"
...
# This proc is called exactly once, when the program first starts up
#
proc init {} {
set ::porridge::gval "just right"
}
}
namespace eval porridge {
variable gval {just right} ;# The Goldilox setting of the porridge. May be
;# one of: "too hot", "too cold", "just right"
}
3.6 Comments.
First let's talk about the form of the comment itself.
Example.
# This proc will return TRUE if goldilocks is tired
#
proc is_tired {} {
return {expr [_insulin_kicking_in $::porridge::_last_eaten]}
}
# This module provides methods to change the temperature of the porridge, and
# to check its current temperature.
#
namespace eval porridge {
variable gval {just right} ;# The Goldilox setting of the porridge. May be
;# one of: "too hot", "too cold", "just right"
# This proc is called exactly once, when the program first starts up
#
proc init {} {
set ::porridge::gval "just right"
}
# This proc will make the porridge warmer by $deg degrees.
# It will also look outside to check on wildlife.
#
proc warmup {deg} {
...
if {[_see_a_bear]} {
_call_911
_find_pistol
} elseif {[_see_a_rabbit]} {
_marvel_at_tail
} else {
_check_stove
}
...
}
#
# All procedures below this point are for the internal use of this
# module only. Do not call them from outside this module.
#
# This proc returns TRUE if a bear is seen outside.
#
proc _see_a_bear {} [
set things_seen [_look_outside]
set bears [_get_bears $things_seen]
set any_bears 0
if {[[llength $bears]>0} {
# Check for the case where it's a teddy bear. Watch out for the
# case where it's a very small but very alive cub.
#
foreach bear $bears {
if {[_tiny $bear] && [_not_moving $bear] && [_not sleeping $bear]} {
continue
}
set any_bears 1
break
}
}
return $any_bears
}
proc _call_911 {} {
if {[catch {set pcall [::phone::call 911]} msg]} {
tk_messageBox -icon info type ok -message \
"Oh my. 911 service is out in the Hundred Acre Wood. Perhaps you\
should consider finding your pistol."
return
}
...
}
}
3.7 Some Command-Specific Items.
3.7.1 The "expr" and "if" commands.
Rule.
Always enclose the body of an expr command with braces. This
is both for stylistic uniformity and for performance reasons: Tcl
will compile any expression-body that is enclosed in braces.
...
proc warmup {deg} {
...
if {![_turn_on_stove]} return
_wait 5 ;# Will finish after waiting 5 minutes
# Allow for some loss of heat
#
set deg [expr {[_current_stove_temperature]*.90}]
...
}
(By the way, there are some reasons why you might not want to put a
"wait" inside a proc, but we'll gloss over that for now.)
3.7.2 The "regexp" command.
Rule.
Use the regexp command in most places where you were considering
writing a simple string parser. Don't use the string match
command for anything but the simplest matches. (And even then the
regexp command is probably a better idea.)
proc _whichbear {bear} {
if {[regexp {mama bear: (.*)} $bear -> name]} {
return "mama bear's name is $name"
}
...
}
3.8 Boolean or enumerated parameters.
Rule.
Use a set of values named "-VALUE" to implement a boolean
or enumerated parameter.
proc _goldi_in_house {{chairstate -notbroken} {bedstate -nosleeper}} {
return [expr {$chairstate!="-notbroken" || $bedstate!="-nosleeper"}]
}
In the above, the variables chairstate and bedstate are
enumerations; their values might range over a set of states (or they
might just be booleans). The values of the enumeration are of the
form "-VALUE" because it is very easy for the program reader to see what's
going on in the call to the routine:
if {[chair_in_2_pieces]} {set chstate -broken} {set chstate -notbroken}
if {[_goldi_in_house $chstate -nosleeper]} {
puts "Someone's in this house"
}
Compare this to the usual way of dealing with booleans or enumerations:
set chstate [expr {![chair_in_2_pieces]}]
if [[_goldi_in_house $chstate 0]} {
puts "Someone's in this house"
}
It's harder to see what the semantics of chstate is. It's even harder
to figure out what that 2nd parameter to goldi_in_house is all about.
3.9 Debug Output.
Rule.
It is fine and expected that you'll add debug statements to
your Tcl code during development (using for example the puts command).
However, the code you commit should contain no such statements.
You won't really know what
you are looking for when you are debugging.
(That's what it means to be a bug.)
So it doesn't really
help much to put a bunch of debug output commands in your code:
it's likely that the bug won't be apparent from
the output. For this reason it's much more important that
your code be introspectable.
3.10 Name Mangling.
Name mangling is a powerful Tcl technique because it can simulate a
simple object-oriented system, without resorting to all of the care
and feeding that the programmer typically has to do in a "true"
object-oriented programming language.
namespace eval porridge {
...
# Place a call to grandma. She's on x2525 at www.grandma.com
#
proc talk_with_grandma {} {
if {[catch {socket www.grandma.com 2525} fd]} {
tk_messageBox -icon error -type ok -message "No grandma: $fd"
return
}
upvar #0 ::porridge::_grandma_$fd V
set V(state) quiet
fileevent $fd readable [list ::porridge::_handle_grandma $fd]
}
# Handle a message from grandma
#
proc _handle_grandma {fd} {
upvar #0 ::porridge::_grandma_$fd V
if {[eof $fd]} {
tk_messageBox -icon info -type ok -message "Grandma no longer talking"
unset V
close $fd
return
}
if {[catch {read $fd} buf} {
tk_messageBox -icon error -type ok -message "Grandma phone failed: $buf"
unset V
close $fd
return
}
switch $V(state) {
quiet {
if {[regexp {Hello} $buf]} {set V(state) greeted}
puts $fd "Hi, grandma"
flush $fd
}
default {
if {[regexp {Goodbye} $buf]} {set V(state) finished}
}
}
if {$V(state)=="finished"} {
puts $fd "Bye, grandma"
unset V
close $fd
}
}
}
What I want you to notice is the upvar command, which is used
to create a namespace variable named ::porridge::_grandma_$fd and
to give it the "local name" of V.
This variable exists as long as the socket exists, and is used to
track the current state of the conversation with grandma. When
the socket closes, the variable is deleted.
4.0 C and C++ Techniques
This section will be filled in at a later time. Do be sure check on
existing examples of 7B Software C and C++ code to use as
a guide. The non-language-specific rules for C/C++ are essentially
the same as those for Tcl/Tk.
5.0 Conclusion
The goal is for this guide to allow you to think about the design
of the program, rather than wrestling to determine the right
low-level technique to accomplish a task. "How many spaces should I indent?"
"How do I manage dynamic but persistent data?" "Should I use
string first in a loop parsing a string?" Answering questions
like this shouldn't be taking up your time.