Ubuntu: not GNU anymore. An opportunity for Ada/SPARK?

A simple example to demonstrate “Why Rust over Ada?”:

Input file:

an example file
🚀
🐮
café

Ada:

with Ada.Text_IO;

procedure Example is
   In_File : Ada.Text_IO.File_Type;
begin
   Ada.Text_IO.Open
     (File => In_File,
      Mode => Ada.Text_IO.In_File,
      Name => "example.txt",
      Form => "WCEM=8");
   while not Ada.Text_IO.End_Of_File (In_File) loop
      declare
         Line : constant String := Ada.Text_IO.Get_Line (In_File);
      begin
         Ada.Text_IO.Put_Line (Line);
      end;
   end loop;
   Ada.Text_IO.Close (In_File);
end Example;

Ada Output (Windows):

$ alr run

Note: Building example=0.1.0-dev/example.gpr...
Compile
   [Ada]          example.adb
Bind
   [gprbind]      example.bexch
   [Ada]          example.ali
Link
   [link]         example.adb
Success: Build finished successfully in 1.11 seconds.
an example file
🚀
🐮
café

Ada Output (Linux):

$ alr run

ⓘ Building example=0.1.0-dev/example.gpr...
Compile
   [Ada]          example.adb
Bind
   [gprbind]      example.bexch
   [Ada]          example.ali
Link
   [link]         example.adb
✓ Build finished successfully in 0.67 seconds.
an example file
ð
ð®
café

Rust:

use std::fs::read_to_string;

fn main() {
    for line in read_to_string("example.txt").unwrap().lines() {
        println!("{}", line)
    }
}

Rust Output (on Windows and Linux):

$ cargo run

   Compiling emojis v0.1.0 (D:\dev\rust\strings)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.45s
     Running `target\debug\strings.exe`
an example file
🚀
🐮
café
2 Likes

Curiously enough, the Ada program produces the correct output for me on Fedora:

$ ./example 
an example file
🚀
🐮
café

I run Wayland though, and suspect something there is doing the lifting.

While I consider the Ada’s string situation regrettable, I don’t see it as the primary obstacle to writing software. A cleanup of string-handling is necessary and long overdue, but there are bigger challanges.

Oh, an by the way, shouldn’t there be some standard-library handle that does Text_IO.Open / Text_IO.Close for you? I distinctly remember watching a presentation last year about this very issue. Why aren’t such boilerplate procedures automated by the library.

This is how you do it in Ada:

with Ada.Text_IO; use Ada.Text_IO;
procedure Example is
   File   : File_Type;
begin
   Open (File, In_File, "example.txt");
   loop
      Put_Line (Get_Line (File));
   end loop;
exception
   when End_Error =>
      null;
end Example;

Output (Debian):

an example file
🚀
🐮
café

P.S. Rust example is just as atrocious as I expected from Rust. It totally unreadable. What the hell is unwrap. Why do I need some lines when a line is already read. And finally a proper way to deal with raw files is Stream_IO:

with Ada.Streams;              use Ada.Streams;
with Ada.Streams.Stream_IO;    use Ada.Streams.Stream_IO;
with Ada.Text_IO.Text_Streams; use Ada.Text_IO.Text_Streams;
procedure Example is
   File   : File_Type;
   Buffer : Stream_Element_Array (1..80);
   Last   : Stream_Element_Offset;
begin
   Open (File, In_File, "example.txt");
   loop
      Read (File, Buffer, Last);
      exit when Last < 1;
      Stream (Ada.Text_IO.Standard_Output).Write (Buffer (1..Last));
   end loop;
 end Example;
3 Likes

There is nothing to be done about it so long the type system does not support array interfaces. You cannot separate encoding (array of octets for UTF-8) from the content (array of code points).

But why the buzz? No other language can, no other language cares. Why Ada should? I repeat it again, just do as other languages do, String is an array of UTF-8 encoded octets. End of story.

1 Like

Alright, the above result for me is with gnatmake. I suspect that’s also what Dmitry used.
With alire I get:

$ alr run
ⓘ Building example=0.1.0-dev/example.gpr...
gprbuild: "example" up to date
✓ Build finished successfully in 0.32 seconds.
an example file
ð
ð®
café

Correct, I use native toolchain.

Rust example is just as atrocious as I expected from Rust. It totally unreadable.

I second that deeply.

By the way, we use here Alire and UXStrings from Pascal (Blady) and UTF-8 rendering is just perfect.

I’ll just chime in to say that I find the Rust example eminently readable, as would any Rust programmer. It’s not at all difficult to parse.

Especially the .unwrap. Of all people, Ada lovers should not start criticizing other languages’ specific idioms and jargon merely because they see something unfamiliar.

5 Likes

Just to be sure, those aren’t my words and while I think Rust has a lot of C-style arcane syntax, I have no trouble following it.

Hard to understand the repulsion above to this code.
This is basic iteration over a string that stores the contents of a whole file.
Rust has no exceptions and the result of read_to_string() is a Result algebraic data type that can be either a value or an error if I/O failure is encountered.
The unwrap() accesses the value.
In any case, it is clear to me what is happening.

This is indeed pretty Ada. I like it very much. No explicit resource management with Open/Close, new/delete, etc. is a must in a high-level language.

P.S. I tested your Streams version and it produces correct results irrespective of the build procedure, while @pyj version only outputs the correct contents when built with gnatmake and trips with alire.

Having said that, I would not bet in either’s favour (at a gunpoint, I’d reveal my soft spot for Ada though). With Ada you don’t avoid writing explicit machinery in the imperative style. With Rust you still have some of that (unwrap().lines()) but with evident declarative/functional influences. This is a very popular style nowadays getting adopted left and right in academic and industrial languages alike. It is very common and it has its merits like the said hiding of “how”.

Does unwrap used thay way imply that errors are not being checked and that if there was an error it will panic and terminate the program? As impressive as Rust is, the potential for ubiquitous panics is something that worries me about it. And The Rust Book warns (or did when I read it a long time ago) that some panics might not be able to be caught.

Yes, that’s exactly what that code does. In normal code you’d pattern-match against the Result instead of calling unwrap().

match read_to_string("example.txt") {
   Ok(contents) => {
      for line in contents.lines() {
         println!("{}", line);
      }
   }
   Err(e) => {
      // handle "e"
   }
}

That’s not how I’d put it. One feature of Rust’s Result type is that the .unwrap method unwraps an Ok result and panics on an Err. In other words, you understand a Result is coming (there’s no way for the compiler to guarantee that example.txt even exists, let alone has data) and either you believe it will be Ok, or you don’t care that it isn’t; that is, you accept the panic in the Err case, because the situation is so irredeemably bad that there’s nothing else to do. And that would be the case here: if the file doesn’t exist, there’s little point doing more than that.

The mark of an unserious dev team would be the existence of “ubiquitous” panics. I work on a project with with hundreds of thousands of lines of Rust code, and our team’s policy is to handle every error; .unwrap is allowed only in example programs. It is a Peer Review failure to use .unwrap or .expect. (.expect is a slightly less ungraceful .unwrap.)

I’m not quite sure what you’re referring to (it’s been a while since I’ve read it as well, and I think the critique of Rust’s unstable definition is dead on) but I just reviewed what The Rust Book says about panics and I think you misunderstand what they’re saying. No panic can be caught; that’s the point of a panic. A Result::Err can be caught.

2 Likes

Thanks.

I’m familiar with that idiom from OCaml.

As someone who spent many years with C++ and has spent the last 3 and a half working with OCaml, I find Rust code that I’ve seen fairly easy to read when it doesn’t involve lifetimes.

But I suspect it would be difficult for someone without some familiarity with similar languages.

Several years ago, after surveying some popular languages, I had decided to write an open source software prototype in Rust. And I wrote the design with Rust in mind and wrote the Rust interfaces, but I had the nagging feeling that it would be a hinderance to many in the target audience: social scientists and people in digital humanities (not only computer scientists). They might have only experience with Python (or maybe R). So, I changed course, and wrote it in Python. Now I’m thinking of writing the second version in Ada.

Ada strikes me as potentially easily readable by people with very limited programming familiarity (even though I can’t say I understood it at a glance when I first encountered it).

I believe that was actually one of the aims when it was developed. They wanted code reviews to be much easier to perform with people less familiar on coding but maybe more familiar with system requirements, so that they could participate with the peer reviews to help ensure the system was going to meet user needs and be correct from a high level perspective.

3 Likes

There is something called catch_unwind.

Not only that. Consider that algorithms are literally imperative instructions. Ada has very robust loop syntax that is perfect for implementing algorithms. It is a shame it has not been adopted by any of the popular algorithms books* and even greater shame Ada has no algorithmic section in the standard library. It blows my mind.

*(although I think Kleinberg/Tardos use a Pascal-like pseudo-language)

1 Like

I’ve found it very difficult to impress people with Ada, because the response is usually “well, obviously that’s what it’s doing.”

Yes, I did read somewhere that was one of its original aims. And I guess I meant to say that I personally find it seems to be fairly well likely to be able to achieve that. It is just one of the things that impresses me about Ada.

When I was reading about Rust, I found that it had everything that I had wished I could have in C++, but when I started reading about Ada, I found that it had things that I liked beyond what I knew I needed or wanted, particularly to do with facilitating code correctness and good software engineering practice.

2 Likes

Yes, I think that is fine. There are times when it is reasonable not to handle an exception too (though likely not in industrial production code). I’ve written quite a bit of academic code, and, for example, in a program I wrote in C++ to score the results of cross-validation of a machine learning experiment, I intentionally didn’t catch any exception so as to be sure (as much as I could be) that if it finished, the calculated result was accurate.

But there are many cases when one can do without the result. For example, if I am searching for a key in a key-value store, Python will treat not finding the key as an error and raise an exception, and from my memory, the only other interface it provides allows one to provide a default value. But not finding a key could be just fine. It is just another result, not an error. Or if I have producer-consumer code and I run out of memory, I just have to pause producing for a while.

Obviously, your team understands such things. The thing I worry about is an undocumented exception or (in the Rust case) panic in someone else’s library. For example, I used a Python library called RPyc in a Python prototype. It does Python to Python RPC. The server being unable to reach the client to update a change to the value referenced by a reference that is held both on the client and server was an undocumented cause of an exception that, unhandled, can terminate the server. But at least I was able to wrap the whole call to start the server with an exception handling block and restart it in the case of unexpected exceptions.

Obviously, a callee can’t (and shouldn’t try to) predict for the caller what is unrecoverable. So, if I can’t even handle a situation in which a callee panics, that worries me.

I was a unfair to Python in the key-value store example. One can check whether a key exists in a dictionary in another way to avoid the possibility of an exception.