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)