· 7 min read ·

Ada Was Right All Along: The Language That Kept Getting Rediscovered

Source: hackernews

There’s an article making rounds on Hacker News called The Quiet Colossus about Ada’s design and its outsized influence on programming language theory. I read it and then spent the next two hours going down a rabbit hole I didn’t expect to fall into. Ada is one of those languages you hear about in the context of defense contracts and avionics and then sort of mentally file away as a relic. That framing is wrong in an interesting way.

Ada was designed in the late 1970s under a US Department of Defense initiative to solve a genuine engineering crisis. The DoD was using somewhere in the range of 450 distinct programming languages across its embedded systems projects, each incompatible with the next, each with its own toolchain and failure modes. Jean Ichbiah led the team at Honeywell that won the design competition, and the result was standardized as Ada 83. The name is a tribute to Ada Lovelace, and the language has been revised three times since: Ada 95, Ada 2012, and Ada 2022.

What’s striking when you read through Ada’s design rationale is how many of the problems it was solving in 1983 are problems the industry is still solving now, and how many of the solutions it reached are ones we’re rediscovering under different names.

The Type System Is Doing Real Work

Ada’s type system is strong in a way that C’s is not, and different from Haskell’s in a way that matters practically. In Ada, you can define a new type that is range-constrained, and the constraint is enforced at compile time and runtime:

type Altitude_Meters is new Integer range 0 .. 50_000;
type Air_Temperature is new Integer range -100 .. 60;

function Compute_Density(Alt : Altitude_Meters; Temp : Air_Temperature)
  return Float;

These are not just type aliases. Altitude_Meters and Air_Temperature are distinct types. Passing one where the other is expected is a compile error. The ranges are checked at runtime on assignment. This sounds like a small thing until you remember the Ariane 5 failure in 1996, where a 64-bit floating point number representing horizontal velocity was converted to a 16-bit integer, overflowed, and caused a $370 million rocket to self-destruct 37 seconds into flight. The software in question was reused from Ariane 4, where the velocity values were in a safe range. In Ada, with proper type definitions and range constraints, the conversion would have raised a Constraint_Error at the precise point of overflow rather than silently corrupting the value.

The GNAT compiler, which has been open source since being developed at New York University and is now maintained by AdaCore, generates range checks inline. There’s a pragma to suppress them for performance-critical sections, but the default is checked.

Design by Contract Before It Had That Name

Bertrand Meyer coined “Design by Contract” for Eiffel in the late 1980s, and the concept became one of the more influential ideas in programming language theory. Ada 2012, released nearly 25 years after Meyer’s Eiffel work, incorporated contracts as a first-class language feature, not through a preprocessor or annotation library but as syntax:

function Binary_Search(
  Arr    : Integer_Array;
  Target : Integer
) return Integer
  with
    Pre  => Arr'Length > 0 and then Is_Sorted(Arr),
    Post => (Binary_Search'Result = -1
             or else Arr(Binary_Search'Result) = Target);

Preconditions and postconditions are checked at runtime by default and can be statically verified using SPARK, which is a formally verifiable subset of Ada. SPARK is used in the Airbus A380’s primary flight control software and in European railway signaling systems. The formal verification toolchain can prove the absence of runtime errors, buffer overflows, and division by zero without executing a single test case.

Contrast this with what Rust offers: ownership and borrowing eliminate a specific class of memory errors at compile time, which is genuinely powerful. But Rust doesn’t give you a way to specify that a function’s input array must be sorted, or that its return value must be a fixed point of a given function. SPARK Ada can prove those properties. They’re complementary approaches solving overlapping but distinct problems.

The language feature that makes Ada’s contracts particularly composable is type invariants. A type can carry an invariant that the compiler will verify holds across all public operations:

package Bounded_Stacks is
  type Stack(Capacity : Positive) is private
    with Type_Invariant => Stack_Size(Stack) <= Stack.Capacity;

  function Stack_Size(S : Stack) return Natural;
  procedure Push(S : in out Stack; Item : Integer)
    with Pre => Stack_Size(S) < S.Capacity;
private
  -- ...
end Bounded_Stacks;

You can’t construct an instance of Stack that violates its invariant through the public API. The compiler tracks this. This is what you’d otherwise implement by hand with private constructors and factory functions in Java or C++, except there the discipline is enforced by convention rather than by the language itself.

The Tasking Model Predates Structured Concurrency

Ada has had built-in concurrency since Ada 83. Not via a library. Not via pthreads wrapped in a nice API. Tasks are a first-class language construct:

task type Sensor_Reader is
  entry Start(Port : Port_Number);
  entry Read_Value(V : out Float);
end Sensor_Reader;

task body Sensor_Reader is
  Current_Port : Port_Number;
  Value        : Float;
begin
  accept Start(Port : Port_Number) do
    Current_Port := Port;
  end Start;
  loop
    Value := Hardware_Read(Current_Port);
    select
      accept Read_Value(V : out Float) do
        V := Value;
      end Read_Value;
    or
      delay 0.1;
    end select;
  end loop;
end Sensor_Reader;

The rendezvous mechanism, where tasks synchronize through accept/entry call pairs, is a direct implementation of Communicating Sequential Processes, the concurrency model Tony Hoare formalized in 1978. Go’s goroutines and channels are a modern rediscovery of essentially the same idea. The main difference is that Ada’s model is typed and integrated into the language’s static checking, while Go’s channels carry the same interface{} erasure problems that Go programmers work around constantly.

Ada 95 added protected objects, which are a cleaner primitive for shared-memory concurrency than mutexes:

protected type Shared_Counter is
  procedure Increment;
  procedure Decrement;
  function Value return Integer;
private
  Count : Integer := 0;
end Shared_Counter;

The compiler guarantees mutual exclusion for procedures and exclusive access semantics for entries. Functions (read-only by convention, enforced by the compiler in SPARK) can execute concurrently. This is the same distinction that Swift actors make, introduced in Swift 5.5 in 2021, 26 years after Ada 95.

Ada 2022 added parallel constructs for loop-level and block-level parallelism:

parallel
  for I in 1 .. N loop
    Result(I) := Expensive_Computation(I);
  end loop;

The runtime schedules these across available cores. The design is conservative, requiring the loop body to be free of dependencies, which the compiler can verify in many cases.

Generics Without Template Hell

Ada generics are distinct from C++ templates in a way that affects compilation performance and error message quality substantially. In C++, templates are expanded at instantiation time, which means type errors in a template body are reported in terms of the expanded code, not the template itself. The infamous pages-long error messages from STL code are a consequence of this design.

Ada generics are compiled once against their formal parameters. The formal parameters carry constraints:

generic
  type Element is private;
  with function "<"(Left, Right : Element) return Boolean is <>;
package Generic_Sorted_Set is
  procedure Insert(Item : Element);
  function Contains(Item : Element) return Boolean;
end Generic_Sorted_Set;

The constraint with function "<" means the generic is valid only for types that have a less-than operator. This is checked at the generic declaration, not at the instantiation site. Error messages point to the actual problem. The compiled generic body is separately analyzed for correctness relative to its formal parameters, which means errors in the implementation are caught without instantiating the generic at all.

Rust’s trait bounds do something similar, and the Rust community has been explicit that Ada generics were part of the prior art that informed the design. Haskell’s type classes are the more direct ancestor, but Ada showed that the approach was practical in an imperative systems language well before Rust made it mainstream.

What Gets in the Way

Ada’s tooling situation has improved, but it’s not where Rust or Go are. The free path is GNAT through GCC, with Alire as a package manager that was introduced relatively recently and is still maturing. The commercial path through AdaCore’s GNAT Pro gives you better IDE support and formal verification tools, but it’s priced for aerospace contractors, not individuals.

The syntax is verbose by contemporary standards. This was a deliberate choice by Ichbiah’s team, who argued that code is read more than it’s written, and that ambiguity in syntax costs more in maintenance than it saves in initial development. That argument is correct in safety-critical contexts. It’s a harder sell for web services.

The community is real but small. The Ada and SPARK subreddit and the AdaCore forums are active, but the ecosystem of libraries and tutorials doesn’t approach what Rust has built in its first decade.

Why It Matters Now

The programming language landscape in 2026 is converging on ideas that Ada had in 1983. Memory safety is now a mainstream concern rather than a niche interest, driven partly by US government guidance recommending memory-safe languages. Formal verification is becoming practical at scale. Structured concurrency has been adopted by Swift, Kotlin, Python’s asyncio, and Java’s Project Loom. Contract-based programming is getting traction through Rust’s experimental #[requires]/#[ensures] attributes via the Verus project.

Ada solved these problems, documented the solutions, and shipped them in production systems that have been running continuously for decades. The lesson isn’t that everyone should switch to Ada. The lesson is that language design is a field with genuine intellectual history, and the ideas worth having tend to get reinvented repeatedly until they finally stick. Ada is evidence of both the difficulty of that process and the patience it sometimes requires.

Was this interesting?