PROGRAM COMPOSITION |
We can distinguish four levels of programme hierarchy:
Two other possible ingredients, which should be discussed here, are Generics and Abstract Data Types (ADTs).
1.1 Block Structured Languages
|
![]() Block structure |
One of the most important issues that has to be addressed in the design of a programming language is the support it gives to the control of complexity. An important technique is the division of a program into "chunks", called routines or sub-programs, that will allow the programmer to think at a more abstract level. At its simplest a routine can be equivalent to a block. More specifically a routine can be defined as a collection of declarations and statements that must be invoked explicitly (routine invocation). Note that after invocation control is returned to the point immediately after the invocation. An additional advantage is that routines can be stored in libraries for reuse.
Further Issues involved include:
These will be discussed later. It should also be noted that although the concepts remain the same routines or subprograns are referred to by different names in different languages. In C routines are referred to as functions. In Pascal and Ada they are called procedures and functions. Modula-2 names them procedures (even if some of them are actually functions). COBOL uses the terms paragraphs, while FORTRAN and BASIC refer to them as sub-routines and functions.
We can identify two types of routine:
The distinction between a procedure and a function is that a function returns a value while a procedure does not. A function declaration must therefore be called as part of an assignment expression and incorporate a definition for its return value. Some imperative languages (Ada, FORTRAN, Pascal) make a clear distinction between procedures and functions. Other imperative languages (C, ALGOL 68) do not.
A procedure (function) comprises a head and a body. In the head the name of the routine and its are specified. In the body the necessary code is given.
Functions and procedures are activated by a procedure or function call which names the routine and supplies the actual parameters. Most imperative languages (Ada and C included) use positional parameters. Examples:
AVERAGE:= MEAN_VALUE(4.2, 9.6); (Ada)
average = meanValue(4.2, 9.6); (C)
Alternatives include keyword or default parameters.
with CS_IO ; use CS_IO ; procedure PROC_EXAMPLE is NUM_X, NUM_Y: float; procedure MEAN_VALUE (VALUE_1, VALUE_2: float) is begin put("MEAN VALUE PROCEDURE"); new_line; put("===================="); new_line; put("The mean is "); put((VALUE_1+VALUE_2)/2.0); new_line; end MEAN_VALUE; begin get(NUM_X); get(NUM_Y); MEAN_VALUE(NUM_X,NUM_Y); end PROC_EXAMPLE; |
with CS_IO ; use CS_IO ; procedure FUNC_EXAMPLE is NUM_X, NUM_Y, NUM_Z: float; function MEAN_VALUE (VALUE_1, VALUE_2: float) return float is begin put("MEAN VALUE FUNCTION"); new_line; put("===================="); new_line; return (VALUE_1+VALUE_2)/2.0; end MEAN_VALUE; begin get(NUM_X); get(NUM_Y); Z:= MEAN_VALUE(NUM_X,NUM_Y); put("The mean is "); put((NUM_X+NUM_Y)/2.0); new_line; put("The mean is "); put(Z); new_line; end FUNC_EXAMPLE; |
#include <stdio.h> void meanValue(float, float); void main(void) { float num_x, num_y; scanf("%f%f",&num_x,&num_y); meanValue(num_x,num_y); } void meanValue(float value1, float value2) { printf("MEAN VALUE PROCEDURE\n"); printf("====================\n"); printf("\nThe mean is %f\n",(value1+value2)/2.0); } |
#include <stdio.h> float meanValue(float, float); void main(void) { float num_x, num_y; printf("Enter 2 real numbers "); scanf("%f%f",&num_x,&num_y); printf("MEAN VALUE FUNCTION\n"); printf("===================\n"); printf("\nThe mean is %f\n",meanValue(num_x,num_y)); } float meanValue(float value1, float value2) { return((value1+value2)/2.0); } |
In block structured languages the order in which procedures are declared also effects visibility. Ada (and other block structured languages such as Pascal) requires that any use of an identifier must be preceded by its declaration. Thus if two procedures are declared at the same level, their order of declaration will dictate "who can call whom". The procedure currently being declared cannot see any following procedures. In Ada this can be overcome by using an incomplete declaration. Ordering of procedure declarations is not significant in C.
Note also that in languages such as Ada and Pascal a following procedure cannot see a procedure or function nested within a previous procedure or function. Consider the following piece of code (nesting given to right):
with CS_IO; use CS_IO; procedure SUM_AND_PROD is X_ITEM : constant := 2; Y_ITEM : constant := X_ITEM*2; -------------------------------------------------- procedure SUM(P_ITEM, Q_ITEM : INTEGER) is TOTAL : INTEGER ; ------------------------------------------------ procedure SUMMATION(R_ITEM, S_ITEM : INTEGER) is begin TOTAL := R_ITEM + S_ITEM; PUT(TOTAL); NEW_LINE; end SUMMATION; ------------------------------------------------ begin SUMMATION(P_ITEM, Q_ITEM); end SUM; -------------------------------------------------- procedure PRODUCT(P_ITEM, Q_ITEM : INTEGER) is begin PUT(P_ITEM * Q_ITEM); NEW_LINE; SUM(P_ITEM, Q_ITEM); end PRODUCT; -------------------------------------------------- begin PRODUCT(X_ITEM,Y_ITEM); end SUM_AND_PROD; |
Here the procedure PRODUCT can see the procedure SUM because it is at the same level and declared before it. The procedure PRODUCT cannot see SUMMATION because it is nested within the procedure SUM. Conversely the procedure SUM cannot see the procedure PRODUCT because it is declared after it (despite being at the same level), however it can see the procedure SUMMATION because it is nested within it.
Scope rules govern the visibility of data items (i.e. the parts of a program where they can be used). They also bind names to types. Where this is determined at compile time this is called static scoping. The opposite is dynamic scoping. Dynamic scoping is generally a feature of logic languages such as PROLOG and functional languages such as LISP. The advantages of static scoping is that it allows type checking to be carried out at compile time. Most imperative languages use static scoping.
Generally speaking, in imperative languages, the scope of a declaration commences at the end of the declaration statement and continues to the end of the block.
For example in the following procedure the constants X_ITEM and Y_ITEM are visible from when they are declared till the end of the procedure. This is why X_ITEM can be used in the declaration of Y_ITEM (we could not do the reverse, or at least not without first reordering the declarations).
procedure SUM_AND_PROD is X_ITEM : constant := 2; Y_ITEM : constant := X_ITEM*2; begin PUT(X_ITEM+Y_ITEM); NEW_LINE; PUT(X_ITEM*Y_ITEM); NEW_LINE; end SUM_AND_PROD; |
Consider the following example program (Nesting illustrated in diagram to right):
with CS_IO; use CI_IO; procedure SUM_AND_PROD is X_ITEM : constant := 2; Y_ITEM : constant := X_ITEM*2; ---------------------------------------------------- procedure SUM(P_ITEM, Q_ITEM : INTEGER) is TOTAL : INTEGER ; begin TOTAL := P_ITEM + Q_ITEM; PUT(TOTAL); NEW_LINE; end SUM; ---------------------------------------------------- procedure PRODUCT(S_ITEM, T_ITEM : INTEGER) is PROD : INTEGER ; begin PROD := S_ITEM * T_ITEM; PUT(PROD); NEW_LINE; end PRODUCT; ---------------------------------------------------- begin SUM(X_ITEM,Y_ITEM); PRODUCT(X_ITEM,Y_ITEM); end SUM_AND_PROD; |
The data items P_ITEM, Q_ITEM and TOTAL are local to the SUM procedure (block), while data items S_ITEM, T_ITEM and PROD are local to the PRODUCT procedure (block).
Thus in the following example program (nesting given ti the right) the data item ANSWER is visible to the two enclosed blocks (SUM and product).
with CS_IO; use CI_IO; procedure SUM_AND_PROD is X_ITEM : constant := 2; Y_ITEM : constant := X_ITEM*2; ---------------------------------------------------- procedure SUM_AND_PRODUCT(P_ITEM, Q_ITEM: INTEGER) is ANSWER : INTEGER; -------------------------------------------- procedure SUM(A_ITEM,B_ITEM: INTEGER) is begin ANSWER := A_ITEM + B_ITEM; end SUM; -------------------------------------------- procedure PRODUCT(C_ITEM,D_ITEM: INTEGER) is begin ANSWER := C_ITEM * D_ITEM; end PRODUCT; -------------------------------------------- begin SUM(P_ITEM,Q_ITEM); PUT(ANSWER); NEW_LINE; PRODUCT(P_ITEM,Q_ITEM); PUT(ANSWER); NEW_LINE; end SUM_AND_PRODUCT; ---------------------------------------------------- begin SUM_AND_PRODUCT(X_ITEM,Y_ITEM); end SUM_AND_PROD; |
Global data items (items which are required to be visible from anywhere in a program) must be declared within the outermost block, i.e. level 1 or the global level. Note that where ever possible use of global data items should be avoided as the values associated with these items can easily be altered erroneously.
In this case the declared items have nothing to do with one another (other than sharing the same name). For example:
with CS_IO; use CI_IO; procedure SUM_AND_PROD is X_ITEM : constant := 2; Y_ITEM : constant := X_ITEM*2; ---------------------------------------------------- procedure SUM(P_ITEM, Q_ITEM : INTEGER) is TOTAL : INTEGER; begin TOTAL := P_ITEM + Q_ITEM; PUT(TOTAL); NEW_LINE; end SUM; ---------------------------------------------------- procedure PRODUCT(P_ITEM, Q_ITEM : INTEGER) is TOTAL : INTEGER; begin TOTAL := P_ITEM * Q_ITEM; PUT(TOTAL); NEW_LINE; end PRODUCT; ---------------------------------------------------- begin SUM(X_ITEM,Y_ITEM); PRODUCT(X_ITEM,Y_ITEM); end SUM_AND_PROD; |
Here the repeated declarations of the data items TOTAL P_ITEM and Q_ITEM define data items that are completely independent (despite the shared name).
This is referred to as occlusion. The identifier which is hidden because of the inner declaration is said to be occluded. This is the case in the following example:
with CS_IO; use CI_IO; procedure SUM_AND_PROD is X_ITEM : constant := 2; Y_ITEM : constant := X_ITEM*2; ---------------------------------------------------- procedure SUM(P_ITEM, Q_ITEM : INTEGER) is X_ITEM : INTEGER; begin X_ITEM := P_ITEM + Q_ITEM; PUT(X_ITEM); NEW_LINE; end SUM; ---------------------------------------------------- procedure PRODUCT(X_ITEM, Y_ITEM : INTEGER) is PROD : INTEGER; begin PROD := X_ITEM * Y_ITEM; PUT(PROD); NEW_LINE; end PRODUCT; ---------------------------------------------------- begin SUM(X_ITEM,Y_ITEM); PRODUCT(X_ITEM,Y_ITEM); end SUM_AND_PROD; |
Here the global data item, X_ITEM is occluded in the SUM and PRODUCT procedures. The data item Y_ITEM is occluded in the PRODUCT procedure in a similar way.
Interactive Ada nesting diagram.
The scope of a data item in C is governed by its storage class. Generally data items belong to one of two storage classes:
C also supports two other storage classes static and register, but these will not be discussed further here.
With reference to the given C program:
GLOBAL PARAMETERS
main
proc2
proc3
|
![]() Interactive C structure diagram. |
proc4)
A number of related declarations of types, variables and routines can be grouped together into a module, also sometimes referred to as a package (Ada) or task. The concept of modules (and modular programming) appeared in 1970's in response to the increasing size of programs. Modules comprise a specification part and a body part. The specification part contains a description of the interface and the body the code that implements the interface. The specification part is accessible to the user, the implementation part is "hidden" (information hiding).
Ada packages comprise two parts:
Note that the body is not visible to the user.
Creating the package:
package TIMES_TWO_PKG is -- SPECIFICATION function TIMES_TWO ( NUMBER : integer ) return integer; end TIMES_TWO_PKG ; -- BODY package body TIMES_TWO_PKG is function TIMES_TWO ( NUMBER : integer ) return integer is begin return(NUMBER*2); end ; end TIMES_TWO_PKG; |
Incorporating the package into another program:
with CS_IO, TIMES_TWO_PKG; use CS_IO ; procedure EXAMPLE is VALUE: integer:= 4; DOUBLE: integer:= 0 ; begin DOUBLE := TINESTWO.TIMES_TWO(VALUE); put("VALUE = "); put(VALUE); new_line; put("DOUBLE = "); put(DOUBLE); new_line; end EXAMPLE; |
Compiling:
ICC timestwo.ada
Creating the module:
/* SPECIFICATION */ int timesTwo(int); /* BODY */ int timesTwo(int value) { return(value*2); }
Incorporating the module into another program:
#include <stdio.h> void main(void) { int value=4; printf("Two times %d = %d\(n",value,timesTwo(value)); } |
Compiling:
cc -Aa -c timestwo.c cc -Aa -c example.c cc -Aa -o example example.o timestwo.o
Whereas a C module is first compiled to create an object file (a .o file) and later linked into the main program, a header file is compiled at the same time as the "main" program. The sequence of activities is as follows - first create a .h file:
int timesTwo(int); int timesTwo(int x) { return(x*2); } |
Second, incorporate the .h file into a program:
#include <stdio.h> #include "timestwo.h" void main(void) { int x=4; printf("Two times %d = %d\n", x,timesTwo(x)); } |
Ada assumes that there exists a method outside the language which allows the programmer to specify the name of a procedure and which will then construct an executable binary program for that procedure containing all the necessary modules.
C assumes that the programmer will specify all necessary user object files in a specialised invocation of the system linker. This invocation searches the object files for a routine with the fixed external name main and constructs an executable binary program which, when called, will execute the routine main.
Consider the following Ada procedure:
procedure SWAP (VAL_1, VAL_2 : in out INTEGER) is TEMP : INTEGER ; begin TEMP := VAL_1; VAL_1 := Val_2; VAL_2 := TEMP; end SWAP; |
Generic "swap" procedure:
-- Specification of generic parameters generic type ELEMENT is private; procedure SWAP (VAL_1, VAL_2 : in out ELEMENT); -- Body procedure SWAP (VAL_1, VAL_2 : in out ELEMENT) is TEMP : ELEMENT; begin TEMP := VAL_1; VAL_1 := Val_2; VAL_2 := TEMP; end SWAP; |
The above must now be compiled before it can be used.
Usage:
with SWAP, CS_IO; use CS_IO; procedure SWAP_THINGS is procedure SWAP_INTEGER is new SWAP(INTEGER); procedure SWAP_FLOAT is new SWAP(FLOAT); procedure SWAP_CHARACTER is new SWAP(CHARACTER); INT_1, INT_2 : INTEGER; FLOAT_1, FLOAT_2 : FLOAT; CHAR_1, CHAR_2 : CHARACTER; begin -- Swap integer INT_1 := 2; INT_2 := 4; PUT(INT_1); PUT(INT_2); NEW_LINE; SWAP_INTEGER(INT_1,INT_2); PUT(INT_1); PUT(INT_2); NEW_LINE; -- Swap float FLOAT_1 := 2.22; FLOAT_2 := 4.44; PUT(FLOAT_1, FORE=>2, AFT=>2, EXP=>0); PUT(FLOAT_2, FORE=>2, AFT=>2, EXP=>0); NEW_LINE; SWAP_FLOAT(FLOAT_1,FLOAT_2); PUT(FLOAT_1, FORE=>2, AFT=>2, EXP=>0); PUT(FLOAT_2, FORE=>2, AFT=>2, EXP=>0); NEW_LINE; -- Swap character CHAR_1 := 'a'; CHAR_2 := 'z'; PUT(CHAR_1); PUT(" "); PUT(CHAR_2); NEW_LINE; SWAP_CHARACTER(CHAR_1,CHAR_2); PUT(CHAR_1); PUT(" "); PUT(CHAR_2); NEW_LINE; end SWAP_THINGS; |
Return to imperative home page or continue.
Created and maintained by Frans Coenen. Last updated 03 July 2001