Ada
Introduction au langage Ada
Avant propos
Avertissement
Ceci n'est qu'une introduction au langage Ada, car il existe des livres entiers avec des centaines de pages sur ce sujet ! Par conséquent ne vous attendez pas à faire le tour de la question... Ce n'est pas non plus un cours de programmation : cet article s'adresse à des personnes possédant des bases de programmation et voulant se mettre à Ada. Cela dit, le langage est relativement simple à comprendre et pourrait constituer un bon choix pour se lancer dans le merveilleux monde du développement. :-)
Présentation du langage
Ada est un langage de programmation conçu en réponse à un cahier des charges établi par le département de la Défense des Etats-Unis. Son développement a commencé au début des années 1980 (Ada 83). Il a été ensuite repris et amélioré au milieu des années 1990, pour donner Ada 95, le premier langage objet standardisé de manière internationale.
Le nom Ada a été choisi en honneur d'Ada Lovelace. Ada est un langage à typage statique, modulaire et offrant une syntaxe claire non ambigûe inspirée de Pascal. Il est souvent utilisé dans des systèmes temps-réel et/ou embarqués nécessitant un haut niveau de fiabilité (armée, transports ferroviaires, technologies aéronautiques, technologies spatiales...). Ada est un langage assez strict qui oblige à coder proprement, il est souvent enseigné pour des raisons pédagogiques.
Bases du langage
Fonction et procédures
Fonctions
En Ada, les arguments d'une fonction sont accessibles uniquement en lecture: il n'est donc pas question de modifier leur valeur lors de l'exécution de la fonction. Les fonctions retournent toujours quelque chose. Voici un exemple:
déclarations
begin
instructions
return ...;
end Nom_fonction;
Procédures
Les procédures peuvent être définies comme des fonctions qui ne renvoient rien; elles peuvent néanmoins produire un résultat de sortie. Pour chaque paramètre de la procédure, on peut préciser la modalité de passage :
- in
: lecture du paramètre, comme pour l'argument d'une fonction.
- out
: écriture du paramètre uniquement (correspond à un résultat de sortie).
- in out
: lecture et écriture: la valeur du paramètre est prise en compte et peut être modifiée.
Voici la structure d'une procédure:
déclarations
begin
instructions
end Nom_procedure;
Syntaxe et opérateurs de comparaison
Chaque instruction est terminée par un point virgule ;
. L'affectation se fait en utilisant :=
. Une ligne de commentaires est préfixée par deux tirets --
. Les opérateurs de comparaison sont les suivants:
=
|
égalité |
/=
|
inégalité |
<
|
strictement inférieur à |
>
|
strictement supérieur à |
<=
|
inférieur à |
>=
|
supérieur à |
in
|
appartenance à un intervalle |
not in
|
non appartenance à un intervalle |
Déclarations
La partie déclarative doit contenir:
- Les types et les sous-types.
- Les variables sous la forme Nom_variable:Nom_type[:= Valeur_Initiale];
- Les constantes telles que
Pi:constant FLOAT:= 3.14159;
- Des procédures et des fonctions qu'on utilisera ensuite dans le corps.
Les types
Types discrets
Pour commencer, un peu de vocabulaire: les "types discrets" sont les types ordonnés où chaque élément a un successeur et un prédécesseur. Ils sont munis d'attributs, des fonctions exprimant les propriétés courantes du type. La syntaxe est T'nom_attribut
, où T est un type discret. Ces attributs sont les suivants:
- T'FIRST
Borne inférieure du type T.
- T'LAST
Borne supérieure du type T.
- T'POS(X)
X est de type T et T'POS(X)
renvoie la position de X dans le type T.
-T'VAL(N)
N est de type entier et T'VAL(N)
est l'élément de position N dans le type T.
- T'SUCC(X)
X est de type T et T'SUCC(X)
est le successeur de X dans T, ainsi T'SUCC(X)= T'VAL(T'POS(X)+1)
.
- T'PREC(X)
X est de type T et T'PREC(X)
est le prédécesseur de X dans T, ainsi T'SUCC(X)= T'VAL(T'POS(X)-1)
.
- T'IMAGE(X)
X est de type T et T'IMAGE(X)
est une chaîne de caractères qui contient l'image de X.
- T'WIDTH
Donne le nombre maximal de caractères pour une image d'une valeur de T.
- T'VALUE(S)
S est une chaîne de caractères qui est l'image d'un élément de type T; T'VALUE(S)
est l'élément en question.
Types prédéfinis
Le type discret entier INTEGER
. Il possède deux sous-types prédéfinis: NATURAL
pour les entiers naturels et POSITIVE
pour les entiers strictement positifs. Certains opérateurs sont prédéfinis, ce sont l'addition +
, la multiplication *
, la soustraction -
, la division /
, le reste de la division mod
, l'exponentiation **
.
Le type discret caractère CHARACTER
. Pour représenter un caractère, on utilise les apostrophes : 'a'.
Le type discret booléen BOOLEAN
, composé de TRUE
et de FALSE
. Les connecteurs logiques sont and
, and then
, or
, or else
et not
.
le type FLOAT
pour représenter les nombres réels.
le type STRING
pour les chaînes de caractères. Pour représenter une chaîne de caractères, on utilise les guillemets, par exemple: "La petite Léa est très jolie".
Type énumérés
On peut définir un type énuméré, qui est un type discret fini (comme le type booléen), par exemple le type Jour:
type Jour is (lunae_dies, martis_dies, mercurii_dies, jovis_dies, veneris_dies, saturnus_dies, solis_dies);
L'ordre d'énumération définit un ordre. On a ainsi dans notre exemple lunae_dies < martis_dies < ... < solis_dies.
Sous-types
Un sous-type est défini comme la restriction d'un type parent discret, sous la forme:
subtype nom_sous_type is nom_type range val1 .. val2;
où val1 et val2 sont deux éléments de type nom_type tels que val1 < val2. . Par exemple:
subtype Chiffre is INTEGER range 0..9;
subtype Alphabet is CHARACTER range 'a'..'z';
On peut appliquer à un sous-type les fonctions prévues pour son type parent, mais il faut faire attention à ce que le résultat reste dans l'intervalle.
Types privés
Les types en Ada peuvent être divisés en deux catégories. Il y a d'une part types énumératifs, types tableaux etc. sans oublier les types prédéfinis, c'est à dire les types dont la structure est connue des utilisateurs. Ils sont dits publiques. D'autre part les types privés permettent de cacher les détails de structure afin de respecter le principe d'abstraction des données en masquant la représentation interne des données, exportant les opérations de manipulation de ce type, empêchant un usage non contrôlé.
La syntaxe d'une déclaration d'un type privé est: type T_NOM_DU_TYPE is private;
.
Types limités
Si pour un type, on ne peut pas utiliser l'affectation et l'égalité prédéfinies, alors ce type est appelé un type limité. Attention, un type composé qui contient un composant limité doit obligatoirement être déclaré limité. Exemple:
numérateur : POSITIVE := 1;
dénominateur : POSITIVE := 1;
end record;
Types privés limités
Les types privés limités sont comme les types privés, mais où l'utilisation est encore plus limitée; logique, non? Ils ne possèdent plus ni la comparaison ni l'affectation. Avec un type privé limité on ne peut que déclarer des variables et utiliser les sous-programmes fournis par le paquetage (sans affectation ni comparaison). Ces techniques permettent un contrôle total sur leur utilisation. Un type privé limité se déclare de la façon suivante : type T_EXEMPLE is limited private ;
Ouf! C'en est fini pour les types ;-) Les types privés limités sont des types privés dont l'utilisation est encore plus restreinte; ils ne possèdent plus ni la comparaison ni l'affectation.
Branchements conditionnels
Si, alors, sinon
Comme dans la majorité des langages on trouve une structure conditionnelle de type "si, alors, sinon" , elle se construit comme suit:
if condition then ... else ... end if;
if condition then ... elsif condition2 ... elsif condition3 ... end if;
Choix multiples
Il existe également une structure permettant de faire des choix multiples.
case expression is
when choix1 => ...
when choix2 => ...
...
when others => ...
end case;
Les boucles
Attention, l'indice d'une boucle est de type discret et c'est une variable muette. Cela signifie que cette variable n'a pas à être déclarée et que sa valeur ne peut pas être modifiée par des instructions explicites.
Pour
les boucles "pour" se construisent comme suit:
for indice in [reverse] debut .. fin loop ... end loop;
Si on veut préciser que l'intervalle doit être parcouru en sens inverse, on utilise le mot clef reverse
.
Tant que
Les boucles "tant que" se construisent comme suit:
while condition loop ... end loop;
Les tableaux
Tableaux contraints
La taille des tableaux est précisée lors de la définition du type :
type Nom_Type_Tableau is array(Intervalle_fini_d'un_type_discret) of Type_Element;
Les bornes d'un tableau ne sont pas nécessairement statiques. Exemple:
tab1 : T;
type MATRICE is array(1..4, 1..4) of INTEGER;
ma_matrice : M;
On accède au i-ème élément d'un tableau tab en tapant tab(i)
. On peut manipuler les cases d'un tableau avec des agrégats, par exemple Tab1:Tableau(1..6):=(1|2=>3, others=>4);
ou Tab2:Tableau:=(1..10=>3);
.
L'opérateur &
permet de concaténer deux tableaux de même type. Ainsi, T1 & T2
donne un tableau de taille T1'length+T2'length, contenant les éléments de T1 suivis de ceux de T2. &
permet également de concaténer un élément à un tableau. Attention aux erreurs de dépassements, un tableau en Ada a une taille fixe une fois pour toutes lorsqu'il est déclaré.
Tableaux non contraints
Le type des indices et du type des éléments est précisé lors de la définition du type mais pas la taille du tableau. On peut faire sur les tableaux non contraints toutes les manipulations qu'on peut faire sur les tableaux contraints. Voici l'exemple d'un tableau de nombres naturels indexé par des entiers:
type T is array (INTEGER range <>) of NATURAL ;
Un objet de type T doit alors être contraint lors de sa déclaration, soit en précisant l'intervalle d'indexation, soit en lui affectant un autre tableau.
Chaînes de caractères
Le type STRING est un type pré-défini du paquetage STANDARD. Il est défini comme un tableau non contraint de caractères et indexé d'entiers strictement positifs. Les chaînes de caractères se manipulent donc exactement comme des tableaux, ainsi : S:STRING(1..18);
. Quand on ne connait pas la longueur d'une chaîne de caractères, on est obligé de fixer lors de la déclaration une taille maximum et de gérer ensuite explicitement la vraie longueur.
Attributs de tableaux
Les tableaux bénéficient d'attributs spécifiques. Soit T un objet de type tableau, on a:
- T'FIRST
indice du premier élément. T(T'FIRST)
nous donne la première valeur.
- T'FIRST(n)
indice du premier élément de la dimension n.
- T'LAST
indice du dernier élément de la première dimension.
- T'LAST(n)
indice du dernier élément de la dimension n.
- T'RANGE
intervalle d'indexation de la première dimension de T.
- T'RANGE(n)
intervalle d'indexation de la dimension n de T.
- T'LENGTH
longueur de la première dimension du tableau.
- T'LENGTH(n)
longueur de la dimension n du tableau.
Les structures
Une structure, ou enregistrement, est un objet composé d'une séquence de membres de types divers, portant des noms. On la construit comme suit:
champ1: type1;
champ2: type2;
champ3: type3;
end record;
On peut donner des valeurs initiales aux champs des structures, en mettant := valeur
après le nom du type.
On accède aux champs d'une structure avec un point .
: S: Nom_structure; ... Nom_structure.champ1 Nom_structure.champ2 Nom_structure.champ3
Pour affecter une valeur complète à une structure, on peut utiliser des agrégats:
jour: POSITIVE range 1..31;
mois: STRING(1..9);
annee: POSITIVE;
end record;
D:=(18, "février", 1983);
D:=(mois=>"février", jour=>18, annee=>1983);
Il est possible de passer des paramètres à une structure. Dans l'exemple suivant, le recours à une structure avec paramètres permet d'avoir des chaînes de caractères variables, et on mettra les dimensions que lors de la déclaration:
Igloo: STRING(1..N);
Banquise: STRING(1..B);
end record;
A: Adresse_Manchot(10,20);
Traitement des exceptions
Ada possède des exceptions prédéfinies, dont le nom est explicite: CONSTRAINT_ERROR
, STORAGE_ERROR
, PROGRAM_ERROR
, NUMERIC_ERROR
, TASKING_ERROR
. Il y a également des exceptions prédéfinies liées aux entrées-sorties que l'on trouve dans la bibliothèque ADA.IO_EXCEPTIONS
. Une exception, lorsqu'elle est déclenchée, interrompt l'exécution des blocs qu'elle traverse.
On peut lancer une exception grâce au mot clef raise
. On peut aussi capturer les exceptions avec when
. En Ada, le traitement des exceptions doit se faire dans les instructions bloc, les sous-programmes, les paquetages, les tâches ou les unités génériques. Ces traitements sont regroupés sous la rubrique exception
comme dans l'exemple suivant:
exception when CONSTRAINT_ERROR => put_line("une contrainte n'a pas été respectée"); raise CONSTRAINT_ERROR;
Il est possible de définir une exception, de la même manière que l'on définit un type ou déclare une variable. On la manipule ensuite comme une exception prédéfinie.
Entrées-sorties
Les fonctionnalités d'entrées-sorties sont disponibles dans le paquetage ADA.TEXT_IO
pour les chaînes de caractères et ADA.INTEGER_TEXT_IO
pour les entiers. Les procédures les plus utiles sont get(...)
, put(...)
, put_line(...)
et new_line
.
Les pointeurs
Les pointeurs sont sans doute la notion la moins explicite du langage. Pas de panique! Il se peut que vous mettiez quelques temps à les prendre en main.
Déclaration
Les pointeurs en Ada s'utilisent grâce au mot clef access
. Soit T_Quelconque un type quelconque, un type "pointeur sur un objet de type T_Quelconque" se déclare comme ceci: type T_Pointeur_T_Quelconque is access T_Quelconque ;
. Désormais, toute variable (ou instance) de ce nouveau type T_Pointeur_T_Quelconque sera considérée comme pouvant pointer sur un objet de type T_Quelconque. On notera que le "type pointeur" n'existe pas en soi, seul un type "pointeur sur un type donné" peut être défini. On déclare deux pointeurs de ce type de la façon suivante: Pointeur_1, Pointeur_2 : T_Pointeur_T_Quelconque ;
. Ils pourront ainsi désigner chacun un objet de type T_Quelconque. A ce moment là , ils ne pointent sur rien : leur valeur est automatiquement initialisée à NULL lors de la déclaration.
Création et accès à l'objet pointé
A tout moment l'utilisateur peut créer des objets dans le corps d'un sous-programme au moyen de l'allocateur new
portant sur le type de l'objet qui doit être pointé. La valeur de retour est l'adresse mémoire dynamique que le système lui a allouée:
Pointeur_2 := new T_Quelconque ;
Les zones mémoire pour chaque objet pointé sont réservées; cependant elles n'ont toujours pas de valeurs. Celles-ci vont pouvoir leur être données comme suit, avec .all
:
Pointeur_2.all := YYY ;
La permutation des deux valeurs s'obtient par permutation des valeurs des deux pointeurs (les valeurs ne changeront pas de place):
Pointeur_1 := Pointeur_2 ;
Pointeur_2 := Pointeur_3 ;
Opérations sur les pointeurs
Les seules opérations de comparaison sur les pointeurs sont l'égalité et la non-égalité. Le type pointeur est donc un type private. Cela est dû au fait que les valeurs d'un pointeur représentent des adresses mémoires (virtuelles), elles n'appartiennent pas à un ensemble ordonné. La représentation des adresses dépend de l'implémentation, et ce ne sont à priori pas forcément des entiers.
Ecrire un programme
Les paquetages
Le programme principal Ada utilise en général des paquetages, qui correspondent à des bibliothèques ou d'autres programmes. Pour cela, La clause with nom_paquetage
au début d'un programme permet d'accéder aux fonctionnalités du paquetage nom_paquetage. On peut ajouter une clause use nom_paquetage
qui permet d'invoquer les fonctionnalités de nom_paquetage directement, sans préfixer l'appel de nom_paquetage.
Un paquetage se partage en deux fichiers :
- nom_paquetage.ads qui contient la spécification du paquetage (fonctionnalités visibles): énumération des types, entête des fonctions et procédures proposées par le paquetage.
déclarations des types, fonctions, procédures...
end nom_paquetage;
- nom_paquetage.adb qui contient l'implémentation du paquetage. Cette partie peut également définir des procédures ou des fonctions cachées à l'utilisateur.
définitions des fonctions, procédures...
end nom_paquetage;
Programme source
Le code écrit par un programmeur est constitué d'unités de bibliothèques qui doivent être placées dans des fichiers sources. Ces fichiers ont un suffixe obligatoire qui est pour la partie spécification .ads
et pour l'implémentation .adb
.
Le point d'entrée d'un programme Ada est la procédure principale par laquelle doit commencer l'exécution. Cette procédure peut porter un nom quelconque mais ne doit pas avoir d'arguments.
Exemple de programme
Après toutes ces présentations nous sommes pleinement capables d'écrire un petit programme. Voici un exemple de petit programme Ada que l'on stockera dans le fichier "bonjour.adb":
procedure bonjour is
begin
Ada.Text_io.put_line ("bonjour le monde");
end bonjour;
Quels outils utiliser ?
Compilateurs
Il existe plusieurs compilateurs pour Ada. Dans ce qui va suivre, nous ne parlerons que de GNAT (GNU Ada Translator) qui fait partie de GCC (GNU Compiler Collection). C'est actuellement le meilleur compilateur, et il est entièrement libre. GNAT est installé par défaut dans la plupart des distributions GNU/Linux; si ce n'est pas le cas, il faut chercher le paquet qui s'appelle en général gcc-gnat.
Débuggueurs
Pour les programmes compilés avec GNAT, l'outil approprié est GDB (GNU Project Debugger). Eh oui, le projet GNU est partout :-). De plus, GDB possède une interface graphique, appelée GVD (GNU Visual Debugger), qui a été développée entièrement en Ada. Si GDB n'est pas installé, il faut chercher le paquet éponyme auprès de sa distribution.
Générateurs de documentation
Deuis quelques années on s'intéresse beaucoup aux outils qui permettent de générer facilement de la documentation à partir d'un code source, tels que JavaDoc pour le Java. Il existe des outils similaires pour Ada, mais ils sont peu connus. Pour n'en citer que deux, nous pourrions parler de AdaDoc et AdaBrowse. Puisqu'ils sont peu connus, il y a fort à parier que vous soyez obligés de les compiler.
Environnements de développements
La lecture d'un code en Ada étant très explicite, n'importe quel éditeur de texte fera l'affaire. On notera cependant qu'Emacs possède un mode Ada très abouti avec un menu et des raccourcis bien adaptés au langage, comme on peut le voir sur la capture d'écran ci-dessous:
Créations d'applications graphiques
C'est possible aussi grâce à GtkAda, une bibliothèque permettant de créer des applications graphiques portables en Gtk+. On peut également créer une application graphiquement. GtkAda est interfacé avec Glade, le logiciel de GNOME permettant de construire des interfaces graphiques.
Compilation
Compilation et édition de liens d'un programme
La façon la plus simple de compiler un programme est d'utiliser gnatmake. Ainsi, la commande gnatmake prog
compilera le fichier prog.adb et donnera un exécutable ./prog. En fait, gnatmake réalise trois opérations. Il remplace les instructions suivantes:
$ gnatbind # test de cohérence des différentes unités
$ gnatlink # édition des liens
L'étape de compilation produit un fichier objet .o et un fichier .ali par unité. Les fichiers .ali (pour Ada Library Information) contiennent les informations nécessaires ensuite à gnatbind et gnatlink.
Compilation de paquetages
La compilation de paquetage est simple, elle aussi. Prenons le cas d'un programme constitué d'un fichier contenant la procédure principale (principal.adb) et de deux autres fichiers contenant la spécification (paq.ads) et le corps (paq.adb) d'un paquetage. Il suffit simplement de taper gnatmake principal
et gnatmake s'occupera de compiler automatiquement tous les fichiers auxiliares. Il va en fait procéder de la sorte:
$ gcc -c paq.adb
$ gnatbind principal
$ gnatlink principal
Mêler Ada et d'autres langages
Ada est peu utilisé. Qu'à cela ne tienne! On peut mêler du code Ada avec un autre langage normalisé. Actuellement, on peut par exemple interfacer Ada avec les langages C, C++, Cobol et Fortran. Cela est possible par le biais du paquetage Interfaces
et de ses fils (Interfaces.C
, Interfaces.C.STRINGs
, Interfaces.C.Pointers
, Interfaces.Fortran
...). Ce paquetage Interfaces
définit les types élémentaires de la machine. Il est muni d'enfants qui définissent la vue Ada des types des autres langages. Par exemple, le paquetage Interfaces.C définit un type int, un type C_Float, etc. dont on garantit qu'ils correspondent aux types de C.
Conclusion
En savoir plus
- Association Ada-France: http://www.ada-france.org
- Les groupes français et anglais: fr.comp.lang.ada et comp.lang.ada.
- Le manuel de référence sur GNAT (en anglais): http://gcc.gnu.org/onlinedocs/gcc-3.3.5/gnat_rm/.
- Le guide de l'utilisateur de GNAT pour UNIX (en anglais): http://gcc.gnu.org/onlinedocs/gcc-3.3.5/gnat_ug_unx/.
- Le manuel de GCC: man gcc
vous dira tous les secrets de la compilation.
- Le forum Développement de Léa-Linux.
C'est parti !
Ada est un langage facile, il peut s'apprendre en quelques heures! Cependant, rien n'est mieux que la pratique. Osez faire de petits exercices pour vous entraîner! Bon courage :-)
Copyright
Copyright © 12/02/2005, Jiel Beaumadier
Vous avez l'autorisation de copier, distribuer et/ou modifier ce document suivant les termes de la GNU Free Documentation License, Version 1.2 ou n'importe quelle version ultérieure publiée par la Free Software Foundation; sans section invariante, sans page de garde, sans entête et sans page finale. |