Irenic comparison of Ada and Rust based on the Advent of Code

Ordinarily I use Ada for the Advent of Code, but some Ada users (well, at least one) were interested in comparing to Rust, which I use at work, so I translated my Ada solutions to Rust. Subsequently I wrote up a comparison on the two. There's no attempt to trash either language; I use and like both.

If language comparisons interest you, and/or you've heard about Ada and are curious what Ada code looks like when compared to Rust code, etc., please do take a look at it. It's long, sorry, perhaps too verbose (heh, some say that about Ada, so perhaps it rubbed off), but I was trying to be careful. If you find any errors I'd be grateful for corrections; Ada people pointed out a few errors in earlier drafts.

I apologize if this isn't desired (moderator may delete!). I've seen that Ada comes up in this forum every now and again, admittedly rarely, but I thought it might interest some.

7 Likes

Here are the things I noted while reading. Or more honestly, skimmed.

array indexing by any discrete type [Rust: No]

After getting to the enumeration section, I don't think this is what you meant, but you can impl Index<LocalType> for ForeignType, including when ForeignType is an array, slice, or Vec<_>.

More notes from the enumeration section:

You can derive Ord and PartialOrd for ordering.

You can use specific discriminant types and values.

Rust's async requires a non-standard library

This is only true in a practical sense.[1] tokio and the other async frameworks aren't extensions to the language and build on std traits and data structures. You could roll your own.

functional purity [Rust: Yes]

I guess you mean const fn for Rust? They're not exactly the same thing. Depending on your definitions I suppose.

i may catch grief for this, but at least in safe Rust, all variables must be local to a function, while Ada allows global variables in a compilation unit, such as a package, and Rust doesn't allow even module-level globals

You can have statics. Mutating them requires synchronization or unsafe, but you can have them.

Perhaps it's not the same as what you want though.

Neither can you index your way into a String.

You can, with ranges. In real code you generally don't want to: it will panic if you hit a non-UTF8 boundary. In AoC type code you probably can because "try it in your language of choice!" exercises typically limit themselves to ASCII inputs, don't give you invalid inputs, etc.

You could work with bytes instead, though it's less ergonomic to output (especially to StdOut/StdErr). I'd probably take this route over chars for the given setting.

Alternatively, there are some some third party crates for byte strings (bstr), and a std ASCII type in also in the making.

Rust calls them modules, but these are so often distributed via the cargo tool, which calls its packages "crates", that in my experience the term "crate" is used instead.

A crate consists of one or more module, which make up a namespace heirarchy within the crate. Cargo also distinguishes between packages and crates. More here.


  1. And maybe someone will argue it's truer than what I'm saying. ↩︎

10 Likes

I've got some thoughts as well:

  • Rust 2021

That's the edition, Rust versions are something like 1.81.0. You should probably put both the edition and version here. Lots of changes happen within a single edition.

That said, Rust's programming culture has generally adopted the idiom; Option and Result types are in such widespread use that I can't remember seeing a crate that lacks them.

My best guess for most popular crate that doesn't use Option or Result is pollster. It's only 133 lines.

I agree that AoC and Rust strings don't mix well. I use Vec<u8> and a lot of b"" and b'', but this isn't great either. I don't think this is a huge problem in general, since it's really only because AoC uses ASCII text with column significance.

One Rust-specific thing I use a lot of in AoC are named loops and blocks. It helps a lot for breaking out of the 2D loops.

2 Likes

Thanks to everyone who replied.

I appreciate even the skim. It's long. I've spent a long time on it, even trying to get it shorter, and not succeeded to my satisfaction. I do appreciate the feedback.

After getting to the enumeration section, I don't think this is what you meant, but you can impl Index<LocalType> for ForeignType, including when ForeignType is an array, slice, or Vec<_>.

I could swear I've tried to implement that for an array and gotten the foreign-type error. In any case, you're right the other way; that's not really what I meant. I meant that you get it "for free". That is, I don't have to manually impl it.

You can derive Ord and PartialOrd for ordering.

Agh, I knew that! and I meant to add that! Thanks for point it out; I've added it to my local copy & will update soon.

You can use specific discriminant types and values.

I think I knew this once and forgot, but I'm not sure how it's relevant to what I was getting at then. Is it something important enough & commonly enough used that I should include it? I honestly don't remember when I last saw it.

Rust's async requires a non-standard library

This is only true in a practical sense.[1]

Yeah, I'm with the caveat in that footnote, unless you can direct me to some examples of how it's both used and relatively easy to use. My impression (sorry if this is completely wrong) is that Rust is still working toward putting something for this in the standard library (perhaps tokio).

functional purity [Rust: Yes]

I guess you mean const fn for Rust? They're not exactly the same thing. Depending on your definitions I suppose.

That's not what I mean. You're referring to this:

whether you can specify a function has side effects only via an opt-in mechanism

I mean that parameters and/or global state can't be modified. In Rust, you have to explicitly denote mutable parameters with &mut. That's the opt-in. Correct me if I'm wrong, but you don't have global state available at all, so you can't modify that.

Modern Ada has a similar in out for functions, but you can modify global state from a function. (er... I think. I better check that. Thanks for making me think about this.) Ada 83 didn't allow even that. Spark doesn't allow global state. (Again: er... I think)

You can have statics. Mutating them requires synchronization or unsafe, but you can have them.

Sigh. I knew that, and I meant mutable. Were I to try and save face, I'd point to the use of "variable", but in all honesty I forgot about that, which is embarrassing because I've used lazy_static before. I'll reword it like this:

mutable global variables ... in safe Rust, all mutable variables must be local to a function, while Ada allows mutable global variables in a compilation unit; Rust does allow immutable / constant static values

Does that seem more accurate? Either way, thanks.

You can, with ranges.

OK, but I was talking about direct indexing; i.e., my_string[i], as in the code excerpt I gave, which the compiler rejects. Or do I misunderstand you?

A crate consists of one or more module, which make up a namespace heirarchy within the crate. Cargo also distinguishes between packages and crates.

Thanks; I was unaware (or had forgotten) that cargo distinguishes between packages and crate. Besides that, comparing what I wrote to what you pointed out, it's clear I botched the wording. Does this seem acceptable?

Rust calls them modules, which are collected into "crates". A project typically has a "workspace" of many crates.

Thanks again.

1 Like

Thank you. I've been working on this for so many months that it's gone through several versions, but I'll add the version I'm using now.

My best guess for most popular crate that doesn't use Option or Result is pollster

Don't think I'll mention that, because I've never heard of that before now, which more or less validates my point. :grin: Besides, I think Option and Result are great things. (I hope that was clear, even though I try to avoid being subjective.)

One Rust-specific thing I use a lot of in AoC are named loops and blocks. It helps a lot for breaking out of the 2D loops.

Oooh, I like that! It won't distinguish it from Ada, which has those, too. But I do like it, so I'll add it, thanks!

Last I looked at Ada you had to purchase a license to get a compiler (Mac). Is that still the case?

Gnat is part of gcc and should be available. I see that you ask about Mac; I think that's been a little more difficult since Apple moved to their custom ARM, but it's (community?) supported and free.

1 Like

I don't know Ada so I'm not completely sure what you were getting at :slight_smile:.[1] But you can do this for example:

    // See https://doc.rust-lang.org/nightly/reference/items/enumerations.html#implicit-discriminants
    #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)]
    pub enum Direction {
        North,
        South,
        East,
        West,
    }

    pub const ALL_DIRECTIONS: [Direction; 4] = [
        Direction::North,
        Direction::South,
        Direction::East,
        Direction::West,
    ];

    fn elsewhere(idx: Direction) {
        println!("{:?}", ALL_DIRECTIONS[idx as usize]);
    }

I'm not sure how common it is.

If it's tokio or anything else opinionated and/or full-featured became part of std, it'd probably kill off the alternatives. I don't actually know what the current thinking is, but allowing different async runtimes at least used to be considered important.

Which is to say, it's true there's no async runtime in std, and I'm not sure it is even something that is being worked toward.

Anyway, that's sort of tangential to what I meant. async can be used without a third-party runtime and that's important to some people (e.g. in the embedded world), from what I understand, and the foundational parts of async are part of the language.

But as far as what most people mean from the word async, your current wording/interpretation is accurate; you need something like tokio to provide a useful runtime.

You can have shared mutability, which allows mutation without &mut _ (or mut bindings),[2] including in globals.[3]

Example.

Shared mutability backs a lot of types: atomics, Rc, Arc, Cell, RefCell, Mutex, etc. OS handles such as File can also be considered a form of shared mutability, which is why &File can implement Read, say.

If you don't know a specific type and all its implementations,[4] for example if you have a generic type parameter, you can't rule out shared mutability.

If you needed something more complicated in a global, you'd typically stick it in a Mutex or such. std uses a reentrant mutex for Stdout and the like, for example.[5]

We do have static muts, although they're being deprecated to some extent. They do require unsafe to use.

You can have mutable globals via interior mutability such as in my example above, without unsafe.

Correct, we don't have that. str is UTF-8 encoded but the indices for ranges are in terms of bytes. Indexing a non-range would have to return a &u8, which would be surprising (just use as_bytes[6] if that's what you want), or a &str to the full codepoint, which would be a range of dynamically determined length. Or something else even more surprising.

I don't recall the historical reason why we didn't get the latter. Most arguments I can think of could also be leveraged against chars, and we have those. (If you want to do proper unicode segmentation, you need an external crate.)

Anyway, for trusted ASCII input like most online excercises, you could my_string[i..i+1] (but again I wouldn't recommend it for most real life code).

Eh... a crate is a library or binary, which may be organized into multiple modules.

I think it's hard to be succinct here due in part to the use case. In a well structured environment, the unit of a reusable collection of code is ideally a library crate that you can use as a dependency elsewhere. Each such crate may have been organized into multiple modules (and may also have its own dependencies, etc). When you want to reuse it, you add it as a dependency, and Cargo pulls it in for you.

In an AOC or online competitive environment, however, I imagine people just have a collection of stand-alone or mostly stand-alone files (modules) that could be crates, but aren't maintained as such. Why? Because actual dependencies usually aren't a thing in those environments (or are restricted to a known fixed set of industry standard dependencies). Then (my guess continues), they just copy a given module file into day17/src if they need to, and add the corresponding mod file; line to lib.rs or whatever. They cp instead of copy-paste, which is relatively nice, but they don't use proper dependencies.

The best wording for your blog post probably depends on if you want to target the AOC-like use case specifically or not.

(Workspaces are yet another layer. I doubt you want to cover all the details of Cargo package management in your blog post, whichever way you go on the modules vs crates wording.)


  1. I couldn't tell you the implications of type Destinations is array (Direction) of Location; say. ↩︎

  2. mut bindings only prevent overwriting and taking a &mut, not moving; temporaries do not have that restriction, so the absence of mut bindings may mean less than you think even without shared mutability ↩︎

  3. Here's an article presenting the main dichotomy in Rust as shared vs. exclusive, in contrast with immutable versus mutable; it may clear up some common misunderstandings. ↩︎

  4. it's not enough to know their fields since a type can utilize globals or I/O in its implementations ↩︎

  5. The global allocator also requires global shared mutability, effectively. ↩︎

  6. or Vec<u8> ↩︎

AoC is unique in that you don't have to submit code, only the output, so you can use whatever dependencies you want. The competition aspect exists but is less emphasized.

I've seen all kinds of different organizational schemes for Rust+AoC. The common ones are one package per day (each with one binary) usually in a workspace, and one module per day in one binary. There's also one file per day using cargo-script/rustc/playground, one binary per day in one package, two binaries per day (each day has two parts), or the cursed overwrite-one-file-every-day and let git remember. These are often accompanied by a shared library in the workspace, although copy-paste is common too. And then people have a bunch of different setups for downloading puzzle inputs, running the code, and sometimes automatically submitting answers.

2 Likes

Ah, thanks!

In that case @johnperry-math, present the well structured use case.

Sorry for the late reply.

From what I can tell, the only issue where you felt I was mistaken regarded global, mutable values. I agree that you're right. We don't normally use them at work, and I don't remember if I was ever aware of them. I've removed that column altogether from the feature comparison.

Please do let me know if I've overlooked anything else!

1 Like

Having used Ada for a short time back in the day and been involved in testing Ada software I find your comparison quite reasonable.

A minor niggle though:

  1. When I see tables of features that contain marks like this:

:heavy_check_mark: :heavy_check_mark: :x:

the immediate impression is that one language is lacking feature and that is a bad thing. I guess it comes from the days of doing homework where teachers red crosses were a bad sign.

Were as of course many would argue, as do I, that not having exceptions and inheritance were a positive feature.

That's a good point. I prefer Rust's approach to error handling (which is why I list it in the row immediately below, and dwell on it a bit in the writeup), and I know at least one Ada user who detests inheritance

I don't like any of my ideas, though:

has it doesn't have it note
:heavy_check_mark: :x: current
:green_circle: :red_circle: even red can be interpreted as "bad"
:green_circle: :yellow_circle: not sure that's an improvement! :grin:
:green_circle: or :heavy_check_mark: (nothing) possible, but seems as if something's missing

What would you suggest?

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.