Ada is incredibly well designed. One way this shows is that it takes the big, monolithic features of other languages and breaks them down into their constituent parts, so we can choose which portions of those features we want. The example I often reach for to explain this is object-oriented programming.
Object-oriented programming, in parts
I never truly understood object-oriented programming until I learned Ada, which breaks down object-oriented programming into separate features, like
- encapsulation,
- reuse,
- inheritance,
- abstract interfaces,
- type extension, and
- dynamic dispatch.
In languages like Java, we type the keyword class, and then all of these
features – whether we want them or not – come along for the ride. By contrast,
in Ada, these are separate features we can opt into separately. I much prefer
Ada’s fine-grained control over which language features are used, both because
it helps write maintainable code, but also because it makes me a better
programmer.
An acquaintance got curious and asked me to translate a small Java example into Ada for comparison. This ended up not being a useful illustration, but it might satisfy someone’s curiosity anyway.
An interface with two implementations
The Java program provided starts out by creating a class hierarchy for vehicle engines.
interface Engine { public void run(); }
This can be translated fairly directly into Ada, with the main difference being that Ada splits up packages into package specification and package body as separate blocks. This causes some repetition of code, but in larger projects I’ve found it to be very useful to see the specification at a glance.1 You can think of a package specification as a C header file, but it’s important to note that Ada package specifications are much more structured and actually used by the compiler to help the programmer.
First, the specification:2 Sorry about the lack of syntax highlighting. I do not have an Ada mode installed at the moment.
package Engines is type Engine is interface; procedure Run (Self : Engine) is abstract; end Engines;
We define Engine as an abstract interface, and give it a method Run. The
Run method is really just a normal procedure, but Ada has some rules for
figuring out which procedures are primitive operations, and all primitive
operations count as methods belonging to their interface or class. Run is the
only one here, and it is public because things in a package specification are
public by default.
Then the Java code adds two implementations of the interface.
class TwoStroke implements Engine { public void run() { System.out.println("bam bam bababam bam bam..."); } } class V8 implements Engine { public void run() { System.out.println("dadadadadadada..."); } }
In Ada, we add the corresponding code to the package specification.
package Engines is ------->8------- type Two_Stroke is new Engine with null record; overriding procedure Run (Self : Two_Stroke); type V8 is new Engine with null record; overriding procedure Run (Self : V8); end Engines;
When we say our child types are new engines with null record we are telling
Ada that we do not wish to add any fields to the object when we extend it.
Whereas many languages default to assuming nothing should happen (like the Java
code above assumes we want to extend without adding data), Ada always requires
the programmer to explicitly request nothing if nothing is what they want. This
makes some bugs more difficult to write.
We also have to implement these methods, and that happens in the package body.
with Ada.Text_IO;
package body Engines is
procedure Run (Self : Two_Stroke) is
begin
Ada.Text_IO.Put_Line ("bam bam bababam bam bam...");
end Run;
procedure Run (Self : V8) is
begin
Ada.Text_IO.Put_Line ("dadadadadadada...");
end Run;
end Engines;
Recalling that Run is a regular procedure, we can actually invoke it as
Run(My_Engine) as long as My_Engine is of a concrete type, like
Two_Stroke. It is only when the real type of My_Engine is unknown (because
it could be anything that implements the Engine interface) that we need to
call it with the object-oriented syntax My_Engine.Run. That syntax performs
dynamic dispatch, which is possible only on types that are tagged. Types
declared as interface are implicitly tagged.
Engine memory management
Great! Moving on. The next step is defining a vehicle. We will flesh it out later, but these are the first important steps from the Java code.
class Vehicle { protected int size; protected Engine engine; public void drive() { this.engine.run(); } }
If we get cracking on the Ada code now, we’d realise that we want the vehicle to
be able to hold any type that implements the Engine interface. This means
the compiler cannot know the size of the Vehicle type in advance. Java doesn’t
care about that because it always assumes Engine is a reference to a
heap-allocated type, but in Ada we need to be explicit about this because Ada
allows us to store things on the stack.
The easy solution in the Ada code is to do what Java does, and store a
reference to a separately allocated Engine object in the Vehicle type. We
go back to our Engines package and add the bits we need to support that.
with Ada.Unchecked_Deallocation;
package Engines is
------->8-------
type Engine_Access is access all Engine'Class;
procedure Free is new Ada.Unchecked_Deallocation
(Engine'Class, Engine_Access);
------->8-------
end Engines;
Here we declare Engine_Access to be a reference to anything that implements
the Engine interface. Ada has C-style pointers, but generally they are
avoided. The various access types used in Ada are safer alternatives, because
they are scoped using clever rules to make it hard to leak references to objects
that can go out of scope.
In this case, however, we mirror the Java code and create a global access type
that can reference anything anywhere. Thus, we also make a concrete instance of
the Unchecked_Deallocate procedure for this type, so that any dynamically
allocated engines can later be freed.
The syntax Some_Type'Class means “the entire class hierarchy starting from
Some_Type”.
With that out of the way, we can complete the Ada code for the Vehicle type.
First the package specification.
with Ada.Finalization;
with Engines;
package Vehicles is
type Vehicle (<>) is tagged limited private;
procedure Drive (Self : Vehicle);
private
type Vehicle is new Ada.Finalization.Limited_Controlled with
record
Size : Integer;
Engine : Engines.Engine_Access;
end record;
overriding procedure Finalize (Self : in out Vehicle);
end Vehicles;
There’s a lot going on here, and most of it comes down to memory management.3 There might be a way to simplify the code to get rid of much of this and rely entirely on static allocation through discriminants, but my Ada-fu is too weak to figure out how.
First off, we declare a general Vehicle type. We give it an unknown
discriminant (that’s the (<>) bit), which prevents it from being implicitly
allocated (e.g. when it is used as a stack variable); this in practice forces
users to go through a constructor to create it. We set it to tagged for
dynamic dispatch. We also set it to limited to prevent assignment, because
that would implicitly copy the Engine reference and cause multiple vehicles to
share the same engine instance.4 Alternatively, we could make it non-limited
and instead implement the Adjust method from Ada.Finalization.Controlled.
The Adjust method is implicitly invoked during assignment to be responsible
for managing resources involved in the copying operation. To conserve space in
this article, let’s not do that here. To begin with, it has only one method,
Drive.
The private part of the package specification is only visible to this package
and its child packages. Here we specify the actual structure of the Vehicle
type. It inherits from Ada.Finalization.Limited_Controlled to make it a
controlled type (more on that soon), and it gets its data fields. As in the
Java code, the Engine field is of type reference-to-engine. Since Vehicle is
a controlled type, we inherit a Finalize procedure we will want to override,
so we declare that here.
Oh, and the in out mode on the Self parameter to the Finalize method
indicates that the method can mutate the object it is called on. Parameters are
by default using in mode, which indicates read-only. It’s also possible to
request out mode, which means the method can assign to that parameter value,
but not read from it.
The package body for this is:
package body Vehicles is
procedure Finalize (Self : in out Vehicle) is
begin
Engines.Free (Self.Engine);
end Finalize;
procedure Drive (Self : Vehicle) is
begin
Self.Engine.Run;
end Drive;
end Vehicles;
Ada understands, even though Self.Engine is a reference to an engine, that we
wish to invoke the Run method on the object it refers to, not the reference
itself.
The bit here that’s new is the Finalize method. In Ada, a controlled type
gives access to raii functionality like in C++. When a controlled type (one that
implements the Controlled or Limited_Controlled interfaces) comes into
scope, the compiler implicitly calls its Initialize method. When it goes out
of scope, the compiler implicitly calls its Finalize method. We use the latter
here to free any engines that may have been allocated when the vehicle was
created, to prevent leaking memory.
Ada does not have a garbage collector, nor is it a fan of C-style malloc/free/sbrk/mmap memory management, which is fraught with difficulties. In regular Ada code, there are often ways to get around malloc-style dynamic memory management entirely. For example, since access types (references) in Ada are scoped, it is safer in Ada to stack allocate than elsewhere – references to stack allocated values cannot leak outside of where they are valid. Ada also supports storage pool-based allocation.
But! In this example do use malloc-style allocation, so we also need to take
care to deallocate. While in C this is up to the programmer, Ada helps us at
least a little by providing controlled types that have their Finalize method
called automatically, before an object becomes unreachable, like in C++.
Interacting objects
The Java code does not end there, though. This is the full Vehicle object.
class Vehicle { protected int size; protected Engine engine; public void drive() { this.engine.run(); } public void crash(Vehicle other) { this.drive(); other.drive(); if (this.size + other.size > 25) { System.out.println("CRASH!"); } else { System.out.println("bonk!"); } other.angryReaction(); } public void angryReaction() {} }
When two vehicles crash, they first drive and then there’s some logic in there for determining the severity of the crash. Finally, the receiving end of the crash presumably shouts some expletives, depending on what type of vehicle they were travelling in.
We add the necessary parts to the Ada package specification.
package Vehicles is ------->8------- procedure Crash (Self : Vehicle; Other : Vehicle'Class); procedure Angry_Reaction (Self : Vehicle) is null; ------->8------- end Vehicles;
…and to the package body.
with Ada.Text_IO;
package body Vehicles is
------->8-------
procedure Crash (Self : Vehicle; Other : Vehicle'Class) is
begin
Self.Drive;
Other.Drive;
if Self.Size + Other.Size > 25 then
Ada.Text_IO.Put_Line ("CRASH!");
else
Ada.Text_IO.Put_Line ("bonk!");
end if;
Other.Angry_Reaction;
end Crash;
end Vehicles;
The only thing of note here is that while Ada performs dynamic dispatch on the
first argument – it finds the most specific method on any Vehicle child type
this method is invoked on – we must explicitly ask that the second argument is
of any type Vehicle'Class, otherwise Ada would expect it to be specifically of
the type Vehicle and not any child type.
This is not really different to the Java code. When in Java we say Vehicle it
automatically implies Vehicle'Class. In Ada, by contrast, we get the
opportunity to ask specifically for Vehicle and not any of its child types. In
fact, that’s the default. This is one of the ways in which Ada makes us us opt
into the specific parts of oop we want, rather than assuming we want all of
them.
Declaring a child type
The Java code then defines a type of vehicle, that comes with a size and a type of engine, and it overrides its reaction.5 Note that none of this is good object-oriented design! I’m not trying to teach that here. I received an example someone was curious about and I’m being faithful to it.
class Moped extends Vehicle { Moped() { this.size = 10; this.engine = new TwoStroke(); } public void angryReaction() { System.out.println("Hey! You twisted my wheel!"); } }
Since these fields are protected in the Java code, they are visible to child types. Ada controls field visibility with its package system instead of classes – another way in which Ada lets us opt into the goodies of oop without everything falling out of the closet at once.
Since we want our child type to access the parent’s fields, we’ll declare the child type in the same package as the parent type.6 Don’t worry, this does not mean it’s impossible to extend third party libraries. The package hierarchy is open, and child packages can also access the internals of their parents.
package Vehicles is ------->8------- type Moped (<>) is new Vehicle with private; function Create_Moped return Moped; overriding procedure Angry_Reaction (Self : Moped); private ------->8------- type Moped is new Vehicle with null record; end Vehicles;
The package body ought to be just as familiar.
package body Vehicles is
------->8-------
function Create_Moped return Moped is
begin
return (
Ada.Finalization.Limited_Controlled with
Size => 10,
Engine => new Engines.Two_Stroke
);
end;
procedure Angry_Reaction (Self : Moped) is
begin
Ada.Text_IO.Put_Line ("Hey! You twisted my wheel!");
end Angry_Reaction;
end Vehicles;
As you might be able to guess after reading this, there are no built-in
constructors in Ada. Any function that returns an object can be used as a
constructor.7 That said, this is one area where Ada has painted itself into
an unpleasant corner. We are able to do this in this code because nothing
inherits from our concrete vehicle child types. If we inherited from them, we
would find that their constructors (being primitive operations, remember!) would
be inherited too, which is undesirable. There are some ways around this, the
most conventional being to define constructors in a child package to prevent
them from being considered primitive operations by the compiler. To construct a
Moped, we first request a Limited_Controlled and then add values for the
fields we want to initialise. This is also where we malloc-style dynamically
allocate a Two_Stroke for the moped, using the new keywoard.
Extending data fields in subtypes
The example requires one more thing: a different vehicle.
class Car extends Vehicle { private String color; Car(String color) { this.size = 50; this.engine = new V8(); this.color = color; } public void angryReaction() { System.out.printf( "Hey! You scratched my %s paint!\n", this.color ); } }
By now, I don’t think much of the Ada code would be confusing. The only thing new here is that this subtype extends the parent with a data field that doesn’t exist on the parent – and this is a detail private to this child type. We add to the vehicle package specification:
with Ada.Strings.Unbounded;
use Ada.Strings.Unbounded;
------->8-------
package Vehicles is
------->8-------
type Car (<>) is new Vehicle with private;
function Create_Car (Colour : Unbounded_String) return Car;
overriding procedure Angry_Reaction (Self : Car);
private
------->8-------
type Car is new Vehicle with
record
Colour : Unbounded_String;
end record;
end Vehicles;
and the package body
package body Vehicles is
------->8-------
function Create_Car (Colour : Unbounded_String) return Car is
begin
return (
Ada.Finalization.Limited_Controlled with
Size => 50,
Engine => new Engines.V8,
Colour => Colour
);
end;
procedure Angry_Reaction (Self : Car) is
begin
Ada.Text_IO.Put_Line (
"Hey! You scratched my " & To_String (Self.Colour) & " paint!"
);
end Angry_Reaction;
end Vehicles;
Plain old String values in Ada are statically allocated, meaning we have to
know the length of the string to use it in a record field (or punt that decision
up the chain). To get out of that annoyance, we use the standard library
Unbounded_String which is dynamically allocated, similar to std::string in
C++.
Tying it together
After all of that comes the payoff. The main function of the Java code says
public class inheritance { public static void main(String[] args) { Moped moped = new Moped(); Car car = new Car("blue"); System.out.println("car crashes into car:"); car.crash(car); System.out.println(); System.out.println("moped crashes into car:"); moped.crash(car); System.out.println(); System.out.println("car crashes into moped:"); car.crash(moped); System.out.println(); System.out.println("moped crashes into moped:"); moped.crash(moped); System.out.println(); } }
In Ada, we have the very similar
with Ada.Text_IO;
with Ada.Strings.Unbounded;
use Ada.Strings.Unbounded;
with Engines;
with Vehicles;
procedure Main is
package IO renames Ada.Text_IO;
Moped : Vehicles.Moped := Vehicles.Create_Moped;
Car : Vehicles.Car := Vehicles.Create_Car (
To_Unbounded_String("blue")
);
begin
IO.Put_Line ("car crashes into car:");
Car.Crash (Car);
IO.Put_Line ("");
IO.Put_Line ("moped crashes into car:");
Moped.Crash (Car);
IO.Put_Line ("");
IO.Put_Line ("car crashes into moped:");
Car.Crash (Moped);
IO.Put_Line ("");
IO.Put_Line ("moped crashes into moped:");
Moped.Crash (Moped);
IO.Put_Line ("");
end Main;
Full code is available at the unlisted adaoopexa repo on SourceHut.
Reflection (not that kind!)
This exercise wasn’t as effecive in showing off how great Ada is as I had hoped for. In part, this is because a lot of what makes Ada great is subtle and comes out of the tiny interactions between features that are designed to fit together perfectly. Although I have tried to hint at things, it does not really show in examples like these.
Additionally, some of the things that make Ada great happen specifically when we do not try to replicate a class hierarchy in it. Being able to hide implementations and data in the private areas of packages, without having to create a full-blown class hierarchy for it, is great.
But most of all, Ada competes with C, not with Java. Imagine trying to make the above things happen in C. I don’t actually have to imagine because I’ve seen an attempt.8 I will not share that since the author has not consented to it. Happy to receive submissions people are willing to share. It was longer than the Ada code – which is already a verbose language – and works only because it happens to be just so for this example. Change requirements and the C code is highly likely to run into confusing bugs, leak memory, or both.