I understand Ada is a really old language, but it sucks that we have to have header files seperate from the actual definition. It feels like extra wasted work. It would be nice if you didn’t have to have seperate .ads and .adb files, consider Rust and other modern low level system programming languages don’t need header files.
You may be misunderstanding Ada’s design.
Ada doesn’t have header-files, it has specification files; while this might seem like a distinction without a difference at first, I will explain how it really is different. (Albeit, I do have to acknowledge that the theoretical/notional header can be used in an analogous manner, in my experience it never is.)
First, the the language disallows an implementation (X.adb) where everything is defined in the specification (X.ads). Certain constructs need to be put into the implementation, like subprograms that are not either (a) imports, or (b) expression functions.
So:
Package Math is
Pi : Constant := 3.1415;
Type Polynomial is Array(Natural range <>) of Integer;
Package Calculus is
Calculus_Constant : Constant:= 3; -- whatever.
End Calculus;
End Math;
Since there is nothing requiring an implementation, an implementation is not allowed for either, but if we were to add to Calculus
Function Integrate( Object : Polynomial ) return Polynomial;
this would require Calculus
to have an implementation, which would require Math
to have an implementation.
Secondly, the strict separation between implementation and specification yields the ability to (a) have separately compiled implementations and specifications, and (b) the ability to have alternate implementations. While the latter is handled by that compiler’s particular build-system, the former is very useful for avoiding needless recompilation: if an implementation changes, it does not require any alteration to those modules that are depending on the specification. (Altering specifications is significantly harder, but [IIRC] some compilers have applied much of the same reasoning to avoid recompiling the entire specification+implementation. [Rational R-1000, IIRC.])
It may help to think of the specification as the INTERFACE that everything in your program actually sees, with the IMPLEMENTATION being the actual instance for that interface.
Ada doesn’t have header files, and doesn’t have a preprocessor at all.
- A GNAT .ads file is a specification.
- A GNAT .adb file is an implementation of that specification.
The specification is a public SUBSET of what’s available in the implementation. Any functions and types you want to use to implement that behavior but not expose to users, doesn’t need to be in the specification.
Continually updating a specification file is indicative to me that I’m doing part of the design incorrectly and that whatever is using this package should either be part of the package, or maybe is a candidate as a child package.
I tend to ignore implementation files in entire portions of my larger programs and rely only on specification files as systems get put into place. It does harm how fast you initially move when developing, but it seems to help later on.
Benefits:
- This lets you separate what is usable from the outside vs what is available inside a package, which is especially important in Ada since encapsulation is at the package level. You can hide things from users of the package by only putting them in the implementation file.
- If only .adb files change, you only need to rebuild those changes and relink. This is much more granular than Rust’s usage of crate as the compilation unit, and in my experience incremental builds in Ada are very quick.
- There is no preprocessor and no conditional compilation (like
#[cfg(...)]
) but you can use Alire/gprbuild to change out the implementation based on things like that platform (Windows vs Mac vs Linux). If you’ve ever dealt with C++ or Rust code with heavy use of#if
or#[cfg(...)]
it can be hard to keep track of what is doing what (especially in C or C++). - Other packages can only use what is placed in that specification file, so users don’t need to sift through additional internal functions to understand the package’s functional interface and types. Subprogram (procedures/functions) declarations can also include pre/post condition annotations in the package specification which can help understand how you can use things.
- EDIT ADDED: If you don’t change a specification file, then you don’t mess up any interfaces of how anyone is using your stuff. This helps isolate “could I break anyone downstream?” sorts of changes.
Cons:
- You have to maintain specification files. The compiler will complain loudly if the specification doesn’t match.
Although it is tedious to maintain, the separate specification is a gift for the users of your code.
For instance:
...
procedure Move (pdf : in out PDF_Out_Stream; to : Point);
procedure Line (pdf : in out PDF_Out_Stream; to : Point);
procedure Cubic_Bezier
(pdf : in out PDF_Out_Stream;
control_1, control_2 : in Point;
to : in Point);
procedure Arc
(pdf : in out PDF_Out_Stream;
center : in Point;
radius : in Real;
angle_1, angle_2 : in Real;
line_to_start : in Boolean);
procedure Circle
(pdf : in out PDF_Out_Stream;
center : in Point;
radius : in Real;
rendering : in Path_Rendering_Mode);
...
As a user, you don’t want to see the technicalities behind Line
or Circle
, you just want to use them.
Other advantages:
- For team work: you can already program with a package before your colleague has even begun to implement it!
- A specification sees only other specifications - and so on recursively. It makes the build times extremely short. An Ada program with millions of lines of code, thousands of files, dozens of interconnected .gpr projects takes only a few seconds to build on a normal PC, even after many changes.
NB: GNAT (not Ada in general) requires separate files. It has its advantages too: GNAT finds the specifications very easily. The users too, by the way.
To this point, this is something that I really love about Ada.
A major factor of what makes Ada a secure and safe language is designing it for readability and making it easier to maintain not just for yourself but for other people who might use the code later. It enforces a lot of things through it syntax with these kinds of things in mind.
Unlike many other languages, rust included, I have what is essentially documentation for any function vital to the package in the code built right in. Half the time when I’m trying to learn a new library, before trying to dig into the docs I often find just reading the specification files to be helpful enough.
Yes, it’s one year younger than C++.
Ada is designed for large projects, where it’s impossible for a developer to keep everything in the implementation in mind at once. This is what Dijkstra called having a “small head”, and as he said, we need techniques to deal with it. The separate specification is how Ada deals with it: a specification should contain everything that a client of the package needs to know to make use of the package. It tells you what the package provides and how to use it. Then you only have to keep in mind the specifications of the packages you are using, which hopefully will fit in your “small head” (or Dijkstra’s, if yours is bigger).
Another factor of large projects is that they are usually implemented by multiple teams, and this usually leads to needing to clearly and unambiguously communicate what one team is working on that other teams need to make use of, ensuring that other teams are aware of and properly adjust to changes in one team’s interface, and providing the ability for teams to make use of parts of the software that have yet to be implemented. The separate specification provides for all of these.
Typically, a project starts by a decomposition into top-level modules, with a package specification written for each module. Work on implementing the packages can make use of the other packages’ specifications, even if no one has started working on the implementations of some of them. If one of those specifications changes, the compiler ensures that all clients have accommodated the change.
For these reasons (and those mentioned here by others), software engineers consider the separate specification a friend that helps them, not an irritating technicality that must be dealt with. When I was involved with hiring, a candidate’s attitude toward separate specifications was one of the indicators I used to differentiate software engineers from coders.
Actually Ada does not expect the specification and the body to be placed in different files. This is a requirement from the GNAT toolchain. Read this short paragraph : Packages.
Not as ancient as C and about the same age as C++.
They’re not header files, that’s C, these are compilation units which can provide declarations for the definitions (bodies), but they don’t have to, see Shark’s post.
As others have said, it was a choice when GNAT was designed to expect each “compilation unit” to be in a separate file. In fact, GNAT does support having multiple compilation units in a single file, by using the “gnatname” tool.
FWIW, the PTC ObjectAda compiler allows multiple compilation units in a file, as does the Green Hills compiler.
Note that by separating specs and bodies, you will generally need to do less recompilation when you make a change to some part of your program. However, I can see if you are doing a quick personal project, you might find it simpler to put everything into one file. In that case, you should probably try using gnatname to see if it does what you want.
Another simple way (simpler, I think, because no GPR needed) is to use gnatchop
. When someone posts a problem with >1 compilation unit, I usually copy them all into one .ada
file, and then run gnatchop -r -w
:
-r
means to add source reference pragmas, so that compiler warnings/errors refer to lines in the.ada
file,-w
allows overwriting on subsequent runs.
Other switches are available.
I wrote an Emacs minor mode to run gnatchop
on save.
I understand Ada is a really mature language.
Welcome @charlie5
!!!
If by mature, you mean well developed, well tested and very refined, then yes you are right. (I hope you didn’t mean old.
C++ is often referred to as “modern” but it’s just as old as Ada.
)
Welcome @ValorZard
!!!
I hope we haven’t scare you off with all these long post that could be interpreted as defensive. But what I’ve seen of this community they are considerate and constructive. So, please interpret it that way.
The separation of the Specification from the Body/Implementation into separate files is not a requirement of the Ada language; it is a requirement for the GNAT compiler. Other compilers are willing to accept them in one file.
But the separation between the Specification and Body/Implementation into separate sections (whether or not it’s separate files) is important for users of your packages. Users of a package need to know the interface of the package. The Specification gives a complete listing of what the package offers without the noise of how it does it.
If you are not trying to create a public package, then you don’t need a specification. You program entry point can be just a body. Any function you need to create for you program could be in the declarative part of the main function. (Before the begin
keyword.)
Example:
with Ada.Text_IO;
function Main return Integer is
--- Nested functions for use within Main ---
function Add (number1, number2: Integer) return Integer is
begin
return number1 + number2
end Add;
procedure Report_Stuff (num1, num2: Integer) is
begin
Ada.Text_IO.Put_Line (num1'Image
& " + "
& num2'Image
& " = "
& Add (num1, num2)'Image );
end Report_Stuff;
--- Whatever else you need for Main ---
begin --- Main ---
Ada.Text_IO.Put_Line ("We are going to combine some numbers...")
Report_Stuff (1, 2);
Report_Stuff (7, 3);
Report_Stuff (4, 6);
end main;
But if you are just making packages for yourself, you might think you don’t need a separate Specification because you know what you are doing and what the interface to the package is.
… Well, the lessons learned about the need for good comments in your code apply here too. Future-you will thank now-you for the effort.
Thanks @Trescott
Yes, I meant well developed, well tested and very refined. I keep hearing Ada referred to as ‘old’, which can have a negative connotation, so I prefer ‘mature’, especially since the latest upgrade was only about 2 years ago.
In addition to all the above, one other dimension imo is documentation which is much easier to navigate than reading heaps and heaps of source code trying to discern the available (public) subprograms that can be called from a client (i.e. whatever other component is making the call).
For example, I rarely remember all the subprograms available in say Doubly_Linked_List
so when I get to a point where I need to change my container in some way, I head to the specification file and simply scroll through the non-private area of that and very quickly find what I am looking for (the toggle button from .ads
to .adb
files and vice-versa in GNATStudio and the really handy comments help a lot there too).
The important bit is that I’m not really interested in how some subprogram has been implemented, all I want to know is its name and what arguments I need to pass when I call it. I’d make really slow progress with my task at hand if I had to read and understand the implementation code!
(I believe there’s also a tool to generate some kind of html-ish docs from the specification too?)
Interestingly Grady Booch 83 components files ended with .1 and .2. I guess predating .ads and .adb. One reason other than API readability might have been that he took advantage of the body or .2 as a privacy feature. I prefer a private section in the .ads but I think there may be reasons to use the body too.