1 points par GN⁺ 2025-10-05 | 1 commentaires | Partager sur WhatsApp
  • Compare les différences et caractéristiques apparues en résolvant des problèmes Advent of Code avec Ada et Rust
  • Analyse les écarts entre la conception de ces deux langages, centrée sur la sécurité et la fiabilité, et leurs approches concrètes de l’écriture de programmes
  • Les différences ressortent sous divers angles, comme la bibliothèque standard de chaque langage, les fonctionnalités intégrées, les écarts de performance et le style de gestion des erreurs
  • Explique, à l’aide d’exemples de code concrets, des cas pratiques rencontrés lors de l’écriture et de l’exploitation du code autour de la modularité, des génériques, des boucles et de la gestion des erreurs
  • Les différences d’expérience de développement sont particulièrement marquées par le typage statique, le traitement des tableaux et les interfaces de gestion des erreurs

Introduction et objectif

  • Après avoir utilisé uniquement Ada pour résoudre les problèmes Advent of Code (ci-après AoC), l’auteur a commencé à écrire aussi des solutions en Rust et en Modula-2 à partir de 2023, ce qui lui a permis de les comparer directement
  • En portant des solutions auparavant centrées sur Ada vers Rust, il a ressenti les différences structurelles entre les deux langages et leurs approches propres
  • L’objectif est de clarifier les écarts d’usage réels du point de vue de la sécurité du code, de la fiabilité et de la conception du langage

Versions des langages utilisées pour la comparaison

  • Ada 2022 (avec, selon les besoins, référence à certaines règles de Spark 2014)
  • Rust 2021 (comparaison principale basée sur Rust 1.81.0)

Fonctionnalités exclues et critères de comparaison

  • Les fonctionnalités emblématiques de chaque langage (= killer features) sont brièvement mentionnées dans le texte sous forme de commentaires
  • Certaines fonctionnalités n’ont pas été traitées, selon l’expérience personnelle de l’auteur et les besoins concrets de chaque solution
  • L’auteur cherche à écarter autant que possible les avis personnels pour se concentrer sur les caractéristiques principales

Contexte et point de vue de l’auteur

  • En tant qu’utilisateur non natif d’Ada comme de Rust, l’auteur s’appuie sur une expérience des langages des années 1980 comme C/C++, Pascal et Modula-2
  • En conséquence, son style de code peut différer des styles modernes ou idiomatiques
  • Les implémentations ne sont pas forcément optimales et, selon le contexte, certaines solutions choisies peuvent être intuitives ou au contraire peu conventionnelles

Positionnement d’Ada et de Rust

  • Ada reste un langage de développement système/embarqué extrêmement sûr et fiable, avec un fort accent sur la lisibilité du code
  • Rust met en avant la sûreté mémoire et les atouts de la programmation système, et a été désigné pendant plusieurs années comme le « langage le plus apprécié » dans l’enquête développeurs de Stack Overflow
  • Ada est un langage généraliste de haut niveau, offrant un spectre particulièrement adapté à la lecture et à la maintenance
  • Rust vise le développement de programmes système de bas niveau, avec une culture de programmation sûre fondée sur la gestion explicite de la mémoire et les types d’erreur/option

Comparaison des caractéristiques de sécurité et de structure

  • Ada

    • Norme ISO (spécification rigoureuse)
    • Il est facile de définir des types adaptés aux caractéristiques du problème (plage, nombre de chiffres, etc.)
    • Les indices de tableau ne sont pas forcément numériques
    • Il existe une spécification encore plus stricte appelée Spark
  • Rust

    • La spécification repose surtout sur la documentation officielle (Reference) et le compilateur
    • Les déclarations de types dépendent des types machine (par ex. f64, u32)
    • L’indexation de tableaux est naturellement limitée aux types numériques

Résumé principal du tableau des fonctionnalités / intégration

  • Différences dans la prise en charge du contrôle des bornes des tableaux, des conteneurs génériques, de la concurrence, des boucles étiquetées et du pattern matching
  • Ada repose sur les exceptions pour la gestion des erreurs, tandis que Rust privilégie une approche par valeur de retour via les types Result/Option
  • Rust se distingue nettement par la prise en charge des macros, du pattern matching et d’une certaine pureté fonctionnelle
  • Ada prend en charge la conception par contrat ainsi que la vérification à la compilation DBC (Design By Contract) dans Spark
  • En matière de sûreté mémoire, Rust et Spark l’imposent, tandis qu’Ada autorise l’usage de pointeurs nuls

Comparaison des performances et du temps d’exécution

  • Rust a généralement la réputation d’être rapide à l’exécution mais lent à compiler, tandis qu’Ada est vu à l’inverse comme rapide à compiler, avec des performances à l’exécution pouvant être un peu moindres selon les vérifications activées
  • D’après les benchmarks, Rust a rencontré un dépassement sur le problème day24 à cause des limites du type f64, alors qu’Ada a pu utiliser des types de haut niveau comme digits 18, permettant une sélection automatique d’un type machine adapté, l’évitement du dépassement et d’excellentes performances
  • Rust doit recourir à f128 non stable ou à une bibliothèque externe, alors qu’Ada peut prendre l’avantage simplement en spécifiant un type conforme aux capacités du compilateur

Traitement des fichiers et gestion des erreurs (Case Study 1)

Traitement des fichiers en Ada

  • Utilisation de Ada.Text_IO par défaut
  • Ouverture explicite du fichier, lecture ligne par ligne, traitement de plages souhaitées ou de lignes par position, de manière relativement intuitive
  • En cas d’erreur, le traitement se fait par exceptions plutôt que via des messages explicites dans la signature de fonction, et la possibilité d’erreur n’apparaît pas dans cette signature

Traitement des fichiers en Rust

  • Utilisation de std::fs::File et de BufReader
  • À l’ouverture du fichier, retour d’un type Result, ce qui rend clairement visible la possibilité d’erreur
  • Pas de prise en charge directe de l’indexation par caractère, traitement obligatoirement via des Iterator
  • Les outils fonctionnels et itératifs comme map, filter, collect, sum sont centraux, avec prise en charge de diverses macros (par ex. include_str!)
  • En déclarant explicitement les erreurs dans le type de retour, Rust assure une propagation claire des erreurs au niveau des fonctions

Modularité et génériques (Case Study 2)

Modularité en Ada

  • Séparation claire entre spécification (interface) et implémentation sur la base des packages
  • Renforcement de la modularité via des sous-packages et une combinaison de la syntaxe use/rename pour ajuster la lisibilité
  • Prise en charge des generic dans les packages : généralisation de types, constantes ou sous-packages entiers

Modularité en Rust

  • Organisation modulaire via le système mod/crate, avec distinction spécification/implémentation automatisée par le générateur de documentation
  • Contrôle d’accès déclaratif avec pub/private
  • Combinaison de use/as pour les imports et renommages
  • Prise en charge intégrée des tests, permettant de déclarer directement des modules de test dans le code, de les compiler et de les exécuter automatiquement

Génériques

  • Ada ne prend en charge les génériques qu’au niveau des packages/procédures (pas au niveau d’un type seul)
  • Rust permet d’appliquer des génériques aux types eux-mêmes (approche de type template)
  • Ada permet d’exprimer clairement des propriétés supplémentaires comme l’étendue d’un type via les types de plage et sous-types, tandis que Rust s’appuie sur des constantes d’instance

Comparaison des types énumérés (Case Study 3)

  • Ada offre une déclaration concise tout en prenant automatiquement en charge les types discrets, l’ordre et leur utilisation dans les boucles/indices
  • Les enum de Rust ont une déclaration similaire, mais leur exploitation via le pattern matching et l’itération demande une approche plus explicite

Conclusion

  • Ada offre un contrôle plus strict sur les types de spécification de haut niveau, la vérifiabilité et les contrôles à l’exécution
  • Rust l’emporte en matière de confort de développement et de sécurité grâce au style de programmation fonctionnel, à la métaprogrammation par macros et à la gestion des erreurs assistée par le compilateur
  • Dans la résolution de problèmes concrets, Ada montre ses forces en compatibilité avec l’ancien code et en maintenance, tandis que Rust bénéficie d’un écosystème d’outils moderne ainsi que d’avantages en sûreté et en parallélisme

1 commentaires

 
GN⁺ 2025-10-05
Réactions sur Hacker News
  • C’est regrettable qu’Ada, malgré de très bonnes idées, ait surtout été utilisé dans des domaines où la sûreté est absolument critique. En particulier, la possibilité de restreindre la plage de valeurs des types numériques est très utile pour prévenir certains bugs. SPARK Ada était à la fois facile à apprendre et simple à appliquer au développement de logiciels conformes au niveau SIL 4 (la norme de sûreté logicielle la plus stricte). Depuis des décennies, l’industrie du logiciel a foncé dans une logique de « croissance d’abord, stabilité ensuite », mais on sent aujourd’hui un retour vers le développement de logiciels sûrs. On peut espérer que toutes les leçons accumulées autour de la sûreté débouchent sur de meilleurs langages. En réalité, il est fréquent que de bonnes idées restent cachées dans des langages mineurs puis disparaissent
    • Quand on fait du développement logiciel depuis longtemps, on se rend compte à quel point la « réinvention de la roue » est courante. Ada et Rust se ressemblent dans leur recherche de sûreté, mais leur définition et leur périmètre diffèrent. Rust poursuit de manière très intense une forme de sûreté cruciale mais très ciblée, tandis qu’Ada adopte une définition plus large et plus concrète de la sûreté. Quand j’ai appris Ada au début des années 1990, la critique la plus fréquente était que le langage était trop gros et trop complexe, ce qui ralentissait le développement (à l’époque, un compilateur Ada 83 certifié coûtait environ 20 000 dollars actuels par personne). Mais les temps ont changé, et tout le monde admet désormais qu’un langage grand et complexe comme Rust est nécessaire pour une vraie programmation concurrente sûre
    • Nim aussi prend en charge les subranges, inspirés d’Ada et de Modula, pour restreindre la plage de valeurs d’un type
      type
        Age = range[0..200]
      
      let ageWorks = 200.Age
      let ageFails = 201.Age
      
      À la compilation, une erreur indique que 201 ne peut pas être converti vers le type Age
      Explication des subranges en Nim
    • Ada (avec GNAT) prend en charge l’analyse dimensionnelle / des unités physiques à la compilation, autrement dit la vérification des unités. C’est extrêmement pratique en ingénierie, et on peut se demander pourquoi les autres langages ne proposent cette fonctionnalité importante que via des bibliothèques tierces
      Documentation associée
    • En C++ aussi, il est facile de créer par code des types numériques à plage de valeurs restreinte (ce n’est pas dans la bibliothèque standard, mais c’est très simple à implémenter soi-même). Certaines vérifications de sûreté peuvent se faire à la compilation plutôt qu’à l’exécution. J’aimerais que tous les langages prennent ce genre de fonctionnalité en charge nativement
    • Ce qui me plaît le plus dans Ada, c’est son approche claire de la programmation orientée objet (OOP). La plupart des langages entassent les concepts OOP dans un bloc monolithique appelé « classe », alors qu’Ada permet de choisir séparément le passage de messages, le dispatch dynamique, le sous-typage, les génériques, etc. J’aimais beaucoup la manière élégante dont ces fonctionnalités s’imbriquent
  • L’auteur cite comme différence le fait qu’Ada a une spécification officielle et Rust non, mais du point de vue des utilisateurs, l’adoption du langage et son écosystème (outillage, bibliothèques, communauté) comptent davantage. Ada a réussi dans l’aérospatial, la sûreté et d’autres domaines, et convient bien à l’AoC ou au bas niveau embarqué, mais dans de vrais projets (systèmes distribués, composants d’OS, etc.), des éléments comme les formats de données, les protocoles, le support IDE et la collaboration avec les collègues ont plus de poids. Au final, au moment de choisir un langage, ce sont souvent ces aspects environnementaux qui sont décisifs
    • Récemment, Rust a aussi reçu de Ferrocene un document de spécification inspiré du style de spécification d’Ada. Il est public, on peut donc s’y référer
      Spécification Rust
    • Rust comme Ada restent faibles si l’on parle de « spécification formelle » au sens strict, c’est-à-dire d’un document prouvable mécaniquement. Même SPARK Ada repose sur des hypothèses sémantiques sur le langage, et cela n’est pas non plus entièrement formel ni lisible par machine
    • Les développeurs de logiciels de contrôle aéronautique répondraient probablement : « si ce n’est pas important dans un environnement réel, alors oui, notre processus est excessif ». Dans les domaines où la sûreté est critique, des langages et processus stricts comme ceux d’Ada sont justement la norme
  • J’ai trouvé intéressant que, même si Ada est moins riche que Rust sur certains aspects liés aux types, Ada soit souvent meilleur en lisibilité du code. Le texte comparatif ne parlait pas de la vitesse de compilation ; l’idée qu’Ada soit un langage complexe appartient peut-être au passé, et la comparaison avec Rust n’est plus forcément en sa défaveur aujourd’hui. Cet article m’a donné envie d’essayer Ada sur un vrai projet
    • Je me demande ce que signifie exactement « faiblesse liée aux types ». D’après mon expérience, le système de types d’Ada est extrêmement expressif. Types à plages de valeurs définies par l’utilisateur, tableaux indexés par des énumérations arbitraires, définition d’opérateurs par type, ajout de vérifications à la compilation ou à l’exécution, préconditions, postconditions, etc. Il y a aussi les discriminated records, les clauses de représentation de structure, et bien d’autres choses. Ce n’est pas une faiblesse, c’est au contraire très puissant
  • Je voudrais parler des différences de chaînes de caractères entre Ada et Rust. Ada a été conçu au début des années 1980, à une époque où une « chaîne » était un tableau de char, donc l’indexation directe comme sur un tableau d’octets est simple. Rust a été conçu avec Unicode en tête, donc ses chaînes sont fondamentalement du texte encodé en UTF-8, c’est-à-dire du vrai « texte ». Ada permet donc une indexation aléatoire comme un tableau, alors qu’en Rust la notion de chaîne est différente, même s’il reste possible de passer par un tableau d’octets
    • Les chaînes Unicode intégrées d’Ada sont généralement des tableaux UTF-32. Contrairement à Rust, Ada ne fournit pas directement de littéraux UTF-8 ; il faut convertir depuis des tableaux 8/16/32 bits
    • Les chaînes Rust peuvent aussi être indexées. Mais Rust ne traite pas les chaînes comme de simples tableaux et privilégie les slices de sous-chaînes. Si on découpe au milieu d’un caractère, cela provoque un panic (cas où l’on viole la frontière d’une valeur encodée Unicode). Dans un cas comme AoC où l’on utilise toujours de l’ASCII, mieux vaut employer un slice d’octets via [u8] ou la méthode str::as_bytes
  • La remarque de l’auteur disant que Rust « ne prend pas en charge la programmation concurrente par défaut » me paraît étrange. Rust intègre les threads dans le langage, et ils sont même plus simples à utiliser que l’async. Cela ne devient problématique que lorsqu’on a besoin d’un très grand nombre de threads au point d’atteindre des limites de ressources ; pour la grande majorité des logiciels, les threads intégrés suffisent largement
    • (Je ne suis pas utilisateur de Rust, donc je pose la question sincèrement.) En Rust, quelle est la différence entre la gestion de l’annulation avec les threads et avec l’async, et en quoi cela diffère-t-il de l’async dans d’autres langages ? En C++, Python et C#, la gestion de l’annulation en async m’a semblé bien meilleure qu’avec les threads. J’ai entendu dire qu’en Rust, comme cette gestion n’est pas traitée via des exceptions, c’est au contraire un peu plus difficile ; j’aimerais savoir ce qu’il en est en pratique. Je serais aussi curieux d’entendre comment Ada gère cela
    • Je me demande à partir de quel point un scheduler à work stealing comme Tokio devient réellement plus rapide que le simple fait de lancer plusieurs threads. C’est un peu comme avec un tableau simple (par exemple VecMap) qui est rapide pour peu d’éléments, mais au-delà d’un certain seuil une autre structure devient plus efficace. J’aimerais savoir où se situe concrètement le point où le work stealing devient avantageux
    • En pratique, la principale raison d’utiliser l’async est souvent que les crates tierces qu’on utilise sont async. (Par exemple, Reqwest nécessite Tokio.) Dans le développement d’applications de haut niveau, si l’on s’obstine à n’utiliser que du non-async, on finit par se heurter à des limites
    • Sur les plateformes où le support des threads est faible (WASM, embarqué, etc.), l’async est plutôt plus adapté. L’idée de « centaines de milliers de personnes qui visitent un blog en même temps » est peu réaliste ; c’est presque exagéré de justifier le besoin d’async surtout avec ce type de scénario
  • J’ai trouvé intéressant qu’il existe aussi un compilateur open source pour Ada. Je pensais autrefois qu’il n’existait que des compilateurs propriétaires, donc je ne m’étais jamais intéressé à Ada ; il va falloir que je regarde ça à nouveau
    • Le compilateur GNAT existe depuis plus de 30 ans. À une époque, l’absence d’exception d’exécution GPL avait fait naître l’idée erronée que les binaires produits devaient eux aussi être sous GPL, mais ce problème est désormais réglé
    • GNAT a été construit sur GCC dès les années 1990, et certaines universités l’utilisaient directement dans des cours très pratiques comme la programmation temps réel. J’ai moi-même essayé d’utiliser Ada pour l’initiation à la programmation avant de basculer rapidement vers Pascal puis C++
  • Parmi les projets qui m’ont récemment intéressé dans l’impression 3D, il y a Prunt, une carte de contrôle et un firmware d’imprimante. Le firmware est développé en Ada, ce qui est assez original tout en étant conceptuellement très cohérent
    Site de Prunt
    Prunt sur GitHub
  • À la fin de la Case Study 2, il est dit que « si le client doit connaître SIDE_LENGTH, alors il faut ajouter une fonction qui le renvoie », mais une déclaration de constante comme pub const SIDE_LENGTH: usize = ROW_LENGTH; serait une solution plus directe qu’une fonction
  • Je ne suis pas d’accord avec l’idée que les deux langages encouragent une programmation centrée sur la pile. Ada recommande au contraire activement l’allocation statique
  • J’ai été surpris que la possibilité, en Ada, d’avoir des types arbitraires comme indices de tableau soit présentée comme un avantage majeur. Presque tous les langages ont un dictionnaire (table de hachage) dans leur bibliothèque standard, et Rust en propose deux
    • Ici, on parle de tableaux intégrés au langage. Par exemple, en Ada, on peut définir le type d’indice du tableau « eggs » comme BirdSpecies, ce qui rend eggs[Robin] et eggs[Seagull] valides, tandis que eggs[5] est interdit. En Rust aussi, on peut créer la structure de données voulue (par exemple en implémentant Index<BirdSpecies>), de sorte que eggs[Robin] fonctionne mais eggs[5] provoque une erreur. En revanche, Rust ne le prend pas directement en charge comme « tableau » au niveau du langage. Ce type d’indexation devient particulièrement intéressant quand, comme en Ada, on peut déclarer un type défini par l’utilisateur comme un entier restreint à un sous-ensemble. En Rust, on ne peut pas encore créer de purs types utilisateurs de ce genre, comme des entiers à plage bornée (il n’existe en interne que des types comme NonZeroI16). Ce serait excellent si Rust allait jusque-là
    • Ada fournit aussi des hash maps et des ensembles en standard. Norme sur les conteneurs Ada (voir la section A.18). Le fait de pouvoir utiliser comme type d’indice de tableau une plage de « valeurs consécutives » typiques (par exemple 0 à N-1) constitue un avantage net par rapport à un dictionnaire dans les cas où l’on veut une structure dense ou un accès mémoire contigu, car c’est bien plus rapide et plus efficace pour le cache
    • La restriction du type d’indice de tableau en Ada (subtype) est, structurellement, un concept totalement différent d’un dictionnaire. Le langage peut aller jusqu’à restreindre les catégories mêmes de valeurs autorisées comme indices de tableau.