Ada: a C Developer's Perspective
methodsandtools.com
methodsandtools.com
Second - many solutions/libraries are purely commercial. You need basic open protocols support by default /free. Otherwise such great language will still be at it's niche.
Rust is on a good path to became XXI century successful Ada :)
They each have their own ideas on what "safety" is, and how to address it. I'll admit I'm biased towards Ada, but I think it is pretty safe to say that "Rust safety" is a subset of "Ada safety" - but Rust does it's section and run further with it than Ada.
Rust is obsessed with memory safety. That's it's thing. Unsafe code aside, it's memory safe. And it tackles some concurrency issues too.
Ada is more concerned with overall correctness, particularly in terms of types, where the types are semantically meaningful. And it goes further and dips into memory safety and concurrency. It doesn't such an extreme tact as Rust, but it does an awful lot to provide good options that allow you to avoid many issues. And it generally tries to make programs easy to understand. And more - some of which, as the article notes, are really quite wonderful for going really low level.
Personally I see the most value in the semantic type safety. That's really been, in my own experience, where Ada has helped me the most. Many of the other features are also great. I just don't see almost any of it in Rust. You can sometimes get stronger guarantees about a few things and you kind of lose everything else.
How is that an alternative or a replacement? Yet this idea is everywhere.
When pitching Rust to others of course memory safety and thread safety get hyped a lot; because it's talking about something that most languages just can't do -- safety without compromise.
But actually ... memory safety is a small part of the reasons why I like Rust, and there are plenty of other cool bits in the language. It stands out a bit because it's different, but that doesn't make it the end-all goal.
But frankly, I doubt that you can. Others will say "operating system kernels", but people are happily writing OS kernels in OCaml, so <shrug>.
Yet others will say "safety-critical hard real-time tiny microcontrollers without an MMU" (one of Ada's domains), but on those systems you don't want to do dynamic allocation anyway, so the question of GC or not is kind of moot.
I've said it before, I'll say it again: Rust would be a better language if it had a GC and the possibility of marking certain program parts as GC-free (the way it allows you to mark certain parts as unsafe).
Here's PyPy's GC, implemented in Python: https://github.com/tycho/pypy/tree/master/rpython/memory/gc
(I haven't looked at it in detail.)
Anyway, even if you absolutely had to use a non-GC language to implement a GC, that would not preclude writing 99% of the browser in a GC language and only the GC in something else.
Also, why would implement GCs multiple times at different levels of abstraction in the stack, other than for something specific like PyPy which is specific like designing an interpreter to help test language features quickly?
But you're still wrong on this drop being necessary, I think. You can do it just fine in a language with GC that also allows you to allocate a block of memory that is not considered by the GC.
I think it's a bit disingenuous to say a "GC language compiled a GC language" when the compiler used non-GC portions of the language to do it. You're right, I should have said "A language which can work without a GC" instead of "a non-GC language;" but I think the spirit of my comment was clear.
That said I think what you said above is right, and non-GC code is really only necessary in the hot parts of an application (and applications that can't use dynamic allocation for whatever reason, but that's another discussion). Rust is even working on adding GC support [1] (albeit very slowly, as they should). That said non-GC languages (or at least the ability to write code that doesn't use a GC) is still necessary in many industries for many reasons; that isn't going to change for a long time, if ever.
[1]: https://manishearth.github.io/blog/2016/08/18/gc-support-in-...
Rust is a very nice language with many useful and interesting features which may exist because of the developers' focus on safety but can very much be marketed without calling out safety all the time, and especially without the hints, suggestion, and innuendo that using a language without Rust's safety model is somehow dirty, dangerous, or worse irresponsible or unethical.
Personally, I interpret the focus on memory safety as a response to "memory issues aren't a big deal" or "memory issues only come from incompetence" viewpoints.
Memory safety is not, itself, program correctness. It's just one way programs can go wrong.
Could you elaborate what you think is cool there ("other cool bits")? Serious question, I'd like to hear a personal opinion.
I think the reason I like the language is the type system which is like the pascal one on steroids and I love Pascal, it was the language that taught me you could reason about a program by building up from primitives.
Server-side stuff is a big focus of the upcoming year, so maybe then? :)
(I've been doing more of it lately and found it surprisingly pleasant)
I'd love to be able to use it at some point and writing a web framework on top of it has been one of those fun if I ever have lots of spare time projects that sits in my queue.
http://www.electronicdesign.com/industrial/rust-and-spark-so...
And I've seen that before. Frankly I thought it overly nice to Rust by not spending any time at all considering the differences in user-defined types. That's really one of the biggest and most important differences to me, and a place I think Rust really falls short.
I agree. It's just a weird thing about these discussions that makes it necessary to bring up. The online forums typically divide between "system" languages you can write OS's or low-level code in versus "application" languages you write anything else in. Performance comes into play. GC vs non-GC, too. The kinds of benefits on safety and system side that people like about Rust are the categories Ada is designed to handle. It's behind on temporal safety but otherwise solid. So, we have to bring up the comparison due to how the local community might be mentally categorizing or evaluating things.
Also, many wanted something safer or better than C/C++ but never heard of Ada. That's worth addressing in and of itself. I think D language has a similar problem where it's a lot better than C++ in many ways but gets too little attention. Once upon a time, there were Delphi and Modula-3, too. Those times are past us. :(
I do think your parent isn't fully right about the relationship though, as first they call Rust a "subset" of Ada's guarantees, but then say that Ada doesn't go so far as Rust in some places. In general, when discussing language semantics, I see them as more of a large overlap with some differences on each side, rather than one being a subset of the other.
Hopefully Rust will get type-level numbers and stuff someday :)
> We believe that we can have an RFC accepted and const generics available on nightly by the end of 2017. :tada:
Additionally, the "newtype" in Ada is far easier to work with. The new type inherits the operations of the parent type. When it comes to this sort of thing, it needs to be as simple and easy as possible or it just doesn't get done consistently.
As for not being alternatives, how do they mix in the same project? Does Ada have C-compatible FFI that lets both Ada and Rust see each other as C?
It has a more limited way of writing safe programs (memory pools and sub-pools, no aliasing by default, etc). That said, for some programs you will have to go unsafe and then you're done; however, from my limited experience with Rust it's also true there (Doubly linked list ?) even though you can go further and check more things.
Some people are working on having something similar in Ada/SPARK FWIW :)
> As for not being alternatives, how do they mix in the same project? Does Ada have C-compatible FFI that lets both Ada and Rust see each other as C?
Ada does have a C-compatible FFI, and in theory you can interface Rust and Ada. I don't know if it has been done though !
But the next step down is to do the memory management in an isolated module of your code, which is then supposed to be vetted thoroughly.
Vanilla Ada (i.e. not the safest variant) does provide RAII for dynamic management of resources in general.
Once you do get to explicit memory management, Ada does a number of things. I'm going to a list here just for my own sanity.
First, all deallocations are essentially marked unsafe. You use Unchecked_Deallocation() to free memory.
More importantly, it provides memory pools, and subpools. Each pointer type can be associated with a pool (or subpool). Once the pool goes out of scope, all memory is freed. You can use this to avoid explicit deallocations yourself. Pools also control allocations and deallocations, and there exist Debug pools that can help ensure memory is accessed correctly.
Ada also requires that stack-based objects be declared as "aliased" before you may make a pointer to them, so it's always clear where there might be trouble.
Finally (I think), Ada has a concept of accessibility levels. Essentially a pointer cannot point to an object that is more deeply scoped than itself. This isn't the perfection of the Rust borrow checker, but it does quite a bit.
As for C FFI, Ada does that quite well. It's got a package in the standard library with C interface types, and aspects for marking things for C FFI.
For me personally Rust is not a good Ada replacement, because 1) it has the usual obscure syntax - maybe to attract C++ programmers, who seem to like obscurity -, 2.) many semantic tricks and borrower idiosyncrasies that makes it harder to write, read and maintain than idiomatic Ada, and 3) it's fast changing. The good thing about Ada is that it is self-documenting, it's almost impossible to intentionally or unintentionally obfuscate it, and once you've written a program it will continue to work 10-20 years later.
Rust's syntax is, like many new languages as they are invented, essentially a pick of the crop of the best syntax ideas from other languages. 'Best' is probably debatable but familiarity is certainly not. Type annotations are basically Scala syntax. Class::method(), ditching `return` in favour of last-line expressions and the || {} closure syntax are all pretty close to Ruby. Structs/enums are also basically Scala. `instance.method()`, and pretty much the rest of the language, is universal.
It's true, Scala syntax for types is the weirdest part, but once you know what it means it's not hard to decipher. Much easier than trying to figure out function pointers in C, if you ask me.
#2 is probably true, but that's the cost of staying close to the metal. Rust makes all your memory access very explicit, rather than bundling it up in the standard/GC or leaving it to you. But it's a model you can get used to, and it has value as a mental model aside from its application in Rust.
#3 – well. Ada was young once. Rust is incredibly stable for a project designed by a distributed community of Mozilla and a ton of volunteers.
How so? Scala class bodies are a mix of ordered constructor statements and unordered declarations/method definitions, while Rust structs can only declare fields (without any sort of initial value). Scala enumerations (both scala.Enumeration and the more common sealed class versions) look nothing like a Rust enum; if anything Rust enums look like ML/Haskell algebraic type definitions.
> It's true, Scala syntax for types is the weirdest part, but once you know what it means it's not hard to decipher.
The part of the type syntax that is like Scala (i.e. the colons and arrows) has its origins in the ML family IIRC. Rust's bootstrapping compiler was written in OCaml, so I suspect that was in fact the inspiration.
I did fail to mention the other very Scala thing: match expressions. Most complicated Rust match expressions could almost compile without errors in Scala.
That you think otherwise only shows that you've never seriously used Ada in a larger project.
Edit: which is apparently now called adelian.
I did consider doing all my projects using ADA. I really want to love ADA. I simply can't because some fundamental building blocks are missing. I am very disappointed over this.
1) Ada is not a magic bullet, the seminal case study of why computer bugs are bad taught in CS courses around the world is the failed 1996 Ariane 5 rocket launch where the rocket veered off course and then exploded due to a software error that caused it not to understand the data it was getting from a sensor. More recently there are plenty of demonstrations of poor security in commercial plane avionics systems.
2) Ada is not easy to write, it's around the level of C I grant you, but it is not easy by any means, more importantly it is complex not simple. Each of those "features" like assert adds a layer of complexity to the system making it far harder for the programmer to write for. I think we all agree if we could program with simple functions that each do one thing and one thing well then combine those functions makes for a much simpler language and easier to think about.
3) Overhead, each of these features like assert adds a lot of overhead into the system, this is just not viable in a lot of systems, whats more those features are optional, so it is the first thing to drop where possible.
4) Toolchain and library support, most of us can't afford expensive toolchains and libraries, we don't work for very rich government contractors. Also we need libraries and toolchains are well maintained. Java has a massive standard library and a lot of free 3rd party libraries available, C/C++, python, perl, ruby, rust, javascript, c#, clojure... are all similar in that they have well maintained tool chains and good library support freely available either directly by language authors or from a third party.
Well, I don't agree. You could end up with tons of small functions, and complex interactions between them (whether they are pure or not) than make for spaghetti logic.
I also don't see how Ada's pre/post conditions make it more difficult to program. More tedious, maybe, but about as much as writing tests. Not more difficult as in conceptually more challenging.
As for the Ariane error, Ada could have prevented it, if it was left to -- but the programmers didn't let it. It's not a silver bullet of course, but nothing is, not even formally proven programs.
Finally, the overhead is true, but some domains don't need much speed anyway, and for others, you could always have the checks for all demo/dev runs and drop them for the final production output.
I think 'one thing well' is pretty much the opposite of this idea. It implies encapsulation and composition, not smushing together. If some functions are pure, there is no such thing as 'complex interactions between them', only simple ones.
On the other hand, it would definitely be weird if Ada were used as a functional programming language.
I do not believe that a simple paradigm shift will help you create arbitrarily complex programs simply. At some point the complexity must be handled.
Sad reality: The best security features in the world are useless if they are too slow for your use case.
Technically, Why3 (intermediate language and prover manager used in SPARK for verification) can interface with Isabelle too. I should try it out.
More edit: someone has done it, called HOL-SPARK. Excellent.
Ada is not a magic bullet
1) Nothing is a magic bullet. (And certainly no actual user of Ada has ever claimed that it is a magic bullet.)
Ada is not easy to write, it's around the level of C I grant you.
2) Ada is way harder to write than C, unless you're very proficient with it, of course. It's a much bigger language, and the syntax is not easy. Ada is designed for easy readability and maintainability, not for easy writing. It also requires a lot of repetition if you write good style. (You can write Ada code like C and Pascal, but then you loose most of the language's main advantages.)
Overhead, each of these features like assert adds a lot of overhead into the system, this is just not viable in a lot of systems.
3.) Ada is blazingly fast even without any optimization, and after optimization basically around the performance of C (or C++, if you use tagged types with dynamic dispatch). That's not very surprising, since GNAT uses gcc's backend optimizations. As for overhead, every runtime-check in Ada can be switched off. You can even discard the whole runtime system and use your own minimal runtime system, if you feel ambitious.
There is reason to believe that future versions of Rust (or even the current compiler) are faster than idiomatic, well-styled Ada. The goal of Ada was never to produce the fastest executables, and the fact that GNAT creates so fast ones is merely due to the authors of GCC and Ada's strong focus on compile-time operations and checks that it shares with Rust.)
Toolchain and library support, most of us can't afford expensive toolchains and libraries
4) Absolutely true, library support is not comparable to C and not even remotely comparable to C++. However, see my other remark here, an Ada library that appears to be abandoned will compile and work 100% fine and you can understand it by reading the source code (no documentation needed). But yeah, there are way less libraries, of course.
As for Java or Python, these are bad comparisons. They are both much slower than Ada and way less maintainable.
In regards to speed as other comments have also pointed out some of the asserts were optimised out of some of the Ariane 5 code, while the ones left in worked towards the cause of the issue ;)
The reason Java is in there is due to the libraries available, you can use those libraries with other JVM hosted languages like clojure ;) now tell me it's not maintainable. Similar reasons why python is in that list, I believe no OOP language is truly maintainable at this point, it adds to much complexity.
You can verify assertions statically and then omit them from the compiled program.
> most of us can't afford expensive toolchains and libraries
Check out the free software toolchains and libraries at http://libre.adacore.com/tools/
https://docs.google.com/spreadsheets/d/1BAiJR026ih1U8HoRw__n...
(Would be cool if we could fill out those remaining white cells btw ;) )
The subtyping feature I love the most. You see the benefit when you need to call a function which is taking many numeric arguments, all of them is a subtype so you can't give the arguments in the wrong order (latitude and longitude for example).
I only have very little experience with Ada, but being able to define a type whose values are integers in the range, say, 1 .. 7, to represent days of a week was an eye opener.
But even without range constraints, wrapper types (called newtypes in Haskell and Rust) are great. Let's say you have a function that accepts a temperature. A temperature is a float, but you want to prevent the user from mixing up celsius and fahrenheit.
This is what newtype looks like in Rust:
struct Celsius(pub f64);
struct Fahrenheit(pub f64);
fn print_celsius(temperature: Celsius) {
println!("Temperature is {}", temperature.0);
}
It will fail to compile if you accidentally pass a Fahrenheit value to `print_celsius`, but at compile time all values are optimized down to plain floats, without any runtime cost.More programming languages should adopt this concept :)
#include <stdio.h>
typedef struct { double v; } Celsius;
typedef struct { double v; } Fahrenheit;
void
print_celsius(Celsius temperature) {
printf("Temperature is %f", temperature.v);
}
void
compile_error(Fahrenheit temperature)
{
print_celsius(temperature);
}
I suspect LLVM will clean this up to plain floats as well.That said, yes, modern ABIs for modern machines usually pass single-element structures of primitive types in registers of the appropriate type. x86 (32-bit) is not modern in this sense: depending on the exact ABI used, these structures might well be passed on the stack or in an integer register.
The only advantage of Rust in this case is that its tuple syntax can be a bit nicer than C structs and the language supports overloading so that you can reimplement the comparison operators for instance (or do more complicated things, for instance if you have a "decibel" type that must have a special addition implementation)
Not that you can use the language-provided == on floats anyway, due to precision issues you should always check if the difference is below a specified limit...
Hehe, I've been there. ;-) Fortunately, that code dealt with physical coordinates, so if two points were less than 100µm apart, they were equal for our purposes.
I don't get why no modern language adopted them...
[someObject doSomethingWithLatitude:55.0 longitude:0.0];I remember when I told my mother (also a software engineer) that I got a job using Ada, she laughed and handed me all of her old Ada83 books (which are still on my bookshelf at work). I'm doing backend services in JVM-land these days, which I enjoy a bit more (not to mention I'm building a more portable skill-set) :)
When I was student, Ada was the main language at my school. It was used the first year to teach algorithms and the second year to teach parallelism. During the first year, I have also learned C by myself for curiosity. After the summer holidays between first and second year, I had forgotten most of the syntax needed to start a new program in Ada, but I was remembering perfectly well how to code in C.
Even After 8 years of usage of Ada, I still needed the user manual regularly. After less than 1 year of usage of C, I could sell my R&K.
I'm not much of a fan of C++ myself, but I often find myself looking back at that short note, as a pretty valid criticism of how C tend to mix some parts that appear simple with some very complex actual behavior (eg: printing strings, reading in strings and integers).
[1] http://www.stroustrup.com/new_learning.pdf "Learning Standard C++ as a New Language"
Eiffel is yet another Pascal-ish language whose passing I lament, and lately I've been seriously considering giving Ada another try, given that Modula-3 and Oberon won't exactly come back to life miraculously, and I'm still a bit dubious about all the new kids on the block.
The build tool, GPRBuild is already a step above makefiles, and personally I find it very convenient to work with.
CMake makes it relatively painless to wrap multi-platform dependencies without having to explicitly know whether they come from a Unix package manager or from Windows DLLs.
"Some people, when confronted with a dependency problem, think 'I know, I'll use Docker.' Now they have two problems."
Unfortunately there's no single answer. Static libraries vs. dynamic? Global install or packages from a repo? Everything depends on the degree of control you have over the deployment environment.
I guess Gentoo's emerge would be the closest thing to a "good" C/C++ package manager we have today, which I find a bit funny :)
The Rust team started working on a package manager early as part of the language design, which seems like the only way to make it actually work.
1. Cargo. But not the one you've heard of. A small Python script that pulled stuff off of GitHub.
2. Rustpkg. Felt like Go's tooling. https://doc.rust-lang.org/0.9/guide-rustpkg.html
3. Cargo, the one we still use today. https://mail.mozilla.org/pipermail/rust-dev/2014-March/00908... just over three years ago.
The timing side of things is extremely misleading: there are time primitives in the language, but you don't know if the implementation of the runtime will actually reach your constraint, and you won't be warned (it took me 2 days on the scope before understanding that it was not my code that was the issue).
if you want to toy around, I have started an Intellij Plugin: https://github.com/nraynaud/ada-intellij
Some registers of the STM32 are mapped here: https://github.com/nraynaud/bldc_ada/tree/master/src
You can see here for more information:
http://www.spark-2014.org/ http://docs.adacore.com/spark2014-docs/html/ug/en/tutorial.h...
Here's a CubeSat operating system written in SPARK/Ada: http://www.cubesatlab.org/CubedOS.jsp
C won because it was more flexible and was chosen by Microsoft, but Pascal definitely had a strong following (it was the original Mac's API language, and on PC it was a major contender thanks to the nice and fast Turbo Pascal IDE).
Ada was a complicated and rigid language designed by a Department of Defense committee. That was not great PR for those early individualist PC users. (Who wants to program in the language of "The Man"?)
If Pascal was a Cessna and C was a Learjet, then Ada was the F-35 -- complex, expensive, late.
Ada was designed by Jean Ichbiah, and although there was a committee he did override them on multiple occasions. So not really a design-by-committee.
The reasons it got no traction were: it was too complex/heavyweight, and too dissimilar to C to warrant a change (businesses waited for C++ instead, since it was around the corner at that time); due to its complexity it took a while for certified compilers to show up, and when they did they were quite expensive.
Since it was properly established in high margin markets, the community (mainly AdaCore, since they are the main compiler vendor) did little to improve its standing -- the only open-source projects they have pushed are all under GPLv3, which is not going to help with its popularity (not a judgement on GPLv3, it's just that basic lib are almost always under more liberal licenses, e.g. Boost). It really feels as if these OSS projects were pushed only to attract new programmers, while trying to prevent[1] an actual independent community from springing up. Odd behaviour. Every 6 months or so someone on HN or Reddit posts an Ada article that get people's interest, and then they discover they cannot write their project with it due to its licensing.
[1]: "...it is the intention of the GPL distribution of GNAT to restrict your freedom" in: http://libre.adacore.com/tools/gnat-gpl-edition/faq/
Drake [1] is a promising new Ada runtime with non-GPL license that deserves more support.
Similarly, DRACO is ISC licensed [2].
Of course, these projects aren't backed by moneyed interests like AdaCore is, so they get no love.
[1]: https://github.com/ytomino/drake/wiki/Why-a-new-Ada-runtime%...
I remember reading about a (compiler-checked) subset of Ada for deterministic realtime code. If I was writing code that had to meet hard realtime requirements, I would certainly love it if the compiler could help me with that.
And while we can do Ada style programming in C++, the lack of control over the C copy-paste compatibility features hinders that, unless one is allowed to turn all security knobs on (warnings as errors, sanitizers, code guidelines compliance).
What's a language that's just always got your back in a situation? That's the F/A18.
I have vague memories of trying to write MacOS apps in CodeWarrior and needing to mark C functions as having a Pascal ABI...
I'd say its performance is usually in the range of C++ performance. You can make it as fast as C, of course, because nobody forces you to do object-oriented programming (tagged types in Ada) and all runtime checks can be switched off with pragmas. However, I don't think that many Ada programmers are interested in that.
In a nutshell, speed is not a reason to opt for Ada over C. Safety and maintainability are. As for reasons for going the other way, there are plenty of them. Getting Ada programs to compile can take a long time, especially when you're learning the language, and the learning curve is relatively steep. Ada is a huge language in comparison to C and it's syntax is "per-construct" - it looks easy but there is a lot more to learn in comparison to, say, Pascal. Another reason: Lack of external libraries. C has way more.
Although I am not sure if that counts as read world usage. ;-)
In practice, some language constructs are not as optimized as their C++ counterpart.
The end result is that you can expect the Ada 83 subset to be extremely fast, but more recent constructs like OO and controlled types (equivalent to C++ virtual destructors) to be slower.
All in all it has a pretty good performance story !
Is this the same theory that makes java is just as fast? Ie, if I wrote idiomatic Ada would the levels of indirection (pointers to pointers to pointers and virtual functions everywhere) by more like c or more like java?
Also, there are several idiomatic dialects of Ada, because Ada has a facility to deactivate language features at compile time. Some people in safety critical don't want any dispatch for example.
FWIW I ported a quite large code base from C++ to Ada so I'm have first hand experience with this.
Puppy 2: Yes, I have a question. Why didn't you use Rust?
Puppy 1: That's a good question. While Rust looks promising, we felt it was too immature for our needs and decided to go with the time-tested, standardized semantics and extensive tooling available with Ada.
Puppy 2: Rust has a borrow checker. That makes it 100% safe.
Puppy 1: Leaving aside the fact that no language is 100% safe, safety is a complicated topic and you can't make strong statements like that about it. Does Rust's borrow checker enforce arbitrary program invariants?
Puppy 2: ...Rust has a borrow checker. That makes it safe. Borrow semantics is the secret ingredient in the memory safety sauce.