Summary after Four Months with Ada
For the last four months I've been learning and writing Ada 2012. I wanted to share my experiences with the language for those who aren't familiar with it. I'm neither endorsing or denouncing Ada. For those unfamiliar with Ada terms, I'll using the vernacular common to C family languages. This will make Ada programmers cringe, but will be much more clear to the general reader.
When I refer to "Ada" here, I'm referring to Ada 2012 and the SPARK 2014 language, which is a subset of Ada 2012 used for formal verification.
Why?
Ada's an obscure language. It's been around for decades and strong opinions about it existed in the past, but overall it seems forgotten outside its niche. When I mention I made things in it, I've gotten baffled responses of "That language is still around?" Another comment was, "I've never heard of anyone else ever working, or having working in Ada."
I've written code in a variety of languages, but Ada by far is the most bizarre and strangely familiar one at the same time. My intent was to mess around with it for a few weekends and move on, but it wasn't the "dead language" I expected. There's been a lot of modernization in the last couple of years, which makes it a surprisingly modern language to work in.
Resources
These are listed in order of what I used:
- learn.adacore.com
- A good tutorial site. It's enough to get you started.
- John Barnes "Programming in Ada 2012"
- Barnes works on several major Ada references, and this book walks through the language. At over 700 pages, it took me about a month to get through.
- This seems to be a common way Ada programmers get onboarded, and Ada programmers stay updated.
- Ada Language Wikibook
- This is exceptionally good and very complete.
- Awesome-Ada
- A site with way more resources than I list here.
- Ada Reference Manual
- Authoritative reference about how Ada should work.
- Building High Integrity Applications with SPARK
- It's ok, the first third of the book is an Ada crash course before it gets into SPARK.
Projects
During this timeframe I made a few projects:
- Septum - context-based source code search
- dir\ iterators - library similar to the incredible walkdir
- project\ indicators - library for spinners and progress bars
- trendy\ terminal - library for cross-platform terminal setup
- trendy\ test - library for simple unit testing
- Ada Ray\ Tracer - a port of Ray Tracing in One Weekend](https://raytracing.github.io/books/RayTracingInOneWeekend.html))
- dirs to graphviz : Make graphviz files from directory trees
Ecosystem
Alire
Alire simplifies Ada development significantly, by simplifying project generation, building, running, and dependency management.
It borrows heavily from Cargo, and if "good artists borrow, great artists steal," then Ada is on par with Michaelangelo. In its quest for modernizing, many concepts of Rust's cargo are being built into this tool.
Before Alire, I had a a lot of confusion about "How do I use project as a dependency?" and with Alire it's one command line and go.
This tool only went to 1.0 since I've been working with Ada and it simplifies development considerably. Alire interfaces with the pre-existing tool grpbuild, which is a common interface into the GNAT ecosystem and tools. Since it wraps it, you can do additional configuration past the initial setup, and it works well with GNAT Studio, one of the major editors. You can also set the config to run any other editor command with environment variables setup, such as Visual Studio Code:
Toolchains are integrated into Alire as of the 1.1 release candidate, so it's becoming a one-stop shop for what you need.
However, there's a few hoops to jump through to get a crate into the manager. For now, every package and version update requires manual approval. This is supposed to prevent name squatting and ensures existing libraries can get their appropriate names. It still can be frustrating waiting for approval, though things usually get approved within a day or so. You can use local versions as a dependency, which helps when developing libraries, and keeps you moving if you're waiting that day for approval.
Overall, Alire is a fantastic tool which makes working with Ada easy and I wouldn't recommend learning Ada without it.
GNAT
The major Ada ecosystem is GNAT. Yes, there supposedly are commercially supported compilers and AdaCore offers paid support, but the Free Software Foundation (FSF) offers a Ada front-end to GCC. This is usually referred to as "FSF GNAT". Alire provides a way to grab the FSF GNAT toolchain.
The entire ecosystem is designed around running tools from the command line, which allows editors and CI to use the same actions for behaviors. For example, GNAT Studio just wraps many of the GNAT tools and it shows you the command line for the actions you want to use. This acts as training wheels if you want to get started quickly and then transition to using another editor, especially a terminal one like Emacs or Vim.
There's a lot of parts to GNAT, which is a super deep dive I'm not interested in doing here. To give an idea, there's a formatter (gnatpp
) and a document generator (gnatdoc
).
There is also an LLVM frontend for Ada in progress. It'd be exciting to see this integrated as a toolchain you can download with Alire.
Editors
If you believe reddit, editor usage is split roughly in thirds between GNAT Studio and Visual Studio Code, with Emacs/Vim balanced in the remaining third.
The big editor is GNAT Studio, which used to be known as "GPS", and supports Ada, C and C++. It comes bundled with GNAT Community Edition, but you can build and run it separately as well. There's some quirky behavior, like tab
indenting to where it thinks the indent should be and not actually inserting a tab, and some obscure keyboard shortcuts, but otherwise is a mature IDE experience. You can make and export your own keybindings, which I've done for Visual Studio which I should probably contribute back at some point.
There's also support for Visual Studio code, which relies on the Ada Language Server.
A coordinated set of Vim plugins is available for those who want to go that route.
I had been primarily using GNAT Studio and then moved over to Visual Studio code, you can get a good editing experience with either one of these.
I'm not an Emacs user, so I'm not familiar with how folks work in that environment.
Cross-Platform Behavior
Alire hooks into GPRbuild's external variable system for cross-platform behavior. In general, I haven't had to mess with GPR configurations too much since Alire wraps it well and provides good defaults on project creation.
This is the Alire side, describing the external values to set for gprbuild to do the "right thing."
Just like in cargo, we have a .toml
describing the build.
On the GPR side, we select the source we want to use, since Ada doesn't have a common preprocessor. GNAT has a preprocessor but it seems frowned upon to use it.
Community
The online Ada community is exceptionally small. Online counts in r/ada hover in the mid twenties, compared to Rust's ~900, and seem to peak in the 50s.
Despite the size, the community is extremely knowledgeable and there's a lot of folks with decades of Ada experience chomping at the bit to answer questions. This means Reddit and Stack Overflow answers regularly include the applicable language rule's section and paragraph from the Ada Reference Manual.
An interesting aside is that if Github locations are to be believed, the Ada community is predominantly European. This correlates with what I've seen, since I'm in the US Eastern time zone, people online tend to be very active in the morning.
AdaCore and Ada Modernization
AdaCore didn't invent Ada, so they're not a direct comparison to Mozilla with Rust, but they did help develop GNAT originally, and continue to contribute back. I'm sure there's other companies promoting Ada, but they seem to be the most visible.
It looks like there was a huge burst of activity after the release of Ada 2012, including a lot of quite-detailed YouTube videos and marketing.
The second wave promoting Ada now seems to be getting better traction and community involvement. AdaCore open-sourced a lot of libraries and have been the primary group I've seen promoting the language. The movement they're pushing seems to be the right direction: building an LLVM front-end to the language, a language server, open-sourcing a lot of libraries, creating learning resources and improving IDE support.
The modernization push is intense. There is a marked improvement working in the language even in the short time span I've seen. It's gone from "a quirky and sometimes clunky car" to feeling "modern."
A Free-Function Focused C++, or a Strongly Typed C
Ada focuses on creating packages of code which contain types and functions. There's no preprocessor, so instead of #include
, you with
packages which contain what you need.
Packages are namespaces for functions and types, unlike other languages where types can "contain" functions and types. Function overloading acts as a key design element, made clear by the lack of implicit casts.
Everything in a package is related, there's no syntactical split between "free function", "class function", or "member function" (method).
Functions get declared similarly, with parameter type and ordering describing the difference between C++'s "free functions" vs instance functions. A class type as a leading parameter determines if one would be considered a member function ("methods"). There's a concept called "primitive operations" which describe bringing in operations of a parent type (even primitive, like Integer) where this matters.
Packages are split between a "specification" (.ads file) and a "body" (.adb file), akin to the "header" and "source" files you encounter in C/C++, though this distinction is understood at the language level. Since physical design matters in ways similar to C and C++, some physical design techniques in Lakos' Large-Scale C++ Software Design book actually work.
Packages can contain startup code executed by the "environment task" prior to entering the main procedure for initialization. Unlike C++, there are language pragmas you can use to control order of initialization of these elements.
SPARK
SPARK is a subset of Ada I glazed over earlier. It's a language for formal verification which you can intermingle with Ada code. Think of it as sort of like extern C
in C++, or unsafe
in Rust, except it marks code as undergoing verification. Since it's a subset of Ada, in addition to verification, you get all the rest of the Ada tooling here, like Alire. Yes, there are SPARK formally verified crates in Alire, such as a formally verified implementation of NaCl.
Focus on Intent
Parameter Modes
Ada source focuses on describing intent and modeling semantics.
I misled you earlier for expediency. What most C-family languages call "functions", Ada calls "subprograms". Ada distinguishes between those which return a value and are truly "functions" and those which do not return a value, and are "procedures."
For example, subprogram parameters can be either in
, out
, or both. in
parameters are readonly, and while you can force passing by reference via specifics in the language, you often just ignore how this happens. Parameters are implicitly in
, so you can omit that if you want. This is common in SPARK code because out
parameters are forbidden.
Derived Types
Describing semantics goes all the way into primitive types, and the rules are consistent between primitive and user-defined types. Creating lightweight types with domain-specific meaning, prevents mishandling of semantics on primitive types due to no implicit casting. Interfaces rarely use Integer
or Float
directly, instead you'll find semantic versions ("derived types") created such as "Meters" or "Kilometers".
Function overloading checks parameters as well as return types, so creating functions and transforms of types is straightforward.
Compile and runtime checks provide bounds-checking and numerical types can have their bounds constrained to "known good" values.
You can also access the ranges of types with the 'First
and 'Last
attributes (read as "tick first" and "tick last").
Enumeration Types
Enumeration types have first class support, with many automatically generated attributes. 'First
and 'Last
get the bounds of the values and 'Pred
(predecessor) and 'Succ
move between individual values. Iteration over all values and conversions to and from strings and integers gets provided for free. Together, these attributes and iteration capability allow writing of generic code which operates on discrete types like integers, or enumerations.
Since arrays operate using a discrete type as an index, enumerations can be used as the type to index into an array.
Pre- and Post-Conditions
Ada 2012 adds built-in support for pre and post conditions, through the use of "aspects." This is a "killer feature" of Ada 2012, on top of all of the other type checking and safety checking. Though used in SPARK analyses, you can also write them in plain Ada 2012 code and as part of specification of the function. Clients can see it as part of the interface and the compiler inserts these runtime checks if enabled. A lot of languages have an assertion mechanism which often effectively gets used for these checks, but it's nice to have a client-visible built-in way of doing this I've also found that adding pre and post condition checks during debugging to be a very effective tool.
Types which expose no private state can also have type invariants which are checked prior to usage as function arguments and after assignments.
Concurrency support
Protected Objects
Protected objects coordinate concurrent access to shared state. The control can also include arbitrarily complex conditionals as well, such as not allowing any writers when readers exist, or blocking any more readers when a writer is waiting.
Tasks
Tasks provide concurrent execution. Additionally, they have special procedures called "entries" which can be "accepted" by a related task during its flow of execution to synchronize (rendezvous) with other tasks and share data at these points.
Tasks run concurrently in the block in which they're declared, and the block will not exit until the task finishes or terminates, unless it is allocated on the heap.
Both single instance and instantiable versions of protected objects and tasks can be created.
Generics
Ada generics are similar to ML signatures, and may contain types, functions and even other packages as parameters. Generic packages or functions must be explicitly instantiated for use. This eliminates the debate of angled brackets (<>) versus square brackets for generics (]), but leads to additional names being created. This makes their usage and their cost explicit, at the expense of verboseness.
Low Level Control
Accessing C functions and compiler intrinsics is straightforward. You create a declaration of the subprogram and then describe where it comes from using aspects or the Import
pragma.
Since the usage is the same as with an Ada function, imported functions can be replaced with actual Ada code if needed. Inline assembler is also available, but due to the lack of a preprocessor, the build system (gpr) is leveraged to choose the appropriate definition (body) file to compile.
Representation clauses allow you to match struct layout or binary formats such as for files.
Vocabulary
Ada suffers from a lack of familiarity for many programmers due to being a Pascal family language and also its peculiar, but very specific vocabulary. However, the usage of keywords over punctuation helps ease many problems of dealing with an unfamiliar language. While this helps with googling and a lot of terms appear in code, many are specific to, or have Ada-specific definitions. Examples are "accesses" (sort of like pointers), "accessibility" (similar to a scope for borrowing), "tagged types" (classes), "derived types" (unrelated to OOP), and "subprogram".
Verboseness
The language has a mind of its own as well. As one online quote says
When I find myself fighting the
[Ada]
language, it usually means that I need to revisit my design.
I've found this to be true overall. Ada makes some easy things verbose and some verbose things easy. When things go from "verbose" to "writing like a Charles Dickens novel", that's when I rethink my approach to the problem. There's usually a significantly better and shorted way to accomplish the task.
Was it worth it?
If I were never to write Ada again, I still learned a lot about program correctness. Correctness isn't usually a fun language feature to talk about and no one likes to admit they write bugs. Ada excels at modeling your program in the language while automating a lot of error checking. For example, you may know you're not going to use the full range of an integer, and Ada enables specification and automatic checking of valid values. Combined with built-in pre and post conditions, this has helped me improve the way I think while programming.