n°3
Novembre
2008
Ada pour les nuls

Disons-le franchement, si vous vous considérez comme une nullité en programmation, Ada n'est pas fait pour vous...

Ada est un langage qui enthousiasme ses utilisateurs, mais qui pourtant n'a pas su convaincre une grande partie des développeurs.C'est qu'il joue un rôle particulier dans le processus de développement: Ada a été conçu pour aider les développeurs à produire des logiciels de qualité industrielle, en renforçant les principes du génie logiciel. Ce qui n'implique pas qu'Ada soit déplaisant à utiliser, bien au contraire! Mais alors que la plupart des langages se vantent de la quantité de choses qu'ils vous permettent de faire, Ada met l'accent sur ce qu'il vous empêche de faire : des bêtises!

Le compilateur assure, en plus de son rôle de traduction en langage machine, de véritables contrôles de cohérence qui ne laissent pas passer nombre de fautes de conception; c'est un véritable outil de génie logiciel, et non un simple traducteur. Ceci tend à rebuter les nouveaux venus qui ont l'impression que le compilateur les empêche de faire "ce qu'ils veulent". Ils ne perçoivent les avantages de cette approche que plus tard, car comme le disent (de façon à peine exagérée) les utilisateurs convaincus: "en Ada, si ça compile, ça marche". Soyons honnête, il reste toujours des erreurs de logique inévitable, mais au moins, le langage vous protège de toutes les erreurs évitables.

Comment? C'est ce que nous allons essayer de montrer maintenant.

Un (bref) historique

Ada est un langage algorithmique d'une puissance d'expression considérable, dérivé de Pascal dont il a retenu les structures de contrôle et certains types de données. Il a été conçu suite à un appel d'offres du département américain de la Défense, d'après un cahier des charges très strict, par une équipe française dirigée par Jean Ichbiah. La première version du langage (Ada 83) synthétisait l'état de l'art de l'époque en fournissant un langage à la syntaxe lisible, mettant l'accent sur le typage fort, la modularisation, l'encapsulation des données, la réutilisabilité, la sécurité (statique, et dynamique par les exceptions) et la programmation parallèle et temps-réel.

Une révision majeure est intervenue en 1995 (Ada 95), qui a apporté l'héritage, de nouvelles possibilités de synchronisation, le support des systèmes distribués et une bibliothèque d'exécution plus étendue. La notion d'annexe a été introduite pour fournir des services complémentaires et optionnels pour certains domaines particuliers (programmation système, temps-réel, systèmes d'information...).

Enfin un amendement est intervenu en 2005 (Ada 2005), standardisé en fait début 2007, qui a remis le langage à la pointe des nouvelles technologies avec la notion d'interfaces, de nombreux perfectionnements pour la programmation temps-réel, et de nouvelles bibliothèques.

Ada est donc un langage bien vivant et qui suit l'évolution des technologies.

Syntaxe de base

Le programme ci-dessous est un premier exemple qui imprime "Bonjour", "Bon après-midi", ou "Bonsoir" en fonction de l'heure de la journée.

with Text_IO;  use Text_IO;
with Calendar; use Calendar;
procedure Bonjour is
Heure : Day_Duration;
begin
Heure := Seconds(Clock)/3600;
if Heure < 12.00 then
Put_Line("Bonjour");
elsif Heure < 19.00 then
Put_Line ("Bon après-midi");
else
Put_Line ("Bonsoir");
end if;
end Bonjour;

On remarquera que toutes les instructions sont "fermées" par un end, ce qui rend inutile les regroupements par {...} que l'on rencontre partout dans les langages à base syntaxique C. Les clauses with et use en tête de la procédure permettent d'utiliser deux paquetages (package) qui fournissent respectivement l'accès aux fonctionnalités d'entrées-sorties et à l'utilisation du temps. Sous-programmes et paquetages constituent des unités de compilation, c'est à dire des structures pouvant être compilées séparément. Un programme complet est donc constitué d'un ensemble d'unités de compilation, fournissant ou utilisant des fonctionnalités aux autres unités. D'une façon générale, toute unité de compilation doit citer au début les autres unités qu'elle utilise au moyen d'une clause with: les dépendances entre unités sont toujours explicites, ce qui facilite grandement les analyses de dépendance dans les programmes.

Le mode de passage des paramètres de sous-programmes fait référence à l'usage qui en est fait, et non à la méthode de passage: les paramètres peuvent ainsi être déclarés in (lecture seulement), out (écriture seulement), ou in out (lecture et écriture). Ceci remplace avantageusement la notion de "passage par valeur" ou de "passage par référence" utilisée en C++. Par exemple:

procedure Decoder (Depuis : in String; Position : in out Positive; Valeur : out Float);

Cette procédure décode un nombre flottant depuis la chaîne "Depuis", à la position "Position" (qui est mise à jour en fonction des caractères lus), le résultat étant mis dans "Valeur". La lisibilité est encore améliorée en appelant cette chaîne en notation dite "nommée":

Decoder (Depuis => Chaine_Entree, Position => Index_Courant, Valeur => Resultat);

Typage fort

Un type définit un ensemble de valeurs muni d'un ensemble d'opérations portant sur ces valeurs. En fait, un type Ada représente des entités de plus haut niveau que les types d'autres langages: il représente une entité du domaine de problème, et non une entité machine. Le programmeur exprime les exigences de son problème; si par exemple il doit représenter des temps avec une précision absolue de 1 ms sur une année et des longueurs avec une précision relative de 10-5 jusqu'à 1025m., il déclarera:

type TEMPS is delta 0.001 range 0.0 .. 86400.0*366;
type LONGUEUR is digits 5 range 0.0 .. 1.0E25;

Puisqu'il s'agit d'entités de nature différente, deux variables de types différents sont absolument incompatibles entre elles, même lorsqu'il s'agit de types numériques. Etant donné:

type Age   is range  0 .. 125; -- Soyons optimistes
type Etage is range -3 .. 10;

une variable de type "Age" est incompatible avec une variable de type "Etage". Autrement dit, en Ada, on ne peut pas "mélanger des choux et des carottes". Ada est le seul langage à offrir cette propriété, qui paraît tout à fait naturelle aux débutants en informatique... et pose parfois des problèmes aux programmeurs expérimentés.

Ada offre des structures de données très puissantes pour la gestion des tableaux et la paramétrisation des types de données, sans équivalent dans les autres langages. Leur description complète serait trop longue pour cet article, mais notons qu'ils permettent d'avoir des données de taille choisie à l'exécution, sans recourir à l'allocation dynamique (malloc), et tout en préservant la sécurité (aucun débordement de tableau n'est possible).

Modularisation

Une notion centrale en Ada est celle de paquetage (package). Le paquetage permet de regrouper dans une seule entité des types de données, des objets (variables ou constantes), et des sous-programmes (procédures et fonctions) manipulant ces objets. Le paquetage comporte une partie visible comportant les informations utilisables à l'extérieur du paquetage, et une partie privée (cachée) qui regroupe les détails d'implémentation auxquels les utilisateurs du paquetage n'ont pas accès.

Les paquetages peuvent être compilés séparément, et seule leur spécification est nécessaire pour permettre l'utilisation du paquetage. On réalise ainsi une séparation complète entre les spécifications d'une unité fonctionnelle et son implémentation. L'exemple ci-dessous montre la spécification d'un paquetage de gestion de nombres complexes:

package Nombres_complexes is
type Complex is private;
I : constant Complex;

function "+" (X, Y : Complex) return Complex;
function "-" (X, Y : Complex) return Complex;
function "*" (X, Y : Complex) return Complex;
function "/" (X, Y : Complex) return Complex;
function Cmplx (X, Y : Float) return Complex;
function Polar (X, Y : Float) return Complex;
private
type Complex is
record
Reel : Float;
Imag : Float;
end record;
I : constant Complex := (0.0, 1.0);
end Nombres_complexes;

Il est possible de créer des paquetages "enfant" qui rajoutent des fonctionnalités à des paquetages existant sans avoir à toucher à leurs "parents", donc sans recompilation des utilisateurs de ces parents. Ceci permet une meilleure séparation en fonctionnalités "principales" et fonctionnalités "annexes", facilitant l'utilisation aussi bien que la maintenance.

Génériques et réutilisation

Une autre propriété très importante du langage Ada est la notion d'unités génériques. Il s'agit d'unités paramétrisables, qui permettent de définir un algorithme indépendamment des types d'objet manipulés. L'idée des génériques a été par la suite reprise en C++ avec la notion de template. Notons cependant une différence importante avec C++: les génériques Ada sont compilés et vérifiés à la compilation; toutes les instanciations ultérieures fonctionneront forcément correctement (ou bien elles ne compileront pas si elles ne respectent pas le contrat demandé). Par exemple, le générique suivant permet d'obtenir une procédure qui permute ses arguments:

generic
type Elem is private;
procedure Swap (X,Y : in out Elem);

procedure Swap(X,Y : in out Elem) is
Temp : Elem;
begin
Temp := X;
X := Y;
Y := Temp;
end Swap;

Exceptions

Les exceptions fournissent un moyen commode de traiter tout ce qui peut être considéré comme "anormal" ou "exceptionnel" dans le déroulement d'un programme. La plupart des langages récents fournissent la notion d'exception, mais la "granularité" de l'exception est variable: n'importe quel objet peut être "jeté" en C++, alors qu'en Java seuls les objets de la classe throwable peuvent l'être. Les exceptions Ada sont plus simples; ce sont de simples identifications, qui peuvent être "levées" en leur adjoignant éventuellement une simple chaîne de caractères. Ce choix a été motivé par la nécessité de conserver une bonne efficacité à ce mécanisme dans les contextes temps réel.

Héritage et programmation orientée objet

Comme C++, mais contrairement à Java et C#, Ada n'impose pas la programmation orientée objet, mais la fournit à ceux qui en ont besoin. Le modèle est original, et fondé sur la notion de type étiqueté (tagged). Ces types fournissent les fonctionnalités classiques de la POO:

  • l'extension de types. Il est possible d'étendre le type de donnée en rajoutant des champs supplémentaires. Les opérations du type parent sont héritées et redéfinissables.
  • Le polymorphisme. Des sous-programmes peuvent posséder des paramètres se référant à la classe d'un type étiquetté: ils sont applicables alors non seulement au type d'origine, mais également à tous les types qui en héritent.
  • Les liaisons dynamiques. Si on appelle un sous-programme en lui fournissant un paramètre réel de type classe, le sous-programme effectivement appelé n'est pas connu à la compilation. C'est l'étiquette (tag) de l'objet réel qui détermine à l'exécution le sous-programme effectivement appelé.
  • Les types abstraits. Il est possible de définir des types munis de l'attribut abstract, qui ne peuvent servir à définir des objets, mais seulement de racine d'arbres de dérivation pour des types concrets définis plus tard.
  • Les interfaces. Ce sont des types abstraits spéciaux (sans données), qui fonctionnent comme les interfaces Java: un type étiqueté peut implémenter plusieurs interfaces simultanément. Ada offre de plus des interfaces synchronisées qui étendent la notion aux tâches et objets de synchronisation.

Remarquons que, contrairement aux autres langages orientés objets, la POO en Ada n'exige aucune allocation dynamique: il est donc possible d'effectuer des liaisons dynamiques sans imposer de pointeurs, qu'ils soient cachés (comme en Java) ou non.

Parallélisme

Le langage Ada permet de définir des tâches, qui sont des unités de programme s'exécutant en parallèle (comme les threads Java). C'est une fonctionnalité obligatoire, totalement intégrée au langage, et donc fournie par tous les compilateurs. Les tâches sont des objets appartenant à des types tâche, et peuvent donc être membres de structures de données: on peut ainsi définir des tableaux de tâches, des pointeurs sur des tâches, etc.

Deux moyens permettent aux tâches de se synchroniser et d'échanger des données: l'un est orienté traitement, c'est le rendez-vous; l'autre est orienté données, ce sont les objets protégés. Noter que ce sont des moyens de communications de haut niveau, beaucoup plus faciles à utiliser que les variables de condition de POSIX ou les méthodes synchronized de Java.

Dans le rendez-vous, une tâche exporte des services (appelés entrées) qui pourront être utilisés par d'autres tâches. Par exemple, un tampon simple se déclarera comme:

task Tampon is
entry Lire (C : out Character);
entry Ecrire(C : in Character);
end Tampon;

C'est la tâche elle même qui décidera (au moyen d'une instruction accept) quand elle est prête à prendre en compte la requète. C'est donc fondamentalement un modèle client-serveur.

Un objet protégé, en revanche est un type de donnée dont les opérations s'effectuent en exclusion mutuelle, comme dans des moniteurs de Hoare (ou des classes synchronized de Java). Mais les services fournis peuvent de plus être munis d'une condition, appelée garde, qui ne s'ouvre que lorsqu'une condition est remplie. On peut ainsi écrire de façon simple (et extrèmement efficace) des objets de synchronisation, comme la barrière simple suivante:

protected Barrière is 
entry Passer;
procedure Ouvrir;
function En_Attente return Natural;
private
Ouverte : Boolean := False;
end Barrière;

protected body Barrière is
entry Passer is
begin
if Passer'Count = 0 then -- Le dernier referme
Ouverte := False; -- la barrière.
end if;
end Passer;

procedure Ouvrir is
begin
if En_attente > 0 then
Ouverte := True;
end if;
end Ouvrir;

function En_Attente return Natural is
begin
return Passer'Count;
end En_Attente;
end Barrière;

Ada supporte de nombreuses méthodes d'ordonnancement (priorités, EdF, budgets de temps...) qui peuvent même cohabiter dans une même application selon le niveau de priorité des tâches.

Gestion du bas niveau

Ada étant un langage également destiné à la programmation des systèmes, il fournit des fonctionnalités pour gérer le bas niveau. On peut ainsi de forcer la représentation physique des structures abstraites au bit près (de façon beaucoup plus précise que les bitfields de C++); par exemple, si une donnée sur 16 bits comporte un numéro d'identification sur 3 bits et une valeur sur 13 bits, on pourra les déclarer ainsi:

type Num_ID is range 0 .. 7;
type Valeur is range 0 .. 2**13-1;
type Donnee is
record
Num : Num_Id;
Val : Valeur;
end record;

for Donnee use -- clause de représentation
record
Num at 0 range 0 .. 2; -- Mot 0, bits 0 à 2
Val at 0 range 3 .. 15; -- Mot 0, bits 3 à 15
end record;

L'utilisateur pourra accéder au champ Num d'une variable D normalement, par la notation D.Num, sans se préoccuper de la représentation de bas niveau: c'est le compilateur qui se chargera d'extraire le champ de bits correct. On voit qu'ainsi, il n'est plus jamais besoin de calculer des masques, décalages, etc. pour intervenir à bas niveau. En cas de modification de la représentation interne des données, aucune modification du code n'est nécessaire, en dehors de l'ajustement de la clause de représentation.

Il est possible de spécifier des traitements d'interruptions physiques, d'inhiber des vérifications de type, et même d'inclure du code machine. Toutefois, des précautions ont été prises pour que même ce genre de manipulations ne puisse se faire sans un minimum de précaution et de sécurité.

Ada est également le seul langage qui définisse, au niveau de sa norme, les moyens de s'interfacer avec des bibliothèques écrites dans d'autres langages de programmation. Grâce à cette fonctionnalité, toutes les bibliothèques classiques (X-Window, Win32, GTK, Qt ...) sont disponibles pour Ada.

Distribution

L'annexe "systèmes distribués" définit certains éléments supplémentaires permettant d'écrire des systèmes distribués en Ada. Ada est le premier langage à inclure un tel modèle au niveau de la définition du langage, donc de façon portable et indépendante des systèmes d'exploitation ou des exécutifs sous-jacents (Java offre un modèle de distribution, mais il est lié au protocole Internet).

Un programme Ada est constitué d'un ensemble de "partitions". Les partitions peuvent être actives ou passives. Les paquetages constituant le programme sont classifiés de façon à assurer leur mise en œuvre automatique en environnement distribué; par exemple, les paquetages remote call interface offrent des services appelables à distance, sans que le programmeur ait à se préoccuper du mécanisme sous-jacent. Il est même possible de compiler le même programme, au choix, comme un seul exécutable pour une exécution locale, ou comme un ensemble de partitions pour une exécution distribuée.

Le modèle de distribution permet également de définir des objets (au sens de la POO) distribués, toujours de façon transparente pour le programmeur.

Le compilateur GNAT

Comment essayer Ada? Rien de plus simple. GNAT est une version du célèbre compilateur libre Gcc qui offre une implémentation complète du langage. Si cet article vous a donné envie d'essayer Ada (ou si vous avez des doutes sur la réalité des bonnes choses qui ont été présentées...), il suffit de le télécharger et d'essayer par vous-mêmes. Mais un bon conseil: lisez quelques-uns des guides ou des tutoriaux référencés ci-dessous. Le but d'Ada est de vous permettre de découvrir une nouvelle approche du développement logiciel, plus sûre et plus efficace. Cela nécessite un petit effort pédagogique...

Références :

Ressources :

Groupes d’utilisateurs :

Groupes de news :

  • En français : news:fr.comp.lang.ada
  • En anglais : news:comp.lang.ada

Jean-Pierre ROSEN (ADALOG)