From: dnsurber@lescsse.jsc.nasa.gov (Douglas N. Surber)
Subject: Re: Controlling Type Definition - Is it useful/necessary?
Organization: Lockheed Engineering and Sciences
Distribution: comp.ada
Date: 14 Sep 92 14:00:11 GMT


In order to discuss how to properly control type definitions, you must
consider what purpose types serve. Obviously they are used to define
storage. All that is required to accomplish that is to say "this thing
takes up this many bits". Second, they allow you to specify certain
types of built in functionality, such as indexing or floating point
multiply. This is pretty well covered by integers, floats, chars,
pointers, arrays and records. Not much sophistication required. Finally
they are used to tell the compiler and the maintainer something about
the program. Does the value of one object relate (more or less) directly
to the value of another object _with_ _in_ _the_ _logical_ _space_ _of_
_this_ _program_.  If so they should have the same type, if not, not.
This is where the full power of Ada's type system is needed.

Like everything else, type definitions can be described to exist along a
continuum between two extremes--those types specified by the interface
to a package and those and those that exist solely to describe/support
the specific implementation of the computation the package performs.
The types of objects that directly relate to the interface, such as the
types of parameters, storage of intermediate results, etc should be
tightly controlled, as you described. Clearly the value of these objects
are closely related.  But in a package of any complexity there may be
other objects which, while influencing the result, are not as closely
related to the result.  The types of these object should be chosen to
convey as much information about the computation as possible.
Arbitrarily restricting the programmer's ability to define new types of
this kind will reduce the quality of the code. It takes away one of
Ada's biggest strengths.

In the first paragraph I said that types are used to tell the compiler
about the program. I feel I ought to clarify that statement. Testing of
programs is *HARD*. Compilation is easy.  The compiler can verify
certain correctness properties of a program using the information in the
types.  Each correctness property of a program that is expressed in the
types is one that you don't have to worry about in testing. (Sure you
want to test for it anyway, but that's just reasonable paranoia.)  If I
define a new type "Foo_Array_Index" and use it as the index type for an
array, then I _know_ that I haven't mistakenly used "Bar_Array_Index" to
access "Foo_Array".  This eliminates a whole class of errors.  This can
be taken to extreemes--you can define a new type for everything. Of
course this doesn't tell the compiler anything since every time two
objects are "combined" you have to do type casting.  Ultimately this is
a judgement call.  The extreemes are obvious, but stuff gets muddy in
the middle.

Guidelines for creating new types are certainly in order. But in my
opinion there are no simple syntatic rules. This is one of those skills
that distinguishes good programmers from poor ones--the ability to write a
program that is not only correct and efficient but readable, testable,
and robust in the face of changes.  Good use of the type system improves
those properties.

I am presently working with a group to improve the speed of a rather
large package. The profiler tells us that 10-12% of the cycles are spent
TRUPping (Trim Right, UPper case) strings. The interface has a number of
parameters of type "String", and the internals of the package require
that those string parameters be TRUPped. The implementation TRUPs just
about every use of every string, so of course most of those strings have
already been TRUPped multiple times.  Obviously the thing to do is to
just TRUP things once and then forget about it.  But this is a large
package with several people working on it and unknown future generation
having to maintain it.  Our solution is to derive a new type,
"Trupped_String", from an existing type "Pascal_String", and write a
procedure, "Trup_String (arg : in String) return Trupped_String", that
trims and upper cases its argument.  Then we convert args at the
interface and don't worry much after that.  Because this is a new type,
the compiler can find certain kinds of errors for us, and for future
maintainers.  Additionally, maintainers will be constantly reminded that
these strings need to be TRUPped.

As far as specific guidelines go, I can think of the following:

    Use the interface types unless there are good reasons not to.
    There often are good reasons not to, but be sure you have thought
    about them before creating a new type.

    Don't create a new type from thin air if you can reasonablly derive
    it from an existing type.  This will help with precision problems.

    If you are type casting two types back and forth all over the place,
    consider making them the same type.  However, if there is a clear
    conceptual transformation that occurs with the type casting, consider
    writing a function that captures that transformation and using it
    instead of the bare type coersion.

    If two objects are never "combined" (transitively) then they should
    be of different types.  If they aren't the same then don't say they
    are the same by making them the same type.

This last is especially important when it comes to array indices.  I
think every array should have its own distinct index type so there is no
problem with getting indices mixed up.

This has all been about creating new types. Subtypes are less of a
problem. I use them every where I can to specify range constraints.
This is a help with testing, but since you can mix and match subtypes to
your hearts desire, they don't give you much help at compile time.

This has gone on rather longer than I intended, I hope it helps. 
Sorry I had to post it, but I don't have email access. :(

Douglas Surber
Lockheed
Houston, TX