Version: 1.33
Release Date: Oct 23, 2002
Author:
J. Borg,
KEP3
Introduction
The following topics will be discussed in this document:
- Naming conventions.
- Source code presentation standards.
- Coding conditions and constraints.
This document is especially written for IDL (Interactive Data Language) but many
guidelines applies to most declarative programming languages.
It is assumed that the reader has basic knowledge of the IDL language and development
environment.
This standard is aimed at building applications (or libraries) in IDL. This means
that the goal is to produce very high quality code that is easy to maintain. To
follow these practices takes more time than just jumping directly into coding, ad
hoc style. At least for a minor project. But as soon as the task grows (as they
always do), it is well worth the effort.
Many issues described in this document may seem superfluous to a programmer with
a working knowledge of C++ (or similar). The difference is that C++ is strongly
typed and puts emphasis on catching errors during compile time to avoid runtime
errors. IDL on the other hand is dynamically typed and interpretated which means
that a certain amount of safety precautions must be employed by any programmer
doing applications in IDL.
Naming Conventions
File names
The file extension for IDL files should be: .pro
Class (or Structure) definition files must be named: <ClassName>__define.pro
For cross platform portability it is recommended that file names are written
in lower case.
It is not necessary to limit file names to 8 characters unless specifically dictated
by the project.
Function / Procedure Naming Conventions
- Names must be descriptive. Maximum allowable function / procedure name length
is 128 characters. So there is no justification for naming a function that calculates
interest based on date "CalcIntDate". Six months later you will not remember what
it means. (Calculate International Date?) There is some more typing involved,
but please do use long names like "CalcInterestFromDate" or similar. You will
thank yourself later. (The worst thing one can do is to make the name an abbreviation
and then write a comment that explain it! Use the comment as a name instead!)
- Composite names should be defined using "CamelHumps", i.e. capital first letter
of each word.
- The name space of IDL is global. The implication of this is that all functions
in a IDL program have global scope. Therefore care has to be taken so that all
functions / procedures have different names. It is recommended that all functions
in a module have a name that starts with the name of the module.
- This is of course of extra importance when creating a library that is to be
reused in several applications. Maybe even passed on to several other customers
who have libraries of their own.
Warning: This is a potential source for serious errors since IDL accepts several
functions with the same name! (The last one processed/compiled is the prevailing one.)
Warning: Also make sure to avoid names that are already defined as IDL functions
or procedures. The code will compile and execute, but the user defined functions
and/or procedures will never be called!
Note that this is an ambiguity in it self. Referring back to the note above which
states that the last instance of a function / procedure is the prevailing one, this
would indicate that the IDL libraries are the last to be compiled. This can of course
not be true, since then no user defined code would compile!
Variable Naming Conventions
- Names must be descriptive. Maximum allowable variable name length is 128 characters.
(A more practical limit is probably 32.) Therefore, abbreviations should be avoided.
As mentioned on Function naming, if you need to comment the name, then use the
comment as the name instead.
- Composite names should be defined using "CamelHumps" i.e. a variable containing
the first array index could be named "nFirstArrayIndex".
- Underscores may not be used, i.e. "n_first_array_index".
- The case of variables must always be retained even though IDL does not enforce
this. (Enhances readability greatly.)
- Class memeber variables should be prefixed with "m_".
- Type prefixes should be used:
| Type |
Prefix |
Example |
| Boolean |
b |
bChanged |
| Integer |
n |
nIndex |
| Long |
l |
lWidgetID |
| Float |
f |
fWeight |
| Double |
d |
dVelocity |
| String |
str |
strPersonName |
| Array |
Arr |
strArrNames |
| Pointer |
p |
pDialogWindow |
| Object |
obj |
objPlotData |
| Structure |
struct |
structCompensationData |
| Global |
g |
ngUserCount |
| Member (of class) |
m_ |
m_pEmployeeList |
Note that type prefixes may and should be combined. I.e. a global integer may
be called "ngCalculationsDone”.
- Constants to be set should be all upper case e.g. "MAX_STRING_LENGTH = 127”.
(And yes, here is the exception to the no underscores rule above...)
- Again the name space of IDL is global. The implication of this is that all
common blocks declared within a IDL program have global scope. Because of this
it has to be ensured that all common blocks have different names. It is recommended
that all common blocks within a module have a name that starts with the name of
the module.
Warning: This is a potential source for serious errors since IDL only catches
multiple declarations of common blocks with the same name if subsequent declarations
contain more variables, i.e. an extension of the common block! Otherwise this is
not caught at all. (Another reason not to use them.)
Source Code Presentation Standards
This section describes the basic guidelines for source code formatting.
Function or Procedure Header Information
The following information should always be listed before each function or procedure
in the source code:
- Name
- Category
- A reference to the corresponding section of a Software Design Description.
- Purpose
- Parameters, including type, passing convention and purpose. Note that they
may be separated into Input and Output parameters to clarify which parameters
that may be modified by the function.
- Return value (It is recommended that all functions / procedures return a value.)
- Global data, read or edited. (Try to avoid using global data!)
- Revision history
- Since IDL support keyword parameters to functions / procedures, any keywords
used should be documented with the following information:
- Name
- Type
- Default value and valid range of values
- Purpose
It is of importance to follow a template exactly since this facilitates easy
(automated) extraction of source code documentation. (You don't want to do that
by hand afterwards do you?)
The following text depicts an example layout of a function or procedure header
(‘;’ denotes a comment) :
;***********************************************************************
;+
; Name : LineIsMonotonic
; Category : Data Manipulation
; Design Ref. : <xxx>
; Purpose : Tests if a line is monotonic (increasing or
decreasing)
;
; Dependencies :
;
; Name
Type Pass by
Purpose
; Inputs : fArrLine Float Array Value Line to test
;
; Outputs :
;
; Keyword Type Default Purpose
; Keyword Inputs :
;
; Type Comment
; Return value : Boolean !TRUE if line is monotonic
;
; Global data :
;
; Comment :
;
; This function checks if a line is monotonic
; Example:
;
; |
; | / Will return !FALSE
; | \/
; -------
;
; |
; | / Will return !TRUE
; |/
; -------
;
; Revisions : J. Borg, 21.06.2002 - Created function
;-
;***********************************************************************
Class Header Information
For classes we specify the following block in the beginning of the class definition file:
; **********************************************************************
; * Class : FiFo
; * File : fifo__define.pro
; * Inherits From :
; * Purpose : Just a First in, First out stack.
; * Dependencies : DynArray
; * Known Limitations : None
; * Possible Enhancements :
; * Created : 21/07/02, J. Borg
; * Test Program : LiFo__Test
; **********************************************************************
; **********************************************************************
; Description:
; Class to provide a simple FiFo stack.
;
; Typical Example (no error handling):
;
; fifo = Obj_New("FiFo")
; fifo->Push(42)
; fifo->Push(4242)
; fifo->Push(424242)
; fifo->Pop(data) & Print, data ; Will print '42'
; fifo->Pop(data) & Print, data ; Will print '4242'
; fifo->Pop(data) & Print, data ; Will print '424242'
; fifo->Pop(data) & Print, data ; Error. Will return -!FALSE
; Obj_Destroy, fifo
; **********************************************************************
In front of the class member functions that should be accessable by users of the class we
put the normal function documentation as per above.
Indentation
Code indentation must conform to the follow standards:
- All indentation will have one tab stop steps or four (4) white spaces. (Spaces are preferred of course, but
tabs are easier to use during development. A tip is to use tabs, but when saving convert to white spaces. Any
good editor can do that.)
- Loops will be indented one step per loop.
- Switch (Select Case, etc.) statements should always be indented two steps, one step for the case statement
and one step for the code within the case statement.
- Blocks of code that perform one specific task may be grouped together and indented to enhance code comprehension.
It is recommended that such code blocks are preceded by a comment statement.
- If, at the end of the indentation, it is not obvious where the indentation started, a comment should be inserted.
Multiple indentation levels or very long indentation blocks could cause an ambiguous indentation end.
- Note that the code within a function or procedure must also be indented one level. The function or procedure
declaration and corresponding end statement are considered to be one level above the actual code itself.
The following example illustrates the recommended indentation of a function or procedure:
Procedure Indent
<Statement>
<Statement>
<Statement>
Loop A
<Statement>
<Statement>
Select Case X
Case 1:
<Statement>
<Statement>
Case 2:
<Statement>
End Case
<Statement>
End ; End of Loop A
<Statement>
End ; End of Procedure Indent
Note: For anybody who has received the source code from somebody else, the rule about always replacing tabs
with spaces is obvious. Regardless of what one does, the tab setting will be different for different users and
one can only be certain that it looks good on one's own machine. Spaces on the other hand are very portable!
Line Formatting
Source code line formatting must conform to the follow standards:
- The maximum line length is 80 characters.
- In practise, lines should be shorter than 50 characters to enhance aesthetics.
- One program statement per line as a maximum.
- Conditional statements can exceed one line.
- Blank lines should be used to enhance aesthetics.
Comments
Single line comments
The comment character is ‘;’. The rest of the line is treated as a comment.
Multiple line comments
No characters for multiple line comments exists.
Comments must conform to the follow standards:
- Comments should be written as ordinary sentences.
- Important comments, that proceed large blocks of code, should be boxed.
- Plain comments can be surrounded by blank comment lines to enhance aesthetics.
- Important: There can be too much comments! Do not comment obvious code.
The following example illustrates how code could be commented:
;****************************************
;* The following code does good things. *
;****************************************
<Statement>
<Statement>
<Statement>
; Loop over *valid* instances in list only
Loop
<Statement>
<Statement>
Select Case X
Case 1:
<Statement>
;
;Following statement added because
;of new requirement, ref. PRD 173
;
<Statement>
Case 2:
<Statement>
End Case
<Statement>
End Loop
;
; Clean up and exit.
;
<Statement>
<Statement>
; Release all memory.
<Statement>
Spacing
It is recommended to always use spaces around all tokens of the code, see examples:
nInt=5 ; Not good!
nInt = 5 ; Preferred
If(nInt==5) ; Not good!
If ( nInt == 5 ) ; Preferred
If (nInt == 5) ; Also accepted but not as good
The reason for this is that it makes searching after variables much easier. If the programming style
without spaces is used, a search after "nInt” would also find all instances of "nInteger”, "nIntTemp”,
etc. If adequate spacing is used, it is possible to search for "nInt " (with trailing space) and only
find the relevant variable.
Apart from aiding searching, proper use of separating spaces also makes the code easier to read!
It has been common practice since a thousand years to separate words by spaces. Until 1957 when
FORTRAN came along and changed all that...
Use of Parenthesis
IDL does not enforce the use of parenthesis around conditional expression. It is recommended to use them
anyway to enhance the appearance of the source code.
Example:
If nIndex EQ 4 Then ; Allowed syntax. Should not be used.
If (xIndex EQ 4) Then ; Preferred usage
Array Access
The IDL syntax allows access to array elements by using both square brackets ‘[ ]’ and normal brackets ‘( )’.
This is very unfortunately since function calls also use ordinary brackets.
To minimise the ambiguity, square brackets must always be used for array access.
Example:
nArr = IntArr(5) ; Create integer array with five elements
nA = nArr(1) ; Allowed syntax. Do not use!
nB = nArr[1] ; Preferred usage.
Coding Conditions and Constraints
Functions and Procedures
Functions and procedures must conform to the follow standards and guidelines:
- All functions and procedures must return a value that can be easily checked for an error
condition. This implies that:
- A procedure must return a Boolean with the value of TRUE or FALSE depending on the result of the procedure
execution. (This would make all procedures functions.) (See Constants below for
information about how to simulate the Boolean data type in IDL.)
- The return values of functions should always be checked as soon as possible to detect errors.
- If a function returns a numerical value, a return value of zero or below could indicate an error.
If this is not possible the function should be changed to return a Boolean with the value of TRUE or FALSE.
The actual value should be passed out as a "pass by reference" input parameter.
- It is not possible to specify in the function or procedure specification whether parameters are "pass by
value" or "pass by reference". Care must be taken to modify only those parameters intended for modification.
This should be achieved by assigning and using local copies of all non-modifiable parameters. Note that it
must be clearly stated in the function or procedure header which variables are intended to be "pass by
reference".
Note: If it is specified in the function header that a parameter is "pass by value" then a user expects that it is
possible to pass in a variable of any sort and it will not be modified! This has to be ensured by the
programmer of the function.
- Parameters that are designed to change during function or procedure execution, i.e. "pass by reference",
must be placed at the end of the parameter list.
- Specifying a constant value for a variable that is intended to be modified within a function does not
produce a compilation error or a runtime error! (It is more to be considered as a programmers error anyway.)
- It is not possible to force the use of a variable value instead of the reference. The FORTRAN syntax of
enclosing the variable in parenthesis to use the value is not supported. It is not possible to use a construct
like "Float(aFloat)” to force use of value either, the variable (aFloat) will still be changed by the called
function. How this works is unknown since what should be passed to the function is the result of
"Float(aFloat)” and nothing else! (Note: "Fix(aFloat)” works correctly though. I.e. it works when the type is
changed. Not very practical though.
- It is allowable to call a IDL function or procedure with less than the specified parameters. Therefore
all functions and procedures must check for correct number of parameters passed.
Warning: This will not produce a compilation error. If the missing parameter(s) are assigned a value before
used as a input value to something else, it is not even detected during runtime!
- If keyword type parameters are used, they should always be checked at the start of the function or
procedure. If they are not specified, a default value should be assigned.
- A function or procedure may be enhanced through added functionality or additional parameters. These
parameters could be defined as keyword parameters to avoid modification to any existing code that uses the
function or procedure. Code that uses the function or procedure must be investigated thoroughly.
Note: This might sound like a harsh rule that all procedures must be turned into functions that return
their result status. But our experience is that it is very rare that there is not something that might go wrong
within a function. And if it goes wrong, the caller of the function must be able to find out. In a recent project
of medium size (but high complexity) there was 2 (two!) out of ~350 procedures that we let be as procedures.
The rest were functions.
Variables
Variables must conform to the follow standards:
- Variables must be declared before they are used.
- Variables should be initialised at declaration to values indicating an error condition.
- Variables must be initialised before they are used (Every path through the code must be considered.)
- Unused variables should not be declared. (Every path through the code must be considered.)
- Variables should not be assigned values if they are not to be used.
- Since dynamic typing is a part of the IDL language, it can not be assumed that a variable is of a certain type.
If a variable must be of a certain type for a operation, the type of the variable must be checked.
Unfortunately no built in IDL functionality exists for this purpose, but it is possible to construct a user defined function.
- Care must be taken to ensure that the variable type is not unintentionally changed during program execution. All dynamic
variable binding must be documented.
- Arrays with different sizes must not be assigned to each other.
- "Relaxed Structure Assignment" and similar procedures should not be used.
Note: Variables should not be declared until they are actually needed. The typical Fortran or Standard Pascal
way of declaring all used variables at the top of the function is considered poor programming style. Don't declare them until
you need them!
Due to a limitation in the IDL language it is not possible to define constant variables (i.e. variables that retain
the same value throughout the program execution) except as system variables with the command:
DefSysV, Name, Value, Read_Only, where Read_Only should be set to 1.
The disadvantages of IDL system variables are:
- Scope is always global, no exceptions possible.
- They must begin with an exclamation mark, "!".
Nevertheless the use of constant system variables are in certain cases very useful and the following should always
be declared:
!TRUE
Variable of type Integer that should be used as a return value from functions. Necessary since no built in Boolean
type exists within IDL.
Set by the command:
DefSysV, ‘!TRUE’, 1, 1
!FALSE
Variable of type Integer that should be used as a return value from functions. Necessary since no built in Boolean
type exists within IDL.
Set by the command:
DefSysV, ‘!FALSE’, 0, 1
It is also recommended to define the following:
!DEBUG
Variable used to execute conditional code for debugging purposes.
Set by the command:
DefSysV, ‘!DEBUG’, 1, 0
Example:
If (!DEBUG) Then Begin
Print, "Variable fVariable = ” + String( fVariable )
EndIf
Note that the !DEBUG flag is not set to read only since typically this is handled by a pair of functions:
SetDebugModeOn & SetDebugModeOff, to be able to toggle the state at runtime.
Type Definitions
Type definitions, i.e. user defined types, are not available in IDL. Use classes.
Classes
- For basic information on class design in IDL, please refer to the IDL documentation. In addition we
have established the following standard practices:
- Any memory allocated within a class should be free'd within the same class.
- All classes should have a test program names <ClassName>__Test, located in the same
source file. The test program should test all functionality of the class and must be updated and run
whenever the class is modified. The program must return !TRUE or !FALSE depending on the outcome. This
and the correct naming ensures that we can automatically test classes when we build a library.
- Always call the base class constructor from your constructor: self->BaseClass::Init
- Always call the base class destructor from your destructor: self->BaseClass::CleanUp
All classes should have a member procedure named PrintSelf that is to be used for debugging, trace output, etc.
The signature is: Pro <Class>::PrintSelf, File = File, Indent = Indent
An example:
;***********************************************************************
;+
; Name : PrintSelf
; Purpose : Prints the contents of the object.
;
; Name
Type Default
Purpose
; Keywords : File AsciiFile Obj_New()
File to write to
; Indent
String ""
Any indentation
;
;
Type Comment
; Return value :
;
; Comment : Print the contents of the object. For debug or trace.
;-
;***********************************************************************
Pro FiFo::PrintSelf, File = File, Indent = Indent
strArrTxt = ""
strArrTxt = [strArrTxt, "FiFo Stack"]
strArrTxt = [strArrTxt, "----------"]
strArrTxt = [strArrTxt, "No Of Items: " + self.m_objStack->GetCount()]
strArrTxt = [strArrTxt, ""]
If (N_Elements(File) GT 0) Then objFile = File Else objFile = Obj_New()
If (Obj_Valid(objFile)) Then Begin
If (N_Elements(Indent) GT 0) Then strIndent = Indent Else strIndent = ""
strArrTxt = strIndent + strArrTxt
res = objFile->WriteLineArray(strArrTxt)
EndIf Else Begin
For nLoop = 0, N_Elements(strArrTxt) - 1 Do Begin
Print, strArrTxt[nLoop]
EndFor
EndElse
self.m_objStack->PrintSelf, File = File, Indent = Indent
End
Comments:
- It is a procedure and not a function since this belongs to debugging / trace and it is not crucial if it fails.
- Make sure to pass the call on to any member object or base class.
- Argument File is used when the output should be directed to a trace file.
- Indentation only makes sense in a trace file and is ignored when printing to the console.
Loop control
Loops must conform to the follow standards:
- The loop termination must be static i.e. the condition for loop termination should not change during the loop.
- A loop control variable should not change during the loop apart from what is required for normal operation of the loop.
- External entry into a loop structure is not permitted.
- Since no Break statement exists within IDL, simulate the behaviour by defining a variable bBreak to use as loop condition.
Example:
bBreak = !FALSE
While (Not bBreak) Do Begin
DoSomeStuff()
If (Not DoSomeOtherStuff()) Then $
bBreak = !TRUE
EndWhile
Another problem with loop control within IDL can be the problem with dynamic typing. A typical FOR loop looks like this:
For I = 1, 5 Do...
A problem can occur if the literal 5 is replaced by a computed MaxValue that during the calculation may suffer from a
small rounding error so that it ends up as for example 4.9999998. In some case the loop will only execute 4 times and not
5 as intended. Tests on a Digital Alpha workstation show that a value of 4.99999998 will execute the loop five times,
whereas a value of 4.999998 will only generate four loops!
Always check computed upper or lower bounds of loops!
Note: There are other problems with loops in IDL when using Byte or Long index variables. Refer to the IDL documentation
for more information.
Note: It is possible to specify the increment in an IDL FOR loop. If the increment is a computed value it has to be
ensured that the value is not zero (causes an infinite loop).
Control branching
- The use of "GOTO" or similar jump commands is only acceptable for error handling (Exceptions are of course excluded from
this). In some cases, the use of a jump may be the best way to achieve the desired result. If so, the use of a jump commands
must be well documented.
- Excessive use of nested "IF THEN ELSE" should be avoided. In most cases, it is better to use select case statements and
function calls to control branching logic.
- Only one entry point into a function is permitted. Note that the design demand from traditional SA/SD (Structural
Analysis / Structural Design) of only one exit point from a function is not necessary anymore. (It is rather considered as
poor programming since it introduces status flags, extra branching logic etc., all which just makes the program more
difficult to understand.) As soon as an error has ocurred, just exit.
- The exception to the above rule about GOTO's concern the case statement. Here GOTO's may be used to simulate the
standard behaviour of grouping several items together.
Example:
Case str Of
"A": GOTO, FirstFour
"B": GOTO, FirstFour
"C": GOTO, FirstFour
"D": Begin
FirstFour:
; Do stuff here...
End
"E": Begin
; Do other stuff here...
; Etc.
End
EndCase
Global Data
Global data should be used as little as possible. If use is unavoidable, global data must conform to the follow standards:
- Direct access of global data is not allowed. Access functions should be used to read and edit data.
- Global data should preferably be grouped together in the source code.
- Global data should be initialised through one code module only.
- It is preferred to use a global object holding the data and associated functionality. Please see the design pattern
"Singleton”.
The use of global data is not allowed for user interface components.
Example:
A generic widget procedure is used to retrieve data from the user and stores the values in a common block. If more than one
of these generic widgets are used at the same time, there exists no way to determine the value of the common block.
In general the use of common blocks is discouraged within IDL.
Localised Data
Text messages intended for the user should if possible be stored in a string resource file. This will enable easy creation
of localised versions of the software.
Note: This holds true regardless whether you believe that the application will only be used in one location! This is such
an easy modifiction that it should always be performed.
Structures
Structures are a very good way of grouping associated data together and should be used as much as possible if the construct
is allowed in the selected programming language. Example of uses are:
- If a function / procedure needs more than ten input variables, they may be replaced by a structure that contains all
necessary parameters.
- If a function / procedure needs to return a large number of results, instead of using global data or many variables
passed by reference, it would be better to use structures.
NOTE: Structures should only be used for very simple constructs. Any data that has associated behaviour should be
contained in a class.
Recursion
Recursion should be avoided, especially if constructing a safety critical application. If that is not possible or
even recommended due to the nature of the problem (don’t try to traverse a linked list without recursion!), the use of
recursion must be well documented and carefully verified.
And if you do, make sure to include the "FORWARD_FUNCTION" declaration inside your function:
Function RecurseMe
Forward_Function RecurseMe
<Do Stuff>
End
(Of course there are problems when recursion is the most suitable solution for the problem. But if you know that this
is the case, then you as a programmer are probably familiar with the concept and you also know how to properly terminate.
Then by all means use it. But don't say we didn't warn you...)
Memory Allocation
The following general guidelines apply for memory allocation / memory management:
- Never assume success. The pointer or object returned from a memory allocation must always be checked.
Even today, computers do actually run out of memory!
- Free the allocated memory. Whenever including a memory allocation statement in code, directly write the
statement that frees the allocated memory. (Or at least think about where the memory is going to be freed.)
- When allocating memory inside of an object, do use the destructor (::CleanUp function) to free it.
The preferred way to catch memory allocation errors is as follows:
Catch, nErrorStatus
If (nErrorStatus) Then Begin
<Do some error handling. Simple example below:>
Print, "Could not allocate memory!"
Return, !FALSE
EndIf
pArrInput = Ptr_New(Temporary(FltArr(nCols, lRows)))
Catch, /Cancel
Note: Failure to include the last line (Catch, /Cancel) will cause the program to use this error catch for
all subsequent errors. This is not recommended since it makes it impossible to identify individual errors.
Conditional Operator
IDL allows the use of the C style conditional operator. The conditional operator is only shorthand for a
standard IF THEN ELSE and must not be used. It saves a few keystrokes but can make reading of the code much
harder, especially within complex contexts.
Example (the following statements produce identical results):
z = (a GT b) ? a : b ; Legal syntax. Must not be used.
If (a GT b) Then z = a Else z = b ; Preferred usage.
|