We have insisted repeatedly on the importance of organizing any program consisting of more than a few dozen lines into paragraphs, each of which implements a well-defined action in a manner free of close involvement with the details of other paragraphs. We also noted that the SETL's procedure and case constructs aid in achieving this. The sample programs seen so far are organized into a main program and a series of procedures. This suffices for small programs, but above a certain size (say 500 lines) additional means of grouping collections of procedures into relatively independent units become necessary. Such collections are called packages, libraries, and object classes.
In this chapter, we will describe the first two of these extended SETL structuring mechanisms systematically and will illustrate their use. Object classes will be described in Chapter 8.
7.1 Packages
SETL allows groups of procedures developed for use in one or more large applications to be grouped together in text bodies called packages, each of which is arranged in two parts: a package 'header' which simply describes the procedures, constants, and global variables that a package provides, and an associated package body which implements each of the procedures listed in the package header. A package's header announces the gift which it is prepared to make to the outside world, and is ideally all that any one other than the package's autor needs to know about it; the associated package body contains the code which implements this 'gift'. Thus the SETL package mechanism, like that of the many other languages providing similar facilities, supports the basic requirement of information hiding: to make procedures available for use without burdening their users with any of their internal details.
7.1.1 package Headers
The syntax of a package header (or 'specification') is
package <package_name> <variable and constant declarations> <procedure declarations> end [<package_name>]
The Technically, packages are 'compilation modules' at the same level as a programs.
The package header must be compiled before
its associated package body, either as part of a single, muti-dection compilation involving several packages and programs, or in a separate compilation step.
7.1.2 package Bodies
A package body contains data visible throughout the package, but not outside the package, along with
the complete definitions of the procedures in the package. The syntax of a package body is
The <use section> is a sequence of clauses of the form
See 7.1.3 for more details.
The <constant and variable declaration section> defines names which will be visible
within the package, but not outside the package. And finally the <procedure section> is a list of
procedure definitions including all procedures listed in the package header, and possibly others visible
only within the package. An example illustrating all of these components is given below.
7.1.3 Importing or 'using' a Package
To import a package a use clause is placed before the declaration section of a program or package body. The syntax of a use clause is
For example, a program importing the package 'Anything' could be written as
Here is a second, somewhat more realistic example.
7.1.4 Compilation Units and Namescopes
As noted above, programs, package specifications and package bodies are all 'compilation units'. One or more of them
can appear in a source file. package specifications must be compiled before the associated package
bodies or any other units that import the package. When a package specification is compiled, the
associated package body and any units that import the package are invalidated, and will need
recompilation. This is particularly important to keep in mind when working with mutually dependent
packages.
A potential ambiguity occurs when two packages contain a common name, and a program imports
both of those packages. SETL adopted Ada rules to handle that occurrence: duplicate names will hide
each other. However, values bound to such names are still accessible using the construction
The following example shows this:
Since the unqualified use of 'some_proc' refers to a name incnflict between the tow packages used, but the qualified names a.some_proc and b.some_proc are unambiguous, the output produced by this program is
SETL names are local to their enclosing procedures, unless they have been explicitly declared (in a var declaration, a const declaration, or a procedure definition) at the start of a
higher level package, program, or procedure. For example, in
the name 'aaa' is accessible inside the subprocedure since it has been declared global at the program level.
It is possible to access hidden names using the construction <owner>.<name>, assuming <name>
would have been visible if not hidden. This is illustrated by the following small program, which produces the output
Note that the redeclaration of the constant 'a' inside the procedure 'some_proc' hides the different declaration of this same constant at the program level, but that the subprocedure can access the hidden value using the qualified form 'test.a'.
SETL also provides a variant of the classes-objects-methods mechanism characteristic of 'object oriented' languages. SETL's object facilities support multiple inheritance, and
operator overloading for user-defined types. They allow the SETL
programmer to create his own types and to define the normal SETL operations
(+, -, * etc.) on those types. Discussion of this whole set of possiblities is postponed to Chapter 8.
Each of the he two packages that appear in this example use variables and procedures supplied by the other. The small test program shown uses both of them, and its output, which is simply
verifies that all behaves as expected.
Whenever the specifier of a package P is rcompiled, all programs, packages, or classes Q that use P must be recompiled. However, since the body of a package need not be compiled together with its specifier, the body of P can be recompiled without forcing Q to be recompiled, provided that only the body, but not the specifier, of P is recompiled. As already said,separate compilation of package specifiers and bodies is necessary if a group of packages use each other circularly, one must compile all the headers first, and then all the bodies.
The following example illustrates some of these considerations. First we compile a simple package and its specifier, after which it is used immediately in a small program.
The output produced is
Next we recompile the body of test_pak, but not its specifier:
It remains possible to execute program 'test' without recompiling it. But when this is done, the newly compiled version of procedure 'proc' will be loaded and executed, so that the output produced will be
An initial set of bodies, whch can be compiled together with these specifiers, are
In the presence of these packages the small test program
produces the output
Now we can recompile the body of test_pak_1 without having to recompile anything else. Suppose that this becomes:
Re-executing program test; now produces the output
Here is another example of the rules governing name conflicts:
This technique can clearly be used to redirect printed output to alternative files or display windows, to replace the newlines normally added by 'print' with blanks, etc.
package body <package_name>
<use section>
<constant and variable declaration section>
<procedure section>
end [<package_name>]
use <package_name 1> [ , <package_name 2> ] ... ;
package Anything;
var visible_var
const visible_const := 5;
procedure visible_proc(p1,p2);
end Anything;
package body Anything;
var hidden_var;
const hidden_const := 10;
procedure visible_proc(p1,p2);
print("Hello");
end visible_proc;
procedure hidden_proc(p1,p2);
print("Goodbye");
end hidden_proc;
end Anything;
program Something;
use Anything;
visible_proc(1,2);
end Something;
package Stack_Module; -- the 'package' or 'package header'
procedure Push(rw Stack,Item);
procedure Pop(rw Stack);
end Stack_Module;
package body Stack_Module;
procedure Push(rw Stack,Item);
Stack with:= Item;
end Push;
procedure Pop(rw Stack);
if #Stack = O then
return OM; -- the SETL 'undefined' or 'null'
else
return Item from Stack;
end if;
end Pop;
end Stack_Module;
package a; -- a first package
procedure some_proc();
end a;
package body a; -- body of first package
procedure some_proc(); return 10 * "a"; end some_proc;
end a;
package b; -- a second package, also providing a procedure named 'some_proc'
procedure some_proc();
end b;
package body b; -- body of second package
procedure some_proc(); return 10 * "b"; end some_proc;
end b;
program test; -- a program using both packages
use a,b;
print(some_proc); print(a.some_proc()); print(b.some_proc()); -- use of procedures from packages
end test;
<om>
aaaaaaaaaa
bbbbbbbbbb
program my_prog;
var aaa;
bbb := 1; aaa := 2; ccc := my_proc(bbb);
procedure my_proc(b);
bbb := b; return bbb + aaa;
end my_proc;
end my_prog;
program test; -- a program using both packages
const a := 1; -- belongs to 'test'
some_proc(); -- procedure call
procedure some_proc(); -- subprocedure
const a := 2; -- belongs to 'some_proc'
print(a," ",test.a); -- both consts can be accessed using qualification where needed
end some_proc;
end test;
7.1.5 Circular Dependencies among Packages
It is possible for a package 'a' to use a package 'b' at the same time that 'b' uses 'a'. How can this be done consistently with the rule that every package used by 'a' must be compilied before 'a'? The answer is obvious if one realizes that it is only the specifier of a package, not the package body, that must be compiled before it is used. Thus mutually dependent sets of packages can be handled by compiling all the specifiers first, and then all the bodies. The following is an example of this:
package a;
var va;
procedure pa;
end a;
package b;
var vb;
procedure pb;
end b;
package body a;
use b;
procedure pa;
print(vb);
end pa;
end a;
package body b;
use a;
procedure pb;
pa();
end pb;
end b;
program test; -- over-writing the system 'print' routine
use a,b; -- save the original procedure, so that console output remains possible
vb := 999999; -- set package b variable
pb; -- call package b procedure, which invokes package a procedure to print vb
end test;
7.1.6 Separate Compilation of Packages and their Headers
The specifier of a package P must be compiled and must be available somewhere in the active library list (see below) before any other program, package, or class Q that uses P is compiled. This makes all P's externally visible procedure, variables, and constant names available during the compilation of Q, allowing spaces for the storage of the corresponding values and procedure values to be allocated and named. When a package body is compiled, the procedures actually present in it are checked for agreement with the procedures declared in its specifier, and an eror message is generated if any discrepancy is found. It is also verified that all procedures declared in the specifier are actually present in the body.
package test_pak;
procedure proc(param);
end test_pak;
package body test_pak;
procedure proc(param);
print("version 1: ",param);
end proc;
end test_pak;
program test;
use test_pak;
proc("param");
end test;
version 1: param
package body test_pak;
procedure proc(param);
print("version 2: ",param);
end proc;
end test_pak;
version 2: param
.
The following is another example of a set of packages which use each other, so that each has access to the public procedures of the other. The specifiers, which must be compiled together, are
package test_pak_1;
procedure proc_1(param);
end test_pak_1;
package test_pak_2;
procedure proc_2(param);
end test_pak_2;
package body test_pak_1;
use test_pak_2;
procedure proc_1(param);
print(param);
if #param >0 then proc_2(param(2..)); end if;
end proc_1;
end test_pak_1;
package body test_pak_2;
use test_pak_1;
procedure proc_2(param);
print(param);
if #param >0 then proc_1(param(2..)); end if;
end proc_2;
end test_pak_2;
program test;
use test_pak_1;
proc_1("param");
end test;
param
aram
ram
am
m
package body test_pak_1;
use test_pak_2;
procedure proc_1(param);
print("New Version: ",param);
if #param >0 then proc_2(param(2..)); end if;
end proc_1;
end test_pak_1;
New Version: param
aram
New Version: ram
am
New Version: m
package test_pak_1;
var test_var := "test_var in test_pak_1";
procedure test_proc();
end test_pak_1;
package body test_pak_1;
procedure test_proc();
print("test_proc in test_pak_1");
end test_proc;
end test_pak_1;
package test_pak_2;
var test_var := "test_var in test_pak_2";
procedure test_proc();
end test_pak_2;
package body test_pak_2;
procedure test_proc();
print("test_proc in test_pak_2");
end test_proc;
end test_pak_2;
program test; -- uses both packages
use test_pak_1,test_pak_2;
print("test_var: ",test_var);
print("test_var: ",test_pak_1.test_var);
print("test_var: ",test_pak_2.test_var);
test_pak_1.test_proc();
test_pak_2.test_proc();
test_proc();
end test;
At thispoint, you should be able to predict the resulting output.
7.1.7 Re-defining SETL's Built-In Procedures
SETL provides many built-in procedures. Those without write parameters may be used as first class
objects. They are all declared as part of a default scope, so the names may be hidden by any local
declarations of variables with the same name. The following example illustrates this fact. In it, the built-in 'print' function is first saved (under the name 'printit'),and then 'print' is redfined (in the subprocedure 'sub_proc') and invoked. To accomplish its output, the new 'print' invokes the built-in 'print' function as 'printit'. The output produced by the call print(99) to the redefined 'print' function is
program test; -- over-writing the system 'print' routine
const printit := print; -- save the original procedure, so that console output remains possible
sub_proc(); -- subprocedure call
procedure sub_proc(); -- sub-procedure redefines 'print'
print(99); -- invocation of redefined 'print'
procedure print(x); test.printit("I have printed: ",5 * (str(x) + " ")); end print; -- redefinition of 'print'
end sub_proc;
end test;
7.1.8 Some Examples
Packages often begin as procedures in ordinary programs which one considers to be useful enough that one wants to package them for repeated use, and perhaps generalize them also. For example, the generally useful binary search procedure of Section 5.4.4
can be wrapped in the following simple package.