Irenic language comparisons: Ada and Rust, AoC overall

Building on @JeremyGrosser’s invitation and an earlier, problem-specific comparison I made here, I’ve written up a more general comparison*** that compares four or five common constructions, depending on how you count: file iteration and exception handling, modularity and generics, enumerations, filtered and enumerated iteration. If that sort of thing interests you and you found the previous discussion useful, feel free to read it, and I’d be happy to receive any corrections or comments.

Otherwise I make no claim to suitability or usefulness. The writeup is longer and wordier than I expected and wanted, even after a significant pruning. Still, I learned a few things in the making of it, including a few surprises.

***ironically filenamed More_Detailed_Comparison but I don’t think it is in fact detailed

7 Likes

Er…what??

is there a link missing or something?

Whoops. Don’t know how I missed that. Fixed, & thanks.

1 Like

Eek (the cat), that is long.

Looks pretty interesting!

One correction: Expression functions were added in Ada2012 (The original release, before the 2015 update to 2012). (See side note at bottom of the post)

Also a question, you list Spark/Ada not having optional or result values, but I would have expected at least an “available with library” since it really just requires a variant record in Ada/Spark to implement? I get that it isn’t a 1st class type like it is in Rust. Maybe some clarification?

On the table or run time results: it might be good to at least list the switches you used for both Ada and Rust. In particular for Ada they make a huge difference because they can affect things like container performance which is usually a bottleneck in large data manipulation.

SIDE NOTE: You can generally figure out when a feature was added to the language by looking at the “slash numbers” in the RM. I know it isn’t available at the ARG website right now, but I keep a local copy (I have trouble reading the version stored here at ada-lang.io). The ending /1 /2 /3 /4 /5 all have specific Ada revisions associated with them so you can scan a section and look for the earliest slash number to get an idea when something was added. In particular a quote from the RM:

" Amendment 1" for the /2 refers to Ada 2005

2 Likes

Yeah… :weary: I know, and I kept asking myself how to shorten it. I didn’t want to remove the “language overviews”, though. You should see what I took out. Probably would have been twice as long.

What do you mean by “the cat”?

Dang! I kenw that, too! Somehow I got the two mixed up.

To be honest, I added that :open_book: icon for “library” availability primarily for Rust, and didn’t think about Ada / Spark originally. I only added the link to @mosteo 's iterators crate this morning, shortly before committing!

So I could see a new icon for “things relatively easily implemented in the other language”. But I’m not sure. There’s a lot more to rust’s Option and Result types; I mean more how they are used in error and data handling, where Ada would use exceptions. In other words, they induce a certain approach, which I happen to mention later. And with that comes a wide range of methods associated with the type that, yes, you could implement in Ada, but now you’re talking about a lot more work.

It might be better to place them both in a separate category, something like “idiomatic error handling with types”. Not sure I like that either, though. I’m very open to suggestions.

I always wondered what that was about. Thanks! :grin:

@jere I forgot to reply to this:

I thought I had mentioned how those were built; I know at one point I was :warning: warning :warning: that time comparisons weren’t that great because of differences in compilers, and I couldn’t control what the compilers did, and that did start me on an investigation of activating/deactivating switches, which is how I discovered that the Rust code for Day 23 was exiting with an error message whenever I enabled overflow checks in --release mode.

I hope you don’t mind that tangent. To answer your question: Unless specified otherwise,

  • “development” means whatever the standard alire or cargo switches are
  • “release” means alire build --release and cargo build --release

I think I would prefer to say just that, but if you prefer I’ll look at the configuration files that alire and cargo create and list those details.

The stupid reply limit, so added that in brackets. Do a search for eek the cat.

1 Like

It’s meant to be humorous (and to fill space in the reply).
For reference: Eek! The Cat - Wikipedia

Definitely. You had specifically sectioning out pattern matching so I figured you were lumping the other stuff in with that based on the examples. Maybe just a blurb that it can be implemented via variant records or something. Either way, not a complaint. I enjoyed reading through it.

Something to consider here. Since you are doing a performance assessment and Alire’s defaults are subject to change, it may be a good idea to list out the switches somewhere for those less familiar with alire and to also cover you in case Alire decides to add/remove switches later in life. Also, I don’t know if Alire turns off tampering checks and such in release mode off hand?

Very interesting!
In the grid, what kind of “macro programming” can’t you do in Ada ?
I would add a row about OOP and inheritance (the Rust doc says it can’t do that).

Can you do any kind of macro programming in Ada? I thought Ada was one of those “we don’t need no stinkin’ macros” languages, which I sympathize with sometimes, as in, when I’m trying to make sense of some stinkin’ Rust macro.

I don’t mean generics, by the way, as I mentioned those elsewhere. I mean code that lets you manipulate the abstract syntax tree at compile time, modifying the code then, usually with the aim of saving a bunch of boilerplate, but sometimes making it impossible even for the IDE to grok the flow.

Nope

Probably we think to different kind of macros.
Of course Ada’s generics are not a simple text substitution, but (unless you have shared generics) it actually works like that in the end. The compiler will check on the way that you don’t put a type in the place of a function, and so on, so you can check the generic unit before even instantiating it and be confident that most instantiations will work.
The RM says: “Generic units can be used to perform the role that macros sometimes play in other languages”

1 Like

Yeah, the macros above are a meta-programming construct, which I think Ada should have.

There are three ways that you can do something akin to optional types in Ada:

  1. An instance of Ada.Containers.Indefinite_Holders; [Ada2012]
  2. Variant records; [Ada83]
  3. Arrays, unconstrained; [Ada83]

This last one is really quire trivial:

Subtype Optional_Index is Boolean range True..True;
Type Optional is array (Optional Index range <>) of ITEM_TYPE;
None : Optional Constant := ( True..False => <> );

-- The Item returned by EXAMPLE is an array of length 0 or 1.
Function Example return Optional;

The only time you can’t use #3 is when you have discriminants (because discriminant → unknown-size).

You also cannot use #3 if you are storing results in an array since Optional is an unconstrained type.

?
It’s perfectly legitimate to say:

   Constant_Value : Constant Optional := Example;
-- OR, BETTER:
   Renamed_Value : Optional renames Example;

Or do you mean X : Array (positive range <>) of Optional:= WHATEVER?

If you really need that sort of construct, consider what you’re really doing, and if you need it, use Ada.Containers,Indefinite_Vectors instantiated w/ Option.

Generally the place where I really consider using optional is in environments where I don’t always have heap allocation or exceptions (small embedded platforms).

I do consider what I am really doing. I think hard about it.

I feel like programming in Ada is a lot about learning to represent your design in code in a way that makes the most sense. Things that lean towards optional results really do lean more towards variant records for me. Sure using an array works in some situations but it feels like a pretty unintuitive way to implement it for me if someone else is reading your code (I’m not saying you are wrong for suggesting it, just talking about my own preference). I much prefer the variant record approach, but I think each of us can pick what we like best out of the options.

1 Like

Technically GNAT provides metaprogramming already, several compilers used to, I don’t know if it is still the case.
There are several ways, some innocent (conditional branches being hardcoded and the alternatives not compiled at all when depending on a static boolean expression, switching bodies depending on the machine or compiler switches) or not (fully on metaprogramming).
The latter is an old way of thinking, like worrying about your variable going in memory or registers. It’s a compiler/automated tools’s job to find and discard conditionally dead code.
The rest is good design and profiting from these capacities of separate compilation no other language has, to their fullest extent

Interesting.
Why is that?
I mean, returning a variable-length array is idiomatic Ada. And there’s nothing too special about an array-length 0… so, having a variable-length array with an index which imposes a maximum of 1 seems the natural way of implementing much of what you’d want to use Optional for… plus, unconstrained-arrays offers a way to naturally express the notion of operating on sets/collections of that element-type.

Variant records map well to the domains of HW-interface and protocol[-packet] definition. They also can be used to interface and “hoist” the ‘parameters’ of the type. e.g.

Type Message(Length : Natural) is record
   Text : String(1..Length):= (others => ' ');
end record;

Type Style in (Plain, Extra);

Type Packet(Length: Positive; Packet_Type : Style) is record;
   Text : Message(Length);
   case Packet_Type is
      when Extra => Data : Integer;
      when Plain => Null;
   end case;
end record;