A Random Walk Through Ada (2014)(cowlark.com) |
A Random Walk Through Ada (2014)(cowlark.com) |
It was also featured on HN (194 points | 6 months ago | 117 comments: https://news.ycombinator.com/item?id=40132373
I've gone from not having anything Ada related on my computer to installing the toolchain, initiating a project , writing, compiling, and running "Hello World" in less then 2 minutes.
Ada has had something of a resurgence in the last couple years, with the latest standard (2022) officially released and for the first time in a few years, Ada has its own dev room at FOSDEM! https://fosdem.org/2025/news/2024-10-28-devrooms-announced/
There's also a growing community at ada-lang.io with a thriving forum: https://forum.ada-lang.io/
He refers to the need for forward declarations as "one-pass compiler". Historically at least, Ada compilers were infamous for requiring many passes and being consequently slow.
Whether it is still acceptable for a programming language to require forward declaration seems to me rather a matter of personal taste. Sure, compilers improved and there's plenty of memory for them to use, but human programmers' capacity increased little, if any, I'd think. I think of those forward declarations as form of double-entry bookkeeping.
I do wish we had better options for embedded! Even C++ isn't making much headway against C when it comes to programming microcontrollers.
A collection of less random and more up to date walks through Ada if this piqued your interest. It's been kept up to date with the latest language features and more tutorials. Also, helpfully, has comparative tutorials to see some features side-by-side in Ada, Java, C++, and C.
"As a historical note, earlier versions of Ada forbade reading from out variables, which allowed more efficient calling conventions, but modern Ada allows it because otherwise people went insane."
What is meant is that the "out" parameters behave like local variables without initializers, which upon entry into a function are likely to have invalid values.
Therefore you cannot read an "out" parameter before assigning a value to it. After the first assignment, it can be read or written at any time before the function returns.
Depending on the type, an "out" parameter is implemented as either a CPU register whose value is unknown upon function entry or as a reference to a memory area allocated in the caller, but uninitialized.
The specification of the function parameters as "in", "out" or "in out" and the irrelevance and transparency of how the compiler chooses to pass the parameters is one of the best features of Ada.
This has not been invented by the creators of Ada, but it has been invented by some anonymous Department of Defense employees or contractors.
This feature was included in the DoD "Requirements for High Order Computer Programming Languages", "IRONMAN" from the July 1977 revision. It was probably already present in the first version of "IRONMAN" from January 1977.
It is a huge mistake in the design of almost all programming languages that have appeared after Ada that they do not include this feature.
The lack of distinction between "out" and "in out" parameters has serious consequences especially for the object-oriented languages. The lack of this feature has caused a very large part of the complexity of C++ and also most of the performance problems of the C++ versions from before 2011.
When "out" and "in out" parameters are distinguished, there is no need for the existence of constructors as a separate concept. Any ordinary function with an "out" parameter can be used wherever C++ requires a dedicated constructor. Instead of constructors, it is enough to have an identifier convention for functions that should be called by the compiler when you want implicit initialization or implicit conversions.
Moreover, there is no need to care about "copy" semantics and "move" semantics or about temporaries which are or which are not generated by the compiler. It is easy for the compiler to always choose the best implementation without programmer intervention, when it knows the correct parameter modes for the functions.
A Random Walk Through Ada (2014) - https://news.ycombinator.com/item?id=12850919 - Nov 2016 (88 comments)
A Random Walk Through Ada (2014) - https://news.ycombinator.com/item?id=9674408 - June 2015 (14 comments)
Why do we need "out" vs "in out" parameters for this? Why can't a function that returns the object work just as well? And "an identifier convention for functions that should be called by the compiler when you want implicit initialization or implicit conversions" seems essentially the same as constructors to me.
>Moreover, there is no need to care about "copy" semantics and "move" semantics or about temporaries which are or which are not generated by the compiler. It is easy for the compiler to always choose the best implementation without programmer intervention, when it knows the correct parameter modes for the functions.
How would I implement something like std::vector with deep copy and move support of its contained objects without writing copy and move constructors (or operators)? There needs to be code saying "allocate a new block of memory and copy all the elements to that new block", and code saying "just copy the pointer, and set the other pointer to null". Are you saying the implementer would still write that code, just not in constructors/operators? How is that better?
> When "out" and "in out" parameters are distinguished, there is no need for the existence of constructors as a separate concept.
I don't agree with this. You can get I need to do things "post-init" with controlled types, or use a `return X : Thing do ... end return` block. Constructors help ensure invariants. You can make a type `is private` to prevent instantiation, only allowing creation via functions (sort of like Rust `new` associated functions), or initialization via an `out` param. It's OK but not perfect, but you can also tag on a `Type_Invariant` aspect if there are conditions which have to be met by a type. My big problem with Controlled types is that forces a type to be `tagged` (i.e. it has a vtable) which means it affects storage layout of values of that type, which isn't a problem in C++.
You can forbid copies by making something a `limited` type, but you'd have to write your own "Move" equivalent, and some of the containers have `Move` subprograms implemented to transfer contents. Limited types might elide the copy when returned from a function, but it's been a while since I looked at those rules.
Therefore calling a function just adds a task of invoking a constructor also into the function, it cannot replace a constructor.
In C++, an object cannot be created by assigning a value to it, because the left hand parameter of the assignment operator is an "in out" parameter, like for any other function, so it is assumed that it already has a value of the type of that object.
Therefore an assignment operator must execute the equivalent of invoking a destructor for the target of the assignment, followed by the invocation of a copy constructor.
Attempting to assign a value to memory allocated but not initialized will attempt to invoke a destructor for an invalid object value.
The constructors are the only C++ functions that implicitly have a result which is an "out" parameter, not an "in out" parameter, like the other functions.
Because the memory where the result of the constructor will be placed is not initialized, no destructor must be invoked for it, so all will be OK.
All these rules about when to use constructors and when to use functions and the need to have duplicate almost identical in meaning functions for certain purposes, e.g. copy constructors and assignment operators, complicates a lot C++.
Any copy or deep copy operations would be implemented very simply, just by copying what needs to be copied.
There would be no need for "move" operations of any kind, which are just a trick to avoid the inefficient code generated by compilers when the semantics for some parameters is "in out" when what would have really been needed is "out".
Specifying that some functions are not normal functions, but constructors, or that some operations have "move semantics" is just an extremely complicated way to specify that some function parameters are "out", not "in out". Besides being hard to understand, these tricks only work in particular cases, instead of being able to specify the mode of any function parameter.
In a language with distinction between "in out" and "out", the mode for the result of assignment is naturally "out" and there is no need for the contortions of C++. In such a language a compiler knows when to invoke destructors and when not to invoke them and it knows when no extra temporaries are needed for computing a complex formula with objects.
Among other useful innovations introduced by Ada, has been the use of underscore not only for making identifiers more readable (a use introduced by PL/I, 15 years earlier than Ada), but also for making the numeric constants easier to read.
Many other languages have copied this, but also decades after Ada. C++ has introduced this feature only in 2014, 35 years after Ada. I am quite annoyed by the fact that for this purpose C++ has replaced the underscore used by Ada and by most other languages that have added this feature with the apostrophe. There has existed no justification good enough for this choice, it was just a stupid decision.
While "the left hand parameter of the assignment operator is an "in out" parameter" is correct, I want to point out that
Foo f = CreateFoo(1);
never calls the assignment operator. It either calls the move constructor, copy constructor (only if the move constructor doesn't exist) or does copy elision (meaning no copies or moves, more on that below).>In C++ or similar languages, where the function parameters behave semantically as either "in" or "in out" parameters, a function that returns an object must have already created somehow the object.
If the object is being declared at the same time as being assigned to, and the function's return instantiates the object, e.g.
Foo CreateFoo(int x) {
return Foo(x);
}
Foo f = CreateFoo(1);
then as of C++17, this uses guaranteed copy elision. So there will be no copying or moving.https://en.cppreference.com/w/cpp/language/copy_elision
Additionally, there are other situations where the compiler is allowed (but not required) to do copy elision, e.g.
Foo CreateFoo(int x) {
Foo result(x);
return result;
}
Foo foo = CreateFoo(1);
Even if there isn't copy elision, usually move constructors (which is what will happen in the second example if the compiler decides to not do copy elision) are pretty cheap.With Ada, if you want to call a function with an out parameter, doesn't the object for the out parameter need to be created before calling the function, and thus at least partially initialized? The article says "Non-scalar values like pointers are always initialised.'
>Therefore an assignment operator must execute the equivalent of invoking a destructor for the target of the assignment, followed by the invocation of a copy constructor.
If the assignment operator isn't used, this problem doesn't happen.
>Attempting to assign a value to memory allocated but not initialized will attempt to invoke a destructor for an invalid object value.
Can you give an example of where this would happen? This should never happen unless you're doing something strange. If you do things with standard high-level capabilities, you won't hit this problem. For example you can allocate memory without initializing it with vector.reserve() , then later initialize an object into that memory with vector.push_back() or vector.emplace_back() . If you want to go low-level (e.g. to implement vector yourself), you have to be careful to get things right, but it's possible to avoid the problem you mention by using placement new.
>All these rules about when to use constructors and when to use functions and the need to have duplicate almost identical in meaning functions for certain purposes, e.g. copy constructors and assignment operators, complicates a lot C++.
I agree it's complicated. But I don't think "in out" parameters would solve all the use cases. E.g. I don't think they can guarantee invariants like constructors and destructors can.
>There would be no need for "move" operations of any kind, which are just a trick to avoid the inefficient code generated by compilers when the semantics for some parameters is "in out" when what would have really been needed is "out".
move operations aren't just needed for output. They're also used for input. E.g.
class Foo {
public:
void set_bar(std::string bar) { bar_ = std::move(bar); }
private:
std::string bar_;
};
Foo foo;
std::string bar = CoputeBar();
foo.set_bar(std::move(bar));
Here we can modify foo to contain a string, without copying the string. "in out" parameters wouldn't replace move semantics here in terms of avoiding copies.