(Based on 32 years of teaching computer programming.)
Page Last modified: 6 January 2016
How to approach the task:
work backwards from the product desired
the main reason for doing this is to identify first
what you need. For example, if you need to plot something,
to do that you might need a particular plot package, and the variables
you send to that package may have to satisfy specific requirements. etc.
write down the tasks to be performed in reverse order and
in English, not code. (Eventually, these statements could become
the comment lines in the program.)
Here is a simple example: "draw the function f(x) = ex from 0
to "
Use a big sheet of paper, or open a new wordprocessor file.
write down the key task (somewhere near the bottom of the paper
if you use paper) e.g. choose plot scheme
leave some blank lines then write above what the task is needed
prior to the one you just wrote. e.g. define values of f
leaving some space above that and write what task comes before the task
you just wrote, etc. e.g. define constants (like
, array sizes, grid
interval, etc.)
continue the steps until you reach the program start: where storage
is allocated, variables are declared, etc.
Do the same for what comes after that initial, key task.
Now you have the basic structure of the program and can start
inserting code into those blank spaces you left between the named
tasks.
This may sound tedious, but unless you have sufficient experience,
it is hard to know
at the start just what you will need to have a working program
in the end.
It can be hard to avoid errors when you have to change
a program once you've started it.
Also, the initial outline of
these tasks may help you decide how to divide tasks amongst
subprograms.
Try to develop a simple, consistent style for handling tasks
Usually the more compact the code the more clever you must be;
simplicity avoids programming that is too clever for you to figure out a
day later, etc
Much programming involves doing the same type of task in program
after program; if you are consistent, you will avoid making the same
error twice.
On the other hand, don't cling to an incorrect or inefficient method,
of course!
The classic style of programming is to divide the program into
simple, self-contained tasks.
When done that way, the parts of the code are easier to debug and
easier to make more general.
This approach has a name: modular programming
Always strive to make parts of the code general.
For example, instead of using the value of the grid interval in a
finite difference formula, use a variable, such as "DX" which is defined
in an easily- found location (such as the top of the subprogram, module, or in a
parameter statement).
By keeping your code general you broaden the range of uses for that
program and you make the code transportable to other programs.
Another example: instead of plotting a number that is supposed
to be the current value of a variable. Plot the numeric value that
variable currently has. Examples using NCAR graphics can be found
in our NCAR graphics character set locally-produced help page.
Here is an example:
character*10 chrs ! at top of program unit
t=3.5 ! anything that fits the format in the write below:
write(chrs,'(6htime = ,f4.2)')t ! write "time = "
then value of t in f4.2 format
call plchhq(0.0,0.70,chrs,16.,0.,0.)
Be careful about using floating point divides to define an integer.
For example, ntstep = t/dt may give you 1 less than the number of
time steps you want in certain compilers. You might want to print out
the numbers just to make sure they are what you expect.
Avoid name conflicts with existing subprograms, in line
functions, intrinsic functions, etc.
Avoid names like: "PRINT" or "REAL", etc. for names of variables,
subroutines, etc.
Avoid using default "integer" names for "floating point" variables
(and vice-versa).
It can be beneficial practice to use mnemonic
abbreviations for names since that can make debugging and later re-use
easier.
Take the time to add comment lines
Even a cryptic comment may make a big difference later on when you
are trying to remember what the code was for and how it works.
This is especially important to help you figure out
how to modify it to perform a similar but different calculation.
You may want to
avoid using local conveniences in order to develop
habits and to have code that are transportable to other computing
environments.
For example,do not use the tab key. Different compilers and OS may
interpret that spacing differently, or worse, as a command...
Another example: learn the unix editor "vi" since all unix systems
have it. (You may still want to do nearly all your tasks in
some other editor that you find more workable. Even so, knowing the
basics of vi might be very useful at some point.)
Other advice:
I generally do not recommend using: "free format fortran".
Once an option in f77 it has become the standard in f90 and
its elements are required in f95 (apparently).
never, NEVER, NEVER use default values for any variable. Some
compilers make it easy to accomplish this. A generic way to do this
is to have the fortran statement
implicit none at the top of your file. It will catch you making a misspelling. It
will not catch undefined variables, for that you need another option and
it is usually specific to a given OS and/or machine.
Create a listing and use compiler options to do additional
error checking beyond the defaults.
A typical compiler command is illustrated
below for a file containing NCAR graphics calls & using ncargf90 on moe:
ncargf90 -Mlist -Mbounds template.for
where template.for is the file being compiled. This creates a listing
file (e.g. template.lst) and checks bounds. The same options work for
the pgf90 command.
Check for consistency. Be extra careful that your subroutines are
consistent with your main program and with each other. A very common
error is to request an out-of-bounds location when a variable is
improperly passed to a subroutine.
When debugging your program, take advantage of the
generalities
you have built in and run a streamlined version of the program.
For example, use fewer grid points, or fewer time steps, etc. when
debugging so as to reduce time waiting for job execution. If you have
made your program general, it should be easy to make these
temporary changes.
Many (but not all) errors show up after a few time steps or with
coarse resolution.
Also, it may be feasible to solve a greatly reduced problem
"manually" in order to check your code. Alternatively, try to
reproduce a known result first. Always have a good idea of what to
expect; if your output does not match your expectations, it is
more likely you have an error than a "fantastic new result".
When debugging a program, produce a compiled listing.
It can help
identify undefined variables, unpassed variables, etc.
If that does not
help, then determine unequivocally where a program stops (due to an
error) by using print statements (even "dummy" prints).
Never assume you
know the value of a variable that is used where the program stops;
print it out
at that point in the program. Another error may have overwritten
the value you think it has.
Integer arithmetic
Avoid integers in fractions and in combination with floating point values
(so called 'mixed mode' arithmetic). The result can be undesired.
One exception: when the power is an integer. For example, if you want
y to equal x squared, then use:
y = x**2 ! because it means multiply y by itself 2 times
whereas
y = x**2. !slower because it means calculate y = exp( 2 ln(y) )