My normal day job is jumping into large codebases (millions of lines, usually C++) and helping out.
I’ve never written Ada professionally, but I did add MDX to the Ada Reference Manual outputter in 2023 to be able to make ada-lang.io’s ARM output. It’s only about 50k LoC, but has this comment showing it started in 2000:
-- Edit History:
--
-- 3/ 9/00 - RLB - Created base program.
-- 4/14/00 - RLB - Created from analysis program.
-- 4/18/00 - RLB - Added scanning pass.
-- ... SNIP A BUNCH OF LINES ...
-- 7/26/23 - RLB - Updated copyright date and address.
- A lot of problem detail is better hidden in packages due to the language structure.
Ada splits code into specification and body implementations, when written for GNAT each is in a separate file (.ads and .adb respectfully). This, combined with encapsulation in Ada being at the package level (types are opaque or non-opaque, visiblity is based on package relation), not the class level (C++, C#) means a lot of implementation details stays in the related implementation files. It also means types related to the same subproblem will be in the same file, and you don’t need to hop between multiple files to see the entire picture. 99% of the time, just seeing the spec file is all you need, and that spec file is much shorter.
In C++, C#, or Java, you start peeling back classes and interfaces and it takes a while to get to an executable line of code, because you need a constellation of objects to do anything meaningful. In Ada, I find that subproblems end up as distinct packages and the focus is on functional behavior, not on playing connect the dots with types.
In C++ or Rust, you’ll often deal with related types (types within types). In Ada, types cannot declare additional types inside themselves, everything is declared at the package level, so I often find an entire subproblem solved in the implementation with all the types used there, rather than jumping five files to understand a high level flow path.
- A lack of macros and turing complete templates means less “magic code.”
I’m confident I could take almost any programmer and toss them into an Ada codebase and they’d be able to read almost any part of the entire codebase in a few weeks. There’s only a few secret handshakes in generics code like (<>)
that you have to look up.
C++ and Rust code lets you really shoot yourself in the foot with complicated macros. Ruby on Rails is built on this sort of magic with metaprogramming as well. Rails, C++ and Rust do a lot of cool stuff with the macros and metaprogramming, but it can hinder understanding what is actually going on.
Ada doesn’t have macros, so what you read is what you get. There’s no wild __VA_ARGS__
usage in macros or std::enable_if
SFINAE or macro_rules!
code with wild and different rules. Ada generics occur at the package and function (or procedure) level, so the little bit of “this is what we’re generic over” is the weird part and the rest is “just code.” You don’t end in angle-bracket hell, instead you get a different type of hell where you have to explicitly instantiate templates, but then you use them just like non-generic code.
The downside of this is that sometimes you’ll see code generators to get around a lot of boilerplate (like in AUnit for unit testing).
Another downside is that straightforwardness sometimes just isn’t convenient. In Trendy Test, I abuse the exception mechanism for flow control in various ways to handle test registration and running. If I had the macro power of C++ or Ruby, or the metaprogramming of Ruby, there would be much better ergonomics.
- Constraints help
You can embed a lot of information in Ada directly. Two different int
s might have different semantic meaning and shouldn’t be mixed and Ada lets you do that, like a built-in Rust newtype or a Go type-definition. Ada depends on function overloading, so you can use that system to only allow meaningful operations: it makes sense to multiply Meters_Per_Second
by Seconds
to get Meters
. If you screw something up with a derived type, the compiler has your back and it embeds the meaning into the program.
This also goes for pre/post conditions which are on the specification, not an assert hidden inside an implementation. Often I didn’t have to bother looking at the implementation of something, which saved time.
- General nestability of packages/functions can lead to insanity
The one big issue I’ve seen in Ada is the ability to nest functions (using that term here instead of “subprogram”) inside of functions combined with the lack of closures in the language. One project I was looked to contribute to had multiply nested local functions, with a gigantic function with most contents starting halfway across my editor.
This is exaggerated, but something like this:
procedure Foo is
procedure Foo1 is
declare
procedure Foo2 is
procedure Foo3 is
-- ...
begin
-- ... some actual executable code