1 Punkte von GN⁺ 2025-10-05 | 1 Kommentare | Auf WhatsApp teilen
  • Verglichen werden die Unterschiede und Eigenschaften von Ada und Rust, die beim Lösen von Advent-of-Code-Problemen sichtbar wurden
  • Analysiert werden die Unterschiede im Sprachdesign und in der praktischen Programmierweise beider Sprachen mit Fokus auf Sicherheit und Zuverlässigkeit
  • Unterschiede zeigen sich aus vielen Blickwinkeln, etwa bei den Standardbibliotheken, eingebauten Funktionen, Leistungsunterschieden und dem Stil der Fehlerbehandlung
  • Anhand praktischer Codebeispiele zu Modularität, Generics, Schleifen und Error Handling werden konkrete Erfahrungen aus der realen Entwicklung und Nutzung erläutert
  • Besonders deutlich unterscheiden sich die Entwicklungserfahrungen durch die Art der statischen Typisierung, die Array-Verarbeitung und die Schnittstellen zur Fehlerbehandlung

Einführung und Zielsetzung

  • Beim Lösen von Advent of Code (im Folgenden AoC) wurde zunächst nur Ada verwendet; seit 2023 werden Lösungen auch in Rust und Modula-2 geschrieben, was einen direkten Vergleich ermöglichte
  • Beim Übertragen bestehender Ada-zentrierter Lösungen nach Rust wurden die strukturellen Unterschiede und die eigenständigen Ansätze beider Sprachen deutlich spürbar
  • Ziel ist es, die praktischen Unterschiede im Hinblick auf Code-Sicherheit, Zuverlässigkeit und Sprachdesign klar herauszuarbeiten

Für den Vergleich verwendete Sprachversionen

  • Ada 2022 (bei Bedarf unter Bezug auf einige Regeln aus SPARK 2014)
  • Rust 2021 (für die zentralen Vergleiche auf Basis von Rust 1.81.0)

Ausgeklammerte Funktionen und Vergleichskriterien

  • Die typischen Funktionen jeder Sprache (= Killer-Features) werden im Text nur kurz in Kommentaren erwähnt
  • Einige Funktionen wurden je nach persönlicher Erfahrung und praktischem Bedarf der jeweiligen Lösung nicht behandelt
  • Es wird versucht, persönliche Meinungen möglichst auszuklammern und sich auf die wesentlichen Merkmale zu konzentrieren

Hintergrund und Perspektive des Autors

  • Sowohl bei Ada als auch bei Rust ist der Autor kein Muttersprachler; die Grundlage bilden Erfahrungen mit Sprachen der 1980er Jahre wie C/C++, Pascal und Modula-2
  • Daher kann sich der Code-Stil von modernen oder idiomatischen Stilformen unterscheiden
  • Die Implementierungen sind möglicherweise nicht optimal, und je nach Problemsituation wurden teils intuitive oder unübliche Lösungsansätze gewählt

Positionierung von Ada und Rust

  • Ada ist weiterhin eine sehr sichere und hochzuverlässige Sprache für System- und Embedded-Entwicklung und legt großen Wert auf Code-Lesbarkeit
  • Rust bringt Stärken bei Speichersicherheit und Systemprogrammierung mit und wurde in der Stack-Overflow-Entwicklerumfrage über mehrere Jahre als „beliebteste Sprache“ genannt
  • Ada ist eine allgemein einsetzbare Hochsprache und bietet ein Spektrum, das besonders auf Lesen und Wartung ausgerichtet ist
  • Rust zielt auf die Entwicklung von niedrigstufigen Systemprogrammen und etabliert eine Kultur sicherer Programmierung mit explizitem Speichermanagement sowie Fehler- und Option-Typen

Vergleich von Sicherheit und strukturellen Eigenschaften

  • Ada

    • ISO-Standard (strenge Spezifikation)
    • Es ist einfach, problemgerechte Typen zu definieren, etwa für Bereiche oder Stellenzahlen
    • Array-Indizes dürfen auch nicht numerisch sein
    • Mit SPARK existiert eine noch strengere Spezifikation
  • Rust

    • Die Spezifikation stützt sich auf offizielle Dokumentation (Reference) und den Compiler
    • Typdeklarationen hängen von Maschinentypen ab (z. B. f64, u32)
    • Array-Indexierung ist im Wesentlichen nur mit numerischen Typen natürlich

Wichtige Zusammenfassung der Tabelle zu Funktionen/Eingebautem

  • Unterschiede gibt es unter anderem bei der Unterstützung von Array-Bereichsprüfungen, generischen Containern, Nebenläufigkeit, gelabelten Schleifen und Pattern Matching
  • Ada verwendet für Fehlerbehandlung Exceptions, Rust dagegen eine rückgabebasierte Behandlung über Result/Option-Typen
  • Rust hebt sich durch Unterstützung für Makros, Pattern Matching und funktionale Reinheit hervor
  • Ada unterstützt vertragsbasiertes Design und die Compile-Time-Prüfung von DBC (Design By Contract) in SPARK
  • Bei der Speichersicherheit erzwingen Rust und SPARK Sicherheit, während Ada die Nutzung von Null-Pointern zulässt

Vergleich von Performance und Laufzeit

  • Rust gilt allgemein als schnell zur Laufzeit, aber langsam beim Kompilieren, während Ada umgekehrt den Ruf hat, schnell zu kompilieren und zur Laufzeit wegen Prüfungen etwas langsamer zu sein
  • In Benchmarks kam es in der day24-Aufgabe bei Rust aufgrund der Grenzen des f64-Typs zu einem Overflow; Ada konnte dagegen mit einer Hochsprachen-Typangabe wie digits 18 automatisch einen geeigneten Maschinentyp wählen, Overflows vermeiden und sehr gute Performance zeigen
  • Rust muss dafür ein nicht stabiles f128 oder externe Bibliotheken verwenden, während Ada schon durch eine zum Compiler passende Typangabe im Vorteil sein kann

Dateiverarbeitung und Error Handling (Case Study 1)

Dateiverarbeitung in Ada

  • Standardmäßig wird Ada.Text_IO verwendet
  • Dateien lassen sich explizit öffnen, zeilenweise lesen und mit gewünschten Bereichen oder positionsbasierten Zeilen relativ intuitiv verarbeiten
  • Bei Fehlern erfolgt die Behandlung eher über Exceptions als über klare Fehlermeldungen, und in der Funktionssignatur ist die Möglichkeit eines Fehlers nicht sichtbar

Dateiverarbeitung in Rust

  • Verwendet werden std::fs::File und BufReader
  • Beim Öffnen einer Datei wird ein Result-Typ zurückgegeben, sodass die Möglichkeit eines Fehlers klar sichtbar ist
  • Direkter Zugriff per Zeichenindex wird nicht unterstützt; die Verarbeitung muss über Iteratoren erfolgen
  • Im Zentrum stehen funktionale und iterative Werkzeuge wie map, filter, collect, sum sowie verschiedene Makros (z. B. include_str!)
  • Durch explizite Fehlerangaben im Rückgabetyp wird die Fehlerweitergabe auf Funktionsebene klar nachvollziehbar

Modularität und Generics (Case Study 2)

Modularität in Ada

  • Klare Trennung von Spezifikation (Interface) und Implementierung auf Basis von Packages
  • Zur stärkeren Modularisierung werden Unterpakete sowie die Kombination von use/rename-Syntax zur Anpassung der Lesbarkeit eingesetzt
  • Generics in Packages unterstützen die Verallgemeinerung von Typen, Konstanten und ganzen Unterpaketen

Modularität in Rust

  • Module werden über das mod/crate-System organisiert; die Trennung von Spezifikation und Implementierung wird durch den Dokumentationsgenerator automatisiert
  • Deklarative Zugriffskontrolle über pub/private
  • Kombination aus Importen und Umbenennungen über use/as
  • Eingebaute Testunterstützung erlaubt es, Testmodule direkt im Code zu deklarieren, zu bauen und automatisch auszuführen

Generics

  • Ada unterstützt Generics nur auf Package-/Prozedur-Ebene (nicht für einzelne Typen allein)
  • Rust kann Generics direkt auf Typen anwenden (template-basiert)
  • Ada kann zusätzliche Typeigenschaften wie Wertebereiche über Range-Typen und Subtypen klar ausdrücken, während Rust dafür Instanzkonstanten nutzt

Vergleich von Enum-Typen (Case Study 3)

  • Ada bietet kompakte Deklarationen und unterstützt dabei automatisch diskrete und geordnete Typen sowie deren Nutzung in Schleifen und als Indizes
  • Rust-Enums sind in der Deklaration ähnlich, erfordern aber für Pattern Matching und Iteration einen expliziteren Umgang

Fazit

  • Bei hochsprachlichen Spezifikationstypen, Verifizierbarkeit und Laufzeitprüfungen bietet Ada eine strengere Kontrolle
  • Bei funktionalem Programmierstil, Makroprogrammierung und compilerunterstützter Fehlerbehandlung liegt Rust sowohl bei Entwicklerkomfort als auch bei Sicherheit vorn
  • In der praktischen Problemlösung punktet Ada mit Kompatibilität zu älterem Code und Wartungsstärken, Rust dagegen mit einem modernen Entwicklungsökosystem sowie Vorteilen bei Sicherheit und Parallelität

1 Kommentare

 
GN⁺ 2025-10-05
Hacker-News-Kommentare
  • Obwohl Ada wirklich viele gute Ideen hatte, ist es bedauerlich, dass es meist nur in Bereichen mit besonders hohen Sicherheitsanforderungen eingesetzt wurde. Vor allem die Möglichkeit, den Wertebereich numerischer Typen einzuschränken, ist äußerst nützlich, um bestimmte Bugs zu verhindern. Spark Ada war leicht zu lernen und ließ sich auch gut für die Entwicklung von Software einsetzen, die SIL 4 erfüllt, also den strengsten Software-Sicherheitsstandard. In den vergangenen Jahrzehnten ist die Softwareindustrie stark in Richtung „Wachstum zuerst, Stabilität zweitrangig“ abgedriftet, aber inzwischen spürt man wieder einen Trend hin zu sicherer Softwareentwicklung. Es wäre wünschenswert, wenn die in dieser Zeit gesammelten Erkenntnisse zur Sicherheit in besseren Sprachen münden würden. In der Realität sind gute Ideen oft in Nischensprachen versteckt und verschwunden
    • Wenn man lange genug Software entwickelt, merkt man, wie oft das Rad wirklich neu erfunden wird. Ada und Rust ähneln sich darin, dass beide Sicherheit anstreben, aber Definition und Anwendungsbereich unterscheiden sich. Rust verfolgt eine sehr fokussierte Form wichtiger Sicherheit mit großer Konsequenz, während Ada eine breitere und konkretere Definition von Sicherheit hat. Als ich Anfang der 90er Ada lernte, war die häufigste Kritik, dass die Sprache zu groß und komplex sei und dadurch die Entwicklung verlangsame. (Ein validierter Ada-83-Compiler kostete damals pro Person umgerechnet etwa 20.000 Dollar nach heutigem Wert.) Inzwischen haben sich die Zeiten geändert, und heute akzeptieren alle, dass eine große und komplexe Sprache wie Rust für tatsächlich sicheres Concurrent Programming nötig ist
    • Auch Nim unterstützt, inspiriert von Ada und Modula, Subranges zur Begrenzung des Wertebereichs von Typen
      type
        Age = range[0..200]
      
      let ageWorks = 200.Age
      let ageFails = 201.Age
      
      Beim Kompilieren erscheint ein Fehler, dass 201 nicht in den Typ Age umgewandelt werden kann
      Erklärung zu Nim Subranges
    • Ada (konkret GNAT) unterstützt Compile-Time-Analyse physikalischer Einheiten bzw. Dimensionsanalyse, also Unit Checking. In der Praxis des Engineerings ist das äußerst nützlich, und es ist fraglich, warum andere Sprachen eine so wichtige Funktion nur über Third-Party-Libraries anbieten
      Dokumentation dazu
    • Auch in C++ kann man numerische Typen mit eingeschränktem Wertebereich leicht selbst im Code bauen. Es gibt sie nicht in der Standard Library, aber die eigene Implementierung ist sehr einfach. Manche Sicherheitsprüfungen lassen sich nicht erst zur Laufzeit, sondern schon zur Compile Time durchführen. Es wäre gut, wenn alle Sprachen solche Funktionen standardmäßig unterstützen würden
    • Was ich an Ada am meisten vermisse, ist sein klarer Ansatz zu objektorientierter Programmierung. Die meisten Sprachen packen OOP-Konzepte in einen einzigen Block namens „Klasse“, während Ada Message Passing, Dynamic Dispatch, Subtyping, Generics usw. einzeln auswählbar macht. Mir gefiel sehr, wie elegant sich diese einzelnen Funktionen kombinieren ließen
  • Der Autor nennt als Unterschied unter anderem, dass Ada eine offizielle Spezifikation habe und Rust nicht. Aus Nutzersicht ist aber oft wichtiger, wie es um Adoption und Ökosystem der Sprache steht, also Tooling, Libraries und Community. Ada war in Luft- und Raumfahrt sowie sicherheitskritischen Bereichen erfolgreich und eignet sich auch für AOC oder Embedded-Low-Level-Arbeit, aber in realen Projekten wie verteilten Systemen oder OS-Komponenten wiegen Dinge wie Datenformate, Protokolle, IDE-Support und Zusammenarbeit mit Kolleginnen und Kollegen oft schwerer. Letztlich sind solche Umgebungsfaktoren bei der ersten Sprachwahl entscheidend
    • Kürzlich hat Rust mit Ferrocene ebenfalls ein Spezifikationsdokument gespendet, das sich am Stil der Ada-Spezifikation orientiert. Es ist öffentlich verfügbar
      Rust-Spezifikation
    • Sowohl Rust als auch Ada sind im strengen Sinn schwach, was „formale“ Spezifikationen angeht, also Dokumente, die sich maschinell beweisen lassen. Selbst Spark Ada stützt sich auf Annahmen zur Sprachsemantik, und auch das ist nicht vollständig formal und maschinenlesbar
    • Auch Entwickler von Flugsteuerungssoftware würden vermutlich sagen: „Wenn es um Dinge geht, die in realen Betriebsumgebungen nicht wichtig sind, dann ist unser Prozess dafür wahrscheinlich tatsächlich übertrieben.“ Tatsächlich sind strenge Sprachen und Prozesse wie bei Ada in sicherheitskritischen Bereichen eher Standard
  • Beeindruckend fand ich, dass Ada Rust trotz schwächerer typbezogener Funktionen bei der Lesbarkeit des Codes oft überlegen sein kann. In dem Vergleich fehlte außerdem eine Erwähnung der Compiler-Geschwindigkeit. Dass Ada als komplexe Sprache empfunden wurde, ist eher eine Geschichte aus der Vergangenheit, und im Vergleich zu Rust muss das heute nicht unbedingt so sein. Nach diesem Artikel hätte ich richtig Lust, ein echtes Projekt in Ada auszuprobieren
    • Ich frage mich, was genau mit den „typbezogenen Nachteilen“ gemeint ist. Meiner Erfahrung nach ist Adas Typsystem enorm ausdrucksstark. Benutzerdefinierte Wertebereichstypen, Arrays, die mit beliebigen Aufzählungstypen indiziert werden können, typspezifische Operatoren, zusätzliche Funktionen wie Compile-/Runtime-Checks, Vor- und Nachbedingungen und mehr. Es gibt auch Variant Records und Representation Clauses für Strukturen. Das ist für mich eher eine Stärke als ein Nachteil
  • Ich möchte über die Unterschiede bei Strings in Ada und Rust sprechen. Ada wurde Anfang der 1980er so entworfen, dass ein „String“ ein Array von Zeichen (char) ist, weshalb man ihn leicht wie ein Byte-Array indizieren kann. Rust wurde dagegen mit einem stärkeren Bewusstsein für Unicode entworfen, daher sind Rust-Strings UTF-8-kodiert und damit wirklich „Text“. Deshalb erlaubt Ada Random Indexing wie bei einem Array, während man in Rust bei einem anderen String-Konzept notfalls auch einfach auf ein Byte-Array umsteigen kann
    • Der eingebaute Unicode-String in Ada ist normalerweise ein UTF-32-Array. Anders als Rust bietet Ada keine direkten UTF-8-Literale, sondern man muss aus 8-/16-/32-Bit-Arrays konvertieren
    • Auch in Rust kann man Strings indexieren. Rust behandelt Strings aber nicht wie gewöhnliche Arrays, sondern arbeitet vor allem mit Substring-Slices. Wenn man beim Indexieren mitten durch ein Zeichen schneidet, kommt es zu einem Panic, also wenn man gegen die Grenzen von Unicode-Codeeinheiten verstößt. Wenn wie bei AoC ohnehin immer nur ASCII verwendet wird, ist es sinnvoll, einen Byte-Slice über [u8] oder die Methode str::as_bytes zu verwenden
  • Die Aussage des Autors, Rust unterstütze Concurrent Programming nicht von Haus aus, wirkt seltsam. Rust hat Thread-Funktionalität direkt eingebaut und ist in diesem Punkt sogar oft einfacher zu verwenden als Async. Problematisch wird es nur, wenn man extrem viele Threads braucht und an Ressourcenlimits stößt; für die meisten Anwendungen reichen eingebaute Threads völlig aus
    • (Als Nicht-Rust-Nutzer ernsthaft gefragt:) Wie unterscheidet sich in Rust die Behandlung von Canceln bei Threads und bei Async, und worin liegt der Unterschied zu Async in anderen Sprachen? In C++, Python und C# war das Management von Abbruch bei Async deutlich besser als bei Threads. Ich habe gehört, dass Rust das nicht über Exceptions handhabt und es dadurch eher schwieriger sein soll. Mich würde interessieren, wie das in der Praxis aussieht. Und auch, wie Ada mit solchen Abbrüchen umgeht
    • Mich interessiert, ab welcher Grenze ein Work-Stealing-Scheduler wie Tokio in der Praxis wirklich schneller ist, als einfach mehrere Threads laufen zu lassen. Das erinnert mich an einfache Arrays wie etwa VecMap: Bei wenigen Elementen sind sie schnell, aber ab einer gewissen Größe werden andere Datenstrukturen effizienter. Ich frage mich, ab welchem Punkt Work Stealing tatsächlich Vorteile bringt
    • Praktisch gesehen ist der Hauptgrund für Async oft schlicht, dass verwendete Third-Party-Crates Async sind. (Zum Beispiel braucht Reqwest Tokio.) Wenn man in der Entwicklung von Anwendungen auf höherer Ebene nur auf Nicht-Async setzt, stößt man irgendwann an Grenzen
    • Auf Plattformen mit schwacher Thread-Unterstützung wie WASM oder Embedded ist Async womöglich sogar besser geeignet. Situationen, in denen Hunderttausende gleichzeitig einen Blog besuchen, sind eher unrealistisch; in solchen Fällen die Notwendigkeit von Async zu betonen, wirkt etwas übertrieben
  • Interessant fand ich auch, dass es für Ada Open-Source-Compiler gibt. Früher dachte ich, es gebe nur proprietäre Compiler, und hatte deshalb überhaupt kein Interesse an Ada, aber das sollte ich mir wohl noch einmal ansehen
    • Den GNAT-Compiler gibt es seit über 30 Jahren. Früher gab es wegen der fehlenden GPL Runtime Exception das Missverständnis, dass auch kompilierte Ergebnisse unter die GPL fallen müssten, aber dieses Problem ist inzwischen gelöst
    • GNAT wurde seit den 90ern auf Basis von GCC entwickelt, und an manchen Universitäten wurde GNAT direkt in praxisorientierten Kursen wie Real-Time Programming eingesetzt. Ich habe Ada selbst einmal als Einstiegssprache erlebt, bevor man recht schnell zu Pascal und C++ wechselte
  • Unter den Projekten aus dem 3D-Druck-Bereich, die ich interessant fand, war zuletzt Prunt, ein Printer Control Board und Firmware-Projekt. Dass die Firmware in Ada entwickelt wird, ist eine ziemlich ungewöhnliche, aber konzeptionell durchaus passende Wahl
    Prunt-Website
    Prunt GitHub
  • Am Ende von Case Study 2 heißt es: „Wenn der Client SIDE_LENGTH kennen muss, füge eine Funktion hinzu, die ihn zurückgibt.“ Direkter wäre es aber, einfach eine Konstante wie pub const SIDE_LENGTH: usize = ROW_LENGTH; zu deklarieren
  • Ich stimme der Behauptung nicht zu, dass beide Sprachen stackzentriertes Programmieren fördern. Ada empfiehlt vielmehr aktiv statische Allokation
  • Ich fand es überraschend, dass die Möglichkeit beliebiger Typen als Array-Indizes in Ada als großer Vorteil dargestellt wurde. Fast alle Sprachen haben Dictionaries bzw. HashMaps in der Standard Library, und Rust bietet davon sogar zwei Varianten
    • Hier geht es um eingebaute Arrays auf Sprachebene. Wenn man in Ada zum Beispiel den Index eines Arrays „eggs“ als Typ BirdSpecies definiert, dann sind eggs[Robin] und eggs[Seagull] sinnvoll, eggs[5] aber nicht erlaubt. In Rust kann man ebenfalls eine gewünschte Datenstruktur bauen, zum Beispiel mit einer Index<BirdSpecies>-Implementierung, und dann funktioniert eggs[Robin], während eggs[5] ein Fehler ist. Rust unterstützt das aber nicht direkt in der Sprache als „Array“. Gerade wenn man wie in Ada benutzerdefinierte Typen als Teilmengen ganzzahliger Typen deklarieren kann, kommt diese Form der Indexierung besonders gut zur Geltung. In Rust kann man reine benutzerdefinierte Typen für bereichsbeschränkte Integer bislang noch nicht bauen; intern gibt es nur Dinge wie NonZeroI16. Es wäre wirklich großartig, wenn Rust so etwas irgendwann unterstützen würde
    • Ada hat ebenfalls eingebaute Unterstützung für HashMaps und Set-Strukturen. Standard zu Ada-Containern (siehe Abschnitt A.18). Dass man für Array-Indizes typische „zusammenhängende Wertebereiche“ wie etwa 0 bis N-1 verwenden kann, ist in Situationen mit dichten Maps oder wenn zusammenhängender Speicherzugriff wichtig ist, gegenüber Dictionaries ein großer Vorteil, weil es viel schneller und cache-effizienter sein kann
    • Die Einschränkung des Index-Typs bei Arrays in Ada über einen Subtype ist strukturell ein völlig anderes Konzept als ein Dictionary. Die Sprache kann dadurch sogar die Menge zulässiger Indexwerte direkt einschränken