Ada Home
June 16, 1997

Article

Ada Can Do It!

© by Samuel Mize, Team Ada
copyright notice at end of text
smize@imagin.net

Common false claims about things Ada won't let you do,
and how to do them
(and why some are not directly supported)

We sometimes hear "You can't do X in Ada."

In fact, you can do X in Ada, C, Fortran, etc. It may be easy or difficult, it may require more or less programmer work, but they can all compute the same results.

What the complainer means is that "Ada does not support doing X easily."

In some cases this is intentional. In some case it is fixed by the upgrade from Ada 83 to Ada 95. In many cases it is simply false.

Where a capability was intentionally left out, other ways are provided to accomplish the task.

As a rule, Ada avoids easy-but-dangerous approaches, preferring safer approaches that require more programmer effort. Ada also tends toward including a lot of information that makes code easier to read, and makes errors easier to detect, even if that makes the code harder to write.

Here are some common can't-do claims, and how to do them in Ada:

  1. Loose typing
  2. C-style "union" types
  3. Memory overlays
  4. Address arithmetic
  5. Bit manipulations
  6. Conditional compilation
  7. Macros and templates
  8. Functions that can change their parameters
  9. Assignment and increment operators (like += and ++ in C)
  10. Value-returning assignment
  11. "static" variables
  12. Direct multiple inheritance in object-oriented programming
  13. Variable-length argument lists
  14. Rapid prototyping

Two appendices containing extended example code are linked after the descriptions.

1. Loose typing

Ada can be as "quick and dirty" as C if one uses only the predefined types (character, string, integer, float), ignoring Ada's strong typing. (Just because you CAN define a new type for each variable, that doesn't mean that you HAVE to, or that you SHOULD!)

Experience and training are important with Ada. Naive programmers may over-specify types and create a lot of programmer work with little benefit.

On the other hand, getting significant code actually WORKING can be much faster with Ada, since Ada catches a lot of simple mistakes. This is especially true for "rapid prototyping," since such a prototype is, by definition, not being carefully designed and coded.

Lisp and its derivatives provide data items ("atoms") that can be treated as string, character, integer, float or whatever. This is often promoted for rapid prototyping. Such a data type can be built in Ada, with a variant record or a tagged type; once this has been done, the type exists to support other prototyping efforts. Several such types have been defined for local projects. However, most Ada vendors have targeted the high-efficiency area, rather than rapid prototyping.

2. C-style "union" types

These are used for different reasons, each with a different solution in Ada.

If you just want a record type with varying fields, based on a "key" field, use a variant record.

If you want to define a base type, then extend it with new fields, but still pass it to the base type's subprograms, use an Ada 95 tagged type. In Ada 83, see the following.

If you must be able to manually alter the "key" field -- if, for instance, you are interfacing to a C subsystem that uses "union" types -- you can overlay several variables, each of a type representing one of the "union" alternatives.

Or, you could define the data item as a packed array of bytes (or bits) and access fields via unchecked_conversion. If you are defining a "union" type that will be used frequently, you can provide access functions and procedures that automatically select the right sub-array and convert it. After inlining and optimization, these should come close to the efficiency of a record field access. This is a "heavy" solution compared to C, but encapsulates a lot of valuable information for later programmers and for the optimizing passes of the compiler.

3. Memory overlays

On any machine where memory overlays make sense, Ada compilers support them, and always have. This was compiler-dependent in Ada 83, and is better standardized in Ada 95.

However, since there are machines for which a memory overlay is meaningless (like the Symbolics Lisp Machine), the standard does not require that a compiler support them. Some people have been confused by this. The Ada standard does not require overlay support, but it does not require absence of overlay support.

The Ada 83 standard says that putting one item at the same address as another is "erroneous." This doesn't mean that it's a mistake. It's a formal term that means the program's behavior is outside the scope of the standard. It could work as expected, it could silently fail, it could cause the Earth to crash into the Sun. The standard is silent. (In the real world, almost all compilers have supported overlays, and almost none crash the Earth into the Sun.)

In Ada 95, the mechanism for overlays is more standardized. If you specify that a variable is "volatile" and "aliased" it should reside in memory and provide a valid address; you can put another "volatile" item at that address, on any architecture where memory overlays make sense. You should check with your vendor to determine exactly what your compiler requires.

Overlays should often be replaced with use of Unchecked_Conversion. Some people avoid this, to avoid procedure-call overhead and copying the data. Whether this actually happens depends on the exact code and the compiler's optimizations. If your resources are tightly constrained, you should check with your vendor and/or test the compiler's output to determine the most efficient way to use a particular compiler.

If overlays are a requirement for your application, you need to choose your compiler carefully. In practice, Ada compilers have always supported this on architectures where it made sense.

4. Address arithmetic

The most common use of pointer arithmetic is to simulate variable-length arrays, which are directly supported in Ada 83 and Ada 95.

Ada 95 directly supports pointer arithmetic. You instantiate a predefined package to use it. Ada 83 support for this is compiler-dependent.

Some C programmers use pointer arithmetic to manipulate array elements because they feel it is faster than using array subscripts. With modern compilers, this is generally false, even in C. Optimization of array accesses is a well-defined compiler technology, and a compiler is much more thorough about it than a human programmer.

5. Bit manipulations

In Ada 83, this required use of packed bit arrays and unchecked conversion between the array type and integer. (This still works, and works for items other than integers.)

Integer bit manipulation is supported directly in Ada 95, with "modular" integer types.

6. Conditional compilation

Ada's design philosophy is that the code you see is the code that runs.

A lot of debugging and maintenance effort has been wasted through mistakes about whether or not given source code was used in the last compilation.

For major code replacements, like platform-specific interface code, it is better to isolate the varying code into a package, and have different bodies for that package. One approach to this is to have several platform-specific packages with different names (e.g., Dos_Services, Unix_Services) and use a library-level renaming declaration to select the appropriate package:

    with Dos_Services;
    package Services renames Dos_Services; -- as noted by J-P. Rosen

For small replacements, like debug/print statements, you can embed them in normal "if" statements. The efficiency loss is usually indetectable. Some compilers will remove "dead" code, so with "if" statements based on static values, there may be no efficiency loss at all.

In the very few cases where a preprocessor is the best engineering choice for managing code changes, it can be used to generate a read-only source file for compilation from a conditionally-encoded source file. You must take care to make changes only in the conditionally-encoded file. Some vendors automate this. For example, the Rational Apex compiler generates read-only Ada files from preprocessor input files. When a preprocessor file changes, it regenerates and recompiles the Ada file.

7. Macros and templates (see also "conditional compilation")

Ada provides ways to do the tasks commonly approached with preprocessor macros and templates.

The Ada generic provides a more structured way to define reusable code structures that work for many variables, subprograms or types. It is a type-safe "template" facility.

One common use of macros in C is to avoid procedure-call overhead. For this, Ada encourages optimizing compilation and procedure inlining.

In C, three "legitimate" uses of macros are for defining compile-time constants, types, and inline procedures. Ada has all three of these facilities, without macros.

For generating different versions of a unit -- for example, for platform-specific code in a run-time system -- the Ada approach is to create a package that encapsulates the platform-specific features, and have different platform-specific bodies for that unit.

General text-substitution macros, like those in the C preprocessor, are considered unsafe. For example, a macro can refer to a variable X; depending where the macro is expanded X may be one thing, or another, or may not even be visible. Also, debugging is harder when the code compiled is a complex function of the code text.

8. Functions that can change their parameters

This was intentionally excluded, but the same effect can be had in Ada 95 with access parameters. Note that this exactly mirrors the C approach, where you pass the address of a variable to modify it.

9. Assignment and increment operators (like += and ++ in C)

Ada provides these computations, of course, but not as special operators.

Their exclusion is a judgement call. In some cases, they can make individual statements simpler to read. Overall, most code is clearer and more reliable without them. It's easy to misread "x += expression" as "x := expression" instead of "x = x + expression", especially if this line is buried in a sequence of assignments with varying assignment operators.

Further, typos are less likely to result in valid (but erroneous) code. Note that "+" and "=" are on the same key on most keyboards, and "-" is next to it. "+=" and "-=" are easy fumbles for "=", or for each other.

John Volan has proposed a generic package that provides such procedures as Increment, Decrement, and Multiply for a given integer type. See the code in appendix A1.

10. Value-returning assignment

This was intentionally excluded. You have to code around it. Doing so is generally simple, and makes the code clearer for maintenance. Assignments buried in expressions are a significant source of bugs and maintenance errors in C.

11. "static" variables

Variables in Ada are lexically scoped; that is, they are created and destroyed according to their location in the program text.

A "static" variable in a C file, outside any function, exists from program startup to termination. To get this in Ada, declare the variable (or constant) in a library package. Items declared in the package's specification persist, and are visible in other units that "with" that package. (If declared in the "private" section, they are only visible in the private part of "child" units of this package.) Those declared in the package's body are only visible inside the package.

A "static" variable inside a C function is permanent, but is only visible inside the function. Many Ada programmers simply co-locate data items and their subprograms:

    declare
       ...
       X: Integer := 0; -- permanent counter

       function Count return Integer is
       begin
          X := X + 1;
          return X;
       end Count;

To limit the scope of a variable to one subprogram is a little more work. One approach is to put the subprogram inside a local package:

    declare
       ...
       package Count_Wrapper is
          function Count return Integer;
       end Count_Wrapper;

       package body Count_Wrapper is

          X: Integer := 0;

          function Count is
          begin
             X := X + 1;
             return X;
          end Count;

       end Count_Wrapper;

       function Count return Integer renames Count_Wrapper.Count;

For a single variable in a single subprogram, this is more code than C requires. However:

  1. It is obvious at a glance when persistent data items exist.

  2. There is no more work for many local variables than for one.

  3. This idiom can handle persistent data that should be shared by several subprograms. You cannot do this in C: data is either visible in the entire file, or in only one function.

12. Direct multiple inheritance in object-oriented programming

There are several common kinds of multiple inheritance, each with several variants. Instead of picking one and building it into Ada's syntax, the Ada 95 developers provided building blocks that can be used to build up the different types of multiple inheritance. The Ada 95 Rationale (section 4.6) describes how to do this.

13. Variable-length argument lists

Most procedural languages (e.g., C, Fortran, Ada) provide for only fixed parameter lists. However, there are workarounds in all of these languages. Fortran has a "varargs" facility; C has "varargs" and "stdarg" (which is ANSI C compatible).

The Ada community has not yet adopted a standard variable-length parameter mechanism. However, it is easy to do in several ways, each of which is best for some applications. The wise programmer, when coding one of them, will create a var-arg package that can be reused.

The Ada approaches take more programmer work than, for example, the C approach, which directly accesses the call stack. This is partly because Ada's semantics don't define a call stack. The Ada approaches will work even on machines (like "Lisp machines") where the "call stack" may not be a single chunk of memory.

The Ada approaches are also type-safe: the called subprogram knows definitely what type of object the caller provided. (In the last approach shown, the called subprogram doesn't know the parameter's type, but it dynamically dispatches to the correct subprogram for the parameter.)

Several approaches are shown in appendix A2, specifically:

14. Rapid prototyping

There are three common types of rapid prototype: a GUI prototype, an exploratory or "proof of concept" prototype, and a small-scale prototype intended to verify a design for a larger system.

For a GUI prototype, the appropriate tool is not a language at all, but a GUI builder.

A "proof of concept" prototype typically uses a GUI builder for a user interface, and a programming language to "rough in" a limited subset of the proposed functionality. Ada is getting more representation among such tools. Some people prefer C for this, some prefer Lisp, some prefer Ada. Experience suggests that the fastest prototype comes from the language most familiar to the developer. Some argue against Ada for this application because of its strong typing; see "Loose typing". Ada's type support can be useful for prototyping, for example with attributes like 'Image.

Design prototypes are often useful, and generally should be built using the language and environment to be used for the real system, so experience with the prototype will directly apply to the actual system. Some people advocate building a prototype in a special prototyping environment, then using that prototype as a first-level design for the real system. There are few, if any, real-world success stories available for this approach.

Copyright © 1997 Samuel Mize. Permission granted to copy the entire text including this notice, to quote entire sections with attribution, and to use code from (or derived from) the examples herein without limit.


Do you want to share ideas or tricks about elegant Ada answers to common objections or false claims?

Resources


What did you think of this article?

Very interesting
Interesting
Not interesting
Too long
Just right
Too short
Too detailed
Just right
Not detailed enough
Comments:

Page last modified: 1997-06-16