Why do you keep using Ada?

I agree as someone who himself has soured on generics.
I’d make a distinction though;
Generic subprograms are not a big deal.
The argument against generics is the massive damage they do to the package system by imposing certain requirements and limitations that need work-arounds compared to the elegance of non-generic packages.

It is possible that the next edition of the Ada standard will clean up this mess.

I doubt it. The language simply lacks array abstraction needed to express encoding and character views.

It would help to move all useless stuff into some obscure annex leaving just String and Unbounded_String visible.

An implicit conversion of wide Character/String literals to String (UTF-8 encoded) would help too, because this is de facto standard across all languages but Ada.

Hu? I’ve worked professionally (also safety critical sw) for more than two decades with Ada and never felt a damage.

It’s hard for me to gather all the annoyances on the fly as I don’t write these encounters down.
Perhaps I should start doing that.

Some quick ones:

  • impossible to instantiate a generic package as (a child of) another generic package
  • impossible to specialize a generic child with an actual type (so that it has access to the parent’s private part)
  • impossible to “re-export” an instantiation (something like use all package Pkg_Instance;), which splits package facilities into different namespaces

There’s also a limited generic formal parameter type set (scalar type, record type, …), forcing the use of type ... is private.
On top of that, I’ve encountered GNAT bugs when mixing type derivation and generics on top of other frictions I cannot think of right now.
Some error messages when generics are involved are cryptic in GNAT.

I understand there are reason for why things are this way, and I’ve read discussions on these topics on usenet.
I’d risk saying that Ada’s generics suffer the same as Ada’s OOP facilities - for whatever reasons they don’t go far enough.

generic
package GA is
end GA;
--
with GA;
package A is new GA;
--
generic
package GI is
end GI;
--
with GI;
package A.I is new GI;
--
with A.I;
procedure Main is
begin
  null;
end Main;

This compiles fine. Is it that what you mean? Or something else? It’s hard to extract the intention from your sketch.

Just for the other (pseudo) examples - can you please be more specific.

This is because the generic formal parameters present to the generic’s body the set of operations that it can guarantee thus Type X(<>) is limited private; is able to accept any type, and can be read as “Any possibly-discriminated, possibly-limited, possibly-private type”, where as type T (<>) is abstract tagged private; is “Any possibly-discriminated, possibly-abstract, possibly-private type which is tagged”, and so on.

The key feature to remember about generics (in this manner) is that the specification’s formal parameters is a sort of double interface:

  • For instantiation, they show what can be supplied to the parameter;
  • For implementation, that is the body of the generic, they show the properties which may be relied upon.
    e.g If it’s a discrete type for the parameter, then you have T'Pred and T'First; if it’s a private type, you cannot rely on any of that.

There is no user defined formal generic types. Everything is hard coded and matched by structure rather than by name. It is better than untyped textual substitution, but not yet on the level of Ada 83 type system.

That is the reason why formal subprograms need to be. Otherwise naming a formal generic ADT would also specify its full interface without a huge trail of formal generic subprograms.

There is just one interface. Generic interfaces are no different from any other. Any interface lists properties the implementation (here the actual) must provide and the client (here the generic body) may consequently rely on.

I started learning it when I was 19 (more than a decade ago).

I love her syntax and the focus on security and safety.
I’m a paranoid myself and I like when the tools I use take good care of me. :slight_smile:

Whilst the responses went into advanced topics. I think I know what you mean. It took me a few attempts before the basic pennies dropped. The basics are actually quite straight forward but I’m not sure there is a really simple explainer out there that removes the confusion instantly (sorry @onewingedshark). I’m fairly sure the peculiarity might be genius but it is confusing until you get it. I think the Barnes book might have two sections on Generics and it was a while before I came across the later section.

I don’t see what’s so difficult about generics unless you’re coming from weird arsed stuff like S/ML. I cam from C++ where templates are er messy and overly complex.

No, there are two: the generic specification interfaces between the using code (via instantiation), and also the implementation of the generic — this is to say:

  1. The spec acts as an interface to the outside world (the user’s codebase), via instantiation.
  2. The spec acts as an interface to the formal parameters, providing the properties of the types/operations/packages to the body.

Hm, what issues were in the ‘Explaining…’ document?
I tried to be both understandable & thorough in the explanation.

C++ is messy, and a huge part of that is that (a) there’s no ‘instantiation’, meaning that [combined with implicit type-casting] you get an explosion of combinatorics; and (b) because they’re Turing-complete they are fundamentally broken: you can define your entire problem in the template language… which is why they have to limit by recursion-depth and time (IIRC), as a meta-language rule, rather than something that is definable within the language. (I consider this as the template-version of the Dangling-Else: Dangling-Else only exists because of ambiguous [read ‘bad’, and ‘incorrect’] grammar. Just like if is ‘fixed’ in C by meta-language rules, so to is the error of C’s templates ‘fixed’ by meta-language rules.)

I’m not overly familiar with SML, but what I did read of it seemed to be decently sensible: define it as an ‘interface’ and let anything that implements it be a ‘body’ (IIUC). Perhaps a bit too general for some of the constraints Ada provides, but it is at least uniform.

That is no interface proper, only an instance of becomes an interface, which is the core problem with having any form of preprocessing. Macro/template/generic is a meta language operating on the core language as opposed to operating on the problem space.

I came from ML/Swift. That is my JAM.

I am deeply offended but I also want to pet that cat so I will ignore it.

Also I did not come from C++. Closest is D actually. But when I think of generics, I admit I default to an ML/Modern-ish ad-hoc model. Specifically from an OCaml flavor or sometimes Swift/Rust.

All that said, since the last reply, I have grown to get a bit more comfortable. Right now I am experimenting with the various indefinite types and holder packages. It is just a culture shift.

Can I add one more, somewhat unrelated thing to the question above from the OP? I actually really like GPRBuild not just for building Ada but just as a build system. As someone who came from higher level languages, I dislike a bit the fragmentation C/C++ builds suffer. Because like, I only cmake because everyone else does and I follow the build instructions for SDL and just pray.

I find myself digging GPRBuild. I also am kind of a zoomer so I don’t use Make either. I think if GPRBuild did not exist, I would probably default to Meson.

So yeah, I keep using Ada because GPRBuild is probably the most transparent build tool I Have tried and it just “clicks”. I am interested in writing compiler drivers for C3 with it lol

Did the Explaining Ada’s Generics paper help?
If so, how? If not, why not?
(I’m eager to get reviews/assessments of it.)

My biggest issue with GPRBuild is that it is a “stringly-typed” language — IMO, it would be much nicer if it were strongly typed and, if possible, reused the compiler itself.

My background is scientific R&D with a few interludes working as a software engineer. Lots of computer vision, machine learning, statistical analysis, data modeling, and system design. Functional programming and denotational thinking has probably been the most useful tool for me over the years.

Last year I was handed a greenfield R&D project to develop a firm real-time system for processing data streams from a variety of different sensors. Low latency, mathematical integrity, and correctness were very important, as the outputs of the system (especially the precise times of detected phenomena) carry critical scientific implications.

Reliability was very important. During an experiment, the system has only one chance to get it right. Having to re-run an experiment because the software didn’t work or crashed would be wasteful in terms of time and money, and would make the system unfit for use.

The hardware for the system had to be physically small and able to run on a sensible amount of power due to installation environment constraints. The system had to be capable of using sensor APIs and numerical analysis libraries written in C/C++. Timely and safe concurrency was critical.

Evolvability and maintainability was really important. In an R&D project like this, it’s essential to be able to incrementally construct a system and safely refactor it to accommodate large changes as the requirements evolve.

I decided to give Ada a try for this project. What a great decision that was! Ada and its excellent tools check all of the boxes for the project requirements. Using Ada made it possible for me to design and develop a working system with all of the high risk functionality implemented in a matter of just a few months. Way ahead of schedule, as the Ada language, runtime, and tooling circumnavigated so many of the usual pitfalls. Much of the usual defensive programming and difficult reasoning about system invariants, concurrency, and timeliness disappeared completely.

Highlights of the great things about Ada:

  • C/C++ interop: Amazing. Smoothest experience I’ve ever had. Wow!
  • Documentation: Excellent. Thank you AdaCore et al., it’s great. Really good, professionally written books are available. The forum is a great source of information. Abundant open source examples to learn best practices.
  • Tasking: Best in class. Absolutely amazing work. Restricted profiles (Ravenscar/Jorvik) is such a great idea. Ada tasking has eliminated almost all of the potential pitfalls of my project by removing all of the complexity of highly concurrent time constrained computing.
  • Type system: Fantastic. Things break down a bit when we have to think about when a particular constraint or type predicate is applied because it cannot be determined statically. Unpleasant surprises at runtime are icky. Tools like SPARK help here, effectively endowing Ada with liquid types and much more.
  • Expressiveness: Very good. It is phenomenal how easy it is to write extremely low level code, while still expressing fairly abstract concepts. The abstraction bit has a ceiling, as usual - I can say some things in Haskell that I can’t say in Ada, but the converse is true as well. The gamut of expressiveness is incredible.
  • Tooling: Great, and continually improving. I was so grateful to be able to use Alire to build and manage my projects, and not have to dive into complicated polyglot tooling. It just works. This alone saved a huge amount of time and effort.
  • Safety: Excellent. Memory management, very good static analysis, and the whole language is designed to ensure that a system is free of many classes of common errors. I sleep well knowing that my system will work as expected.

As someone who takes full advantage of powerful type systems such as those in Haskell, Scala, Lean, etc., Ada has been great. The basics are very easy to learn, and the remaining learning curve is manageable.

There are a small number of pain points from my perspective. Not show stoppers by any means. Some aspect of these are probably because Ada is new to me, and I am trying to bend it into something that it is not:

  • Generics: Coming from Haskell/Scala/Lean, having to instantiate a generic package for every combination of concrete types in Ada is a big downer and limits their expressive power and potential elegance. Maybe this will help, but probably won’t go all the way to the ergonomic satisfaction seen in Haskell etc. There are pluses and minuses to Ada’s design of generics, and a long history of real world use cases probably supports those design decisions.
  • Source Code Files: In an R&D workflow, it’s very useful to quickly spike out a new idea in a system. Having to bend over backwards to jointly cultivate “.ads” and “.adb” files, and insert stubs to keep the compiler happy slows things down. That said, I understand and respect why things are the way they are. Maybe I need a different workflow using gnatstub/gnatchop.
  • Strings: Would be nice to have out-of-the-box modern string handling.
  • JSON: Again, out-of-the-box JSON supporting Unicode with good ergonomics would be nice to have.
  • Rapid Prototyping Ergonomics: When spiking something out, I don’t want to have to think too much about concerns such as memory management or whether my files/packages/etc are all ready to be immortalized. Unfortunately, the rigmarole for things like this get in the way of expressing the what that I am trying to do (denotational), and forces me to divert time and brain power into managing the how (operational). Based on massive development time savings elsewhere, this is an ok tradeoff.
  • SPARK: Fantastic auto-active verification, but you have to know how to play the game. To play the game, there is a certain amount of metis and tacit knowledge needed. You can’t just read books and fiddle - you need to know what the proof automation is trying to do, how it works under the hood at some level, what it may choke on, whether your config files need to be twiddled, etc. Auto-active verification always has limitations, so easily dropping into an interactive proof mode (Rocq) is a very nice escape hatch, though I haven’t used it yet. If I had a magic wand, I’d make a tool for SPARK to start any compatible interactive theorem prover (ITP) to discharge stubborn VCs, to directly see the proof context, and use the ITP’s ecosystem to its fullest extent to get the job done. Maybe something like Velvet could serve as an example of what is possible in a general sense.
  • Non standard terminology: Some of the terminology used to describe the language context seems non-standard, possibly due to historical happenstance. Might be helpful to have a Rosetta Stone somewhere that shows the Ada term for a thing, and the term that’s considered more modern. I should come up with concrete examples to support this claim. For example, what’s a “nonlimited definite type?” I think some of the Ada OOP terminology may be considered idiosyncratic by some people, but I don’t use many OOP features right now.

It is incredible to get an opportunity to use this language and ecosystem. I will certainly use it again, and I actively look for opportunities to apply it.

Instantiating generics immediately reminded me of using functors in OCaml (e.g. to make Set types), and Ada packages remind me of modules in Ocaml.

No, because “modern string handling” is wasting resources and error-prone. It is very helpful to treat strings fixed-length and immutable in general.

There many Ada JSON parsers around. For example this. Unicode is supported.

A type with assignment that you can pass down in mutable mode being able to change all of it.

I am afraid you will not find a “modern” equivalent just because “modern” languages have no such thing at all. Assignment is not considered an operation associated with a class of types. Mutable parameters are non-existent, they use explicit references instead. Variable (unconstrained) types are non-existent too. Instead of these an opaque pointer is used with operations defined to access it.

Yes, since OO is not consistently typed except in Ada. It is difficult when the word “class” means a dozen completely different things at once: “type”, “set of types”, “closure of a set of types”, “declarative region” etc. I think Ada did it fine to have a distinct word for each. But for those accustomed to sloppy language it may appear strange.

Yeah, this is a big issue, but there are really three parts to consider, as they impact the ‘why’:

  1. Some ‘non-standard’ terminology comes from before there was a standard terminology. One good example is OOP, in the 80’s Grady Booch put out a book wherein he spoke of objects “suffering an operation”, which in modern parlance would be “invoking a method”.
  2. Some, like your given “nonlimited definite type”, are technical descriptions of things that typically don’t appear [directly] in other languages
    1. Nonlimited: has an implementation provided assignment operator. This is in contrast to Limited, which does not. [Limited types are useful for (a) modeling/interfacing to that which cannot be copied, like a HW-clock, and (b) controlling an object’s ‘ownership’.]
    2. Definite: (Informally) A type where the ‘shape’ and ‘size’ is known, such that you may construct an array of elements of that type [and use offsets because of known [uniform] size].
      Formally, as per http://ada-auth.org/standards/12rm/html/RM-3-3.html#I1468
      At the place where a view of an object is defined, a nominal subtype is associated with the view. The object’s actual subtype (that is, its subtype) can be more restrictive than the nominal subtype of the view; it always is if the nominal subtype is an indefinite subtype. A subtype is an indefinite subtype if it is an unconstrained array subtype, or if it has unknown discriminants or unconstrained discriminants without defaults (see 3.7); otherwise, the subtype is a definite subtype (all elementary subtypes are definite subtypes). A class-wide subtype is defined to have unknown discriminants, and is therefore an indefinite subtype. An indefinite subtype does not by itself provide enough information to create an object; an additional constraint or explicit initialization expression is necessary (see 3.3.1). A component cannot have an indefinite nominal subtype.
  3. The Ada terminology is, thankfully, standardized — that is, in the standard.