comp.lang.ada
 help / color / mirror / Atom feed
* Re: Ada can-do list
  1997-05-21  0:00 Ada can-do list Samuel Mize
@ 1997-05-21  0:00 ` John G. Volan
  0 siblings, 0 replies; 2+ messages in thread
From: John G. Volan @ 1997-05-21  0:00 UTC (permalink / raw)



Samuel Mize wrote:
> 
>                                ADA CAN DO IT!
>                                 Samuel Mize
> 
>           Common false claims about things Ada won't let you do,
>                             and how to do them
>                  (and why some are not directly supported)

An excellent contribution!  I applaud your effort!  I do hope this is
accepted at adahome.

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

One thing you might add is that it is easy to write Ada procedures which
are equivalent in functionality to these C operators, but which are much
safer because they would not be functions-with-side-effect.

It might have been nice if Ada somehow included such procedures as
built-in features (e.g., as attributes), so that you wouldn't have to
write them every time you introduced a new scalar type, but this is not
the case.  However, generic packages can help reduce the work of writing
them by hand.  Here's an example for integer types (similar generic
packages can be worked up for modular, enumeration, floating and
fixed-point types):

  generic
    type Integer_Type is range <>;
  package Generic_Integer_Ops is

    procedure Increment (This : in out Integer_Type'Base); -- ++ analog
    procedure Decrement (This : in out Integer_Type'Base); -- -- analog
    procedure Increase  (This : in out Integer_Type'Base;  -- += analog
                         By   : in     Integer_Type'Base);
    procedure Decrease  (This : in out Integer_Type'Base;  -- -= analog
                         By   : in     Integer_Type'Base);
    procedure Multply   (This : in out Integer_Type'Base;  -- *= analog
                         By   : in     Integer_Type'Base);
    procedure Divide    (This : in out Integer_Type'Base;  -- /= analog
                         By   : in     Integer_Type'Base);
    procedure Modulo    (This : in out Integer_Type'Base;  -- %= analog
                         By   : in     Integer_Type'Base);
    procedure Remainder (This : in out Integer_Type'Base;  -- %= analog
                         By   : in     Integer_Type'Base);

    pragma Inline (Increment, Decrement, Increase, Decrease,
                   Multiply, Divide, Modulo, Remainder);

  end Generic_Integer_Ops;

Here's the body for this generic.  As you can see, the implementations
of these procedures are fairly trivial:

  package body Generic_Integer_Ops is

    procedure Increment (This : in out Integer_Type'Base) is -- ++
analog
    begin
      This := Integer_Type'Succ(This);
    end Increment;
 
    procedure Decrement (This : in out Integer_Type'Base) is -- --
analog
    begin
      This := Integer_Type'Pred(This);
    end Decrement;

    procedure Increase  (This : in out Integer_Type'Base;  -- +- analog
                         By   : in     Integer_Type'Base) is
    begin
      This := This + By;
    end Increase;

    procedure Decrease  (This : in out Integer_Type'Base;  -- -= analog
                         By   : in     Integer_Type'Base) is
    begin
      This := This - By;
    end Decrease;

    procedure Multply   (This : in out Integer_Type'Base;  -- *= analog
                         By   : in     Integer_Type'Base) is
    begin
      This := This * By;
    end Multiply;

    procedure Divide    (This : in out Integer_Type'Base;  -- /= analog
                         By   : in     Integer_Type'Base) is
    begin
      This := This / By;
    end Divide;

    procedure Modulo    (This : in out Integer_Type'Base;  -- %= analog
                         By   : in     Integer_Type'Base) is
    begin
      This := This mod By;
    end Modulo;

    procedure Remainder (This : in out Integer_Type'Base;  -- %= analog
                         By   : in     Integer_Type'Base) is
    begin
      This := This rem By;
    end Remainder;

  end Generic_Integer_Ops;

Users could instantiate this generic for any of their own integer types:

  Error_Limit : constant := ...;
  type Error_Count_Type is range 0 .. Error_Limit;
  package Error_Count_Ops is new Generic_Integer_Ops (Error_Count_Type);
  ...
  Error_Count, Unhandled_Error_Count, Handled_Error_Count : 
    Error_Count_Type := 0;

  ...
  -- when a given error first occurs:
  Error_Count_Ops.Increment (Error_Count);
  Error_Count_Ops.Increment (Unhandled_Error_Count);

  ...
  -- once a given error is handled:
  Error_Count_Ops.Decrement (Unhandled_Error_Count);
  Error_Count_Ops.Increment (Handled_Error_Count);

The users' work isn't reduced to zero: They still have to do the
instantiation, and they have to include the instance name as a prefix on
every call to one of these procedures (unless they throw in
use-clauses).

There is also the possibility of code-bloat if these generics are
instantiated for many user-defined types.  However, this is alleviated
somewhat by inlining all of the procedures: They'll only get
inline-expanded where they are called, so they will be equivalent to the
user's having written "X := X + 1;" and so forth by hand.  (Also,
code-bloat is really an implementation concern; it depends on how your
particular compiler happens to implement generics.)

Another alternative is to exploit Ada's inheritance mechanism for
non-tagged derived types.  Instead of just providing the ops, the
generic could provide a new type as well, in which case the ops become
inheritable primitives for that type. For example:

  generic
    type Implementation_Type is range <>;
  package Generic_Integers is

    type Integer_Type is new Implementation_Type'Base;

    procedure Increment (This : in out Integer_Type); -- ++ analog
    procedure Decrement (This : in out Integer_Type); -- -- analog
    procedure Increase  (This : in out Integer_Type;  -- += analog
                         By   : in     Integer_Type);
    procedure Decrease  (This : in out Integer_Type;  -- -= analog
                         By   : in     Integer_Type);
    procedure Multply   (This : in out Integer_Type;  -- *= analog
                         By   : in     Integer_Type);
    procedure Divide    (This : in out Integer_Type;  -- /= analog
                         By   : in     Integer_Type);
    procedure Modulo    (This : in out Integer_Type;  -- %= analog
                         By   : in     Integer_Type);
    procedure Remainder (This : in out Integer_Type;  -- %= analog
                         By   : in     Integer_Type);

    pragma Inline (Increment, Decrement, Increase, Decrease,
                   Multiply, Divide, Modulo, Remainder);
  end Generic_Integers;

(The body of Generic_Integers would be almost identical to
Generic_Integer_Ops.)

This generic could be instantiated just for the standard types supported
by the given compiler (avoiding potential code-bloat):

  with Generic_Integers;
  package Integers is new Generic_Integers (Integer);

  with Generic_Integers;
  package Long_Integers is new Integers (Long_Integer);

  with Generic_Integers;
  package Short_Integers is new Integers (Short_Integer);

  ... etc.

(Or the users could define a few application-specific base types and
instantiate for those.  Or they could use types from Interfaces or
whatever...)

Then a given user-defined type could be declared by deriving from one of
the types from one of these instantiations:

  Error_Limit : constant := ...;
  type Error_Count_Type is new Short_Integers.Integer_Type
    range 0 .. Error_Limit;
  -- "automagically" inherits Increment, Decrement, etc...

  ...
  -- when a given error first occurs:
  Increment (Error_Count);
  Increment (Unhandled_Error_Count);

  ...
  -- once a given error is handled:
  Decrement (Unhandled_Error_Count);
  Increment (Handled_Error_Count);

The downside of this is that a given user-defined type must be tied to
some underlying implementation, which reduces its portability.

------------------------------------------------------------------------
Internet.Usenet.Put_Signature 
  (Name => "John G. Volan",  Home_Email => "johnvolan@sprintmail.com",
   Slogan => "Ada95: The World's *FIRST* International-Standard OOPL",
   Disclaimer => "These opinions were never defined, so using them " & 
     "would be erroneous...or is that just nondeterministic now? :-) ");
------------------------------------------------------------------------




^ permalink raw reply	[flat|nested] 2+ messages in thread

* Ada can-do list
@ 1997-05-21  0:00 Samuel Mize
  1997-05-21  0:00 ` John G. Volan
  0 siblings, 1 reply; 2+ messages in thread
From: Samuel Mize @ 1997-05-21  0:00 UTC (permalink / raw)



Greetings,

I'm planning to submit this to adahome.  Please comment, suggest
other tasks one supposedly can't do in Ada, etc.  My email is
MUCH more reliable than my news feed, please use both (or just
email).  Thanks!

Sam Mize



                               ADA CAN DO IT!
                                Samuel Mize

          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. Object-oriented programming with multiple inheritance
 13. Rapid prototyping

1. Loose typing -----------------------------------------------------------

Sometimes one doesn't want strong typing, e.g. for a quick use-and-discard
program, or in rapid prototyping.

Ada can be as "quick and dirty" as C.  Just use the predefined types
(character, string, integer, float).  You CAN define a new type for each
variable, but 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,
almost by definition, not being carefully designed and coded.

Lisp and its derivatives provide mutable 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.

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.

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.

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. Object-oriented programming with multiple inheritance -----------------

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. 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.

-- end --



-- 
Samuel Mize -- smize@imagin.net -- Team Ada
(personal net account)




^ permalink raw reply	[flat|nested] 2+ messages in thread

end of thread, other threads:[~1997-05-21  0:00 UTC | newest]

Thread overview: 2+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
1997-05-21  0:00 Ada can-do list Samuel Mize
1997-05-21  0:00 ` John G. Volan

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox