Apprendre à programmer avec Ada étape par étape

Apprendre à programmer avec Ada étape par étape
Le langage ADA est un langage à part parmi tous les dialectes informatiques. Défini comme l réponse à un appel d'offre du ministère américain à la défense (DoD), norme ISO, il présente une richesse fonctionnelle étonnante (ceci dès sa première version en 1983) tout en mettant en oeuvre des mécanismes stricts garantissant la conformité des programmes à l'exécution.
Aujourd'hui ce langage peu mis en avant est utilisé dans l'industrie aéronautique et de défense dans le monde entier.
Le nom du langage fut choisi en hommage à Lady Ada Byron, comtesse de Lovelace, assistante de Babbage et considérée comme le premier programmeur de l'histoire.
- Structure d'un programme
La base syntaxique d'ADA est le langage Pascal. Rien d'original en la matière donc. Leend;
langage distingue procédures et fonctions comme il se doit et toute procédure peut être choisie pour sous-programme principal (en C il s'agit de main).
-- mon premier programme ada
-- hello world !
with text_io; use text_io;
procedure exemple1 is
begin
put("hello world!");
end;
Par défaut et contrairement aux langages fondés sur C, les arguments de la ligne de begin commande ne sont pas associés au sous-programme principal. Il convient pour en disposer de faire appel à une bibliothèque spécifique. Ce fonctionnement est compréhensible pour un langage dont la vocation est le plus souvent de réaliser des systèmes embarqués.
I.1. Sous-programmes
Concernant les sous-programme en général, ADA propose un mécanisme de passage des paramètres indépendant du compilateur. Traditionnellement c'est au contraire a ubegin développeur de choisir entre passage par copie ou par référence. foo(3,4);
Le langage ADA offre une approche différente en proposant de choisir entre un mode ene nd exemple3;
lecture seul, un mode en écriture seule, ou un mode mixte. La réalité du mode de passage choisi incombant au seul compilateur, probablement mieux à même de déterminer pour une architecture donnée la solution la plus efficace.
-- définition d'une procédure et d'une fonction
--
with Ada.text_io; use Ada.text_io; -- ignorer pour l'instant
procedure Exemple2 ; is
Package int_io is new Ada.Text_Io.integer_io(Integer); use
Int_Io; -- idem
value : integer;
procedure Bar(a: in integer) is
a i: integer := 1;
begin
i := 4;
end Bar;
function Foo(B: in Integer) return integer is
I: integer := 3;
begin
return i;
end Foo;
bar(Value);
Value := foo(4);
put("hello world!"); new_line; put(value); new_line;
Il faut noter que les fonctions n'acceptent que des paramètres en lecture seule. Ceci afin de limiter les effets de bord. Enfin, en ADA, toutes les procédures et fonctions sont surchargeables, sous réserve d'avoir des signatures (prototypes) différentes, valeurs par défaut exclues.
with ada.text_io;
procedure exemple3 is
package int_io is new ada.text_io.integer_io(integer); procedure foo(i : integer) is
int_io.put(i);
end foo;
procedure foo(i : integer; j: integer) is begin
int_io.put(i); ada.text_io.put(","); int_io.put(j); ada.text_io.new_line;
end;
Le langage ADA prend parfois à contrepied certaines bonnes vieilles habitudes, en matière de syntaxe comme de contraintes.
Utilisation d'attributs
Il s'agit d'un des éléments probablement parmi les plus perturbants à première vue. ADA définit un ensemble d'attributs, qui appliqués à des données ou des types, vont permettre d'obtenir des informations souvent élémentaires ou intimement liées à la donnée.
L'appel à ces attributs, on parle de qualification, se fait au moyen du caractère « ' » (apostrophe ou quote).
Ainsi sur un type de données on pourra obtenir la borne inférieure, la précision maximale.begin Pour les objets il sera possible d'obtenir la classe ou le type. Sur un tableau les différents indexes.
En réalité ses attributs sont tous simples, il suffit de les imaginer comme de simples méthodes appelées non pas avec la notation pointée habituelle mais avec la quote.
-- un exemple de type enuméré, avec utilisation de la qualification with Ada.text_io; use Ada.text_io;
procedure exemple4 is
type couleur is ( blanc, bleu, jaune, rouge, vert, orange); package couleur_io is new ada.Text_io.enumeration_io(couleur); use couleur_io;
c : couleur := bleu;
begin
put(c); newline;
put(Couleur'_pred(C)); New_line; end exemple4;
Les tableaux : intervalles et agrégats
Contrairement au langage C, un tableau peut être indexé par tout intervalle d'un type discret cela inclut donc les types énumérés, les entiers, et les types dérivés.
Les bornes de l'intervalle sont librement fixables.
Enfin il est possible d'utiliser la notion d'agrégat (un équivalent à { ... } en C) pour initialiser un tableau en une seule fois. En ADA l'agrégat est donné entre parenthèses, on peut initialiser les valeurs par position, par nom (notamment pour les index de types énumérés), enfin lap clause others permet de donner une valeur par défaut à tous les éléments non encore initialisés du tableau.
with ada.text_io;
procedure exemple5 is
type couleur is ( bleu, rouge, vert, jaune , blanc ); package int_io is new ada.text_io.integer_io(integer);
t1 : array (rouge..blanc) of integer; type mytab is array (couleur'range) of integer;
t2 : mytab;
procedure put(a : mytab) is
begin
ada.text_io.put ("(");
for c in a'range loop
int_io.put(a(c));
if c /= a'last then
ada.text_io.put(", ");
end if;
end loop;
ada.text_io.put(")");
end put;
t2 := ( bleu | vert => 5, others => 3 ); put(t2);
Exceptions
ADA complète la syntaxe de base du langage par le support des exceptions. Un support qui pourra paraître simpliste au regard des mécanismes offerts par exemple dans Java. La réalité est qu'un tel niveau est probablement très suffisant.
Le langage propose un type de données : exception. Chacun peut naturellement déclarer de nouvelles exceptions (en plus de celles définies par défaut). L'exception en ADA s'assimile à un signal nommé, elle ne porte pas d'information additionnelle.
Lorsque qu'une exception est déclenchée (on dit levée) le signal se propage en remontant la pile des blocs d'instructions puis des sous-programmes, ceci tant qu'aucun bloc de traitement n'est rencontré.
Au pire, si même le sous-programme principal ne comporte aucun
bloc pour traiter l'exception, l'exécution du programme est stoppée. Le bloc de traitement des exceptions peut être vu comme le switch du langage C. Chaque ,exception peut recevoir un traitement dédié, ou un traitement global peut être exécuté pour un ensemble d'exceptions.
L'instruction raise permet de lever une exception explicitement. Les exceptions définies dans le langage sont elles le plus souvent levées par le runtime.
A noter que les exceptions sont présentes dans le langage depuis 1983.
with text_io; use text_io;
rocedure Exemple6 is
Monexception : exception;
procedure Foo(I : in Integer := 0) is
begin
if I < 0 then
raise Constraint_Error;
elsif I > 10 then
raise Monexception;
end if;
end Foo;
begin
begin
Foo(-4);
exception
when Constraint_Error =>
put("Ok, il n'y a pas mort d'homme."); new_line;
raise Monexception;
end;
exception
when others =>
Put("Rah! Mieux vaut en finir"); new_line;
end Exemple6;
III. Modèle de composants
Au delà des améliorations apportées au langage Pascal, ADA introduit une approche modulaire du développement en permettant la compilation séparée de portions du code. Ces différents unités de compilation vont permettre de mettre en oeuvre des approches dites top¬down et bottom-up.
Parmi les unités de compilation, ADA introduit la notion de Packages. Ces packages vont permettre la définition de bibliothèques (approche bottom-up).
III.1. Packages
Les packages ADA sont donc des librairies réutilisables. Chacun dispose d'une spécification et d'une ou plusieurs implantations.
Cette séparation va permettre de tirer profit des différents modes de développement (top-down ou bottom-up). En effet, spécification comme implantation sont des unités de compilation et sont compilables séparément. On pourra donc compiler un programme principal avant ses composants, tester des composants en priorité ou encore développer des implantations de test.
L'instruction package est utilisée seule pour définir les spécifications d'une librairie. Pour l'implantation on utilisera le couple : package body.
package ComplexPkg is
type Complex is record
X,Y : Float := 0.0;
end record;
procedure Set ( Z: out complex; A, B : in Float := 0.0);
function Getr (Z : in Complex) return float;
function Geti (Z : in Complex) return Float;
function i return complex;
procedure Put(Z: in Complex);
end ComplexPkg;
with text_io; use text_io;
package body ComplexPkg is
package real_io is new float_io(float); use real_io;
procedure Set ( Z: out Complex; A, B : in Float := 0.0) is
begin
Z.X := A; Z.Y := B;
end Set;
function Getr (Z : in Complex) return Float is
begin
return Z.X;
end Getr;
function Geti (Z : in Complex) return Float is
begin
return Z.Y;
end Geti;
function I return Complex is
tmp : complex ;
begin
Set ( tmp, 0.0, 1.0);
return Tmp;
end I;
procedure Put(Z: in Complex) is
begin
Put("["); Put(Z.X); Put(", "); Put(Z.Y); Put("]");
end Put;
begin -- initialisation optionnelle
null;
end ComplexPkg;
A ce stade on dispose d'un modèle de composant satisfaisant mais n'offrant aucun niveau de protection des données. Ainsi un type de données défini dans la spécification d'un composant verra son implantation librement accessible, avec le risque évident de dysfonctionnement à la moindre mise à jour.
Pour contrer ces mauvaises pratiques potentielles, ADA complète son modèle composant en affinant la notion de spécification, désormais partitionnée en deux blocs : une partie publique et une partie privée.
On pourra donc se contenter d'indiquer l'existence d'un type de donnée dans la partie publique, quand son implantation sera réservée à la partie privée. Pour un tel type de données aucun accès direct à l'implantation n'est possible. Seules les primitives proposées par le package permettront d'obtenir ou de fixer les valeurs de variables du type concerné.
package ComplexPkg is
type Complex is private;
procedure Set ( Z: out complex; A, B : in Float := 0.0);
function Getr (Z : in Complex) return float;
function Geti (Z : in Complex) return Float;
function i return complex;
procedure Put(Z: in Complex);
private
type Complex is record
X,Y : Float := 0.0;
end record;
end ComplexPkg;
Lorsqu'une unité de compilation doit faire appel à un package, l'instruction with est utilisée.
Elle permet à la fois de disposer d'une visibilité sur les types et sous-programme définis, mais instruit aussi le compilateur pour les opérations de link futures. On peut imaginer que l'instruction with équivaut au couple #include, -lXXX du langage C.
with ComplexPkg;
procedure exemple7 is
z1, z2: ComplexPkg.complex;
begin
ComplexPkg.set(z1, 6.0, 8.0);
ComplexPkg.set(a =>9.0, b =>3.0, z => z2);
ComplexPkg.put(z2);
end exemple7;
On couple le plus souvent l'instruction with à la clause use. Ceci va importer l'ensemble des symboles définis dans le package directement le contexte courant. Il devient alors inutile de préfixer les noms de types, variables, sous-programmes par le nom du package. En cas de conflit, notamment entre deux packages, il suffira de préfixer au cas par cas.
with complexPkg; use complexPkg;
procedure exemple8 is
z1, z2: complex;
begin
set(z1, 6.0, 8.0);
set(a =>9.0, b =>3.0, z => z2);
put(z2);
end exemple8;
III.2. Librairies hiérarchiques
Le package en tant que tel est un modèle de composant satisfaisant. Cependant, il est apparu avec l'expérience que les différents niveaux de visibilité offerts étaient trop rigides, avec pour conséquences recompilations et modifications de code à répétition.
Pour résoudre ces problèmes, la version 1995 du langage introduit le concept de librairie fille sur les packages. Il devient alors possible de créer une arborescence de librairies complétant chacune un noeud de l'arbre.
package Complexpkg.Numerics is
function "+" ( Z1,Z2 : in Complex) return Complex;
Function "*" ( Z1,Z2 : in Complex) return Complex;
end Complexpkg.Numerics;
En outre il est possible de définir des extensions privées d'une librairie, soit pour traiter des éléments internes, soit encore pour permettre des extensions vendeur (en fonction du matériel par exemple).
with Ada.Numerics;
private package Complexpkg.Internals is
type PolComplex is private;
function Cart2pol(Z: in Complex) return Polcomplex;
private
subtype Positivefloat is Float range 0.0 .. Float'Last;
subtype Angle is Float range -Ada.Numerics.PI .. Ada.Numerics.PI;
type Polcomplex is record
R : Positivefloat := 0.0;
A : Angle := 0.0;
end record;
end Complexpkg.Internals;
Avec ces librairies hiérarchiques il devient possible de faire évoluer des composants sans remettre en cause l'existant et notamment sans devoir procéder à de nouvelles campagnes de tests sur les éléments existants.
- Généricité
Pour compléter son modèle composant ADA dispose de mécanismes permettant la réalisation de composants génériques.
On peut voir un composant générique comme un élément paramétré.
Lors de l'utilisation effective on précisera les paramètres et une version spécifique du composant sera créée et utilisable. De manière imagée on peut se représenter le composant générique comme un moule qui va permettre de créer autant de composants que nécessaires.
En règle général un composant générique permettra d'implanter un algorithme et sera paramétré par un type de données auquel appliquer cet algorithme (exemple la gestion de piles ou de files d'attentes).
generic
type Item is private;
with function "+" (A, B : in Item) return Item;
with function "*" (A, B : in Item) return Item;
Zero : Item;
with procedure put (i : in item);
package Matrixpkg is
type Matrix is array (Integer range <>, Integer range <>) of Item;
function "+"(A, B : in Matrix ) return Matrix;
function "*"(A, B : in Matrix ) return Matrix;
procedure put(m : in matrix);
end Matrixpkg;
Modèle de package
Paramètres :
types,
valeurs,
fonctions
Nouveau package
Il convient à chaque fois de bien étudier les paramètres requis par un module générique. Si naturellement un type de donnée est en général requis, le plus souvent il est nécessaire de l'accompagner de diverses opérations et des éléments neutres associés.
Le corps d'un module générique ne diffère en rien du corps d'un module classique. Le type de donnée générique est manipulé comme un type standard, à ceci près que seuls l'affectation, le test d'égalité et les opérations passés en paramètre sont disponibles. Ce qui est logique puisque rien ne permet de supposer l'existence d'autres opérations sur ce type.
A l'usage, un module générique doit être instancié. Cela revient à créer un module classique à partir de son modèle générique et de paramètres.
with Matrixpkg; -- impossible d'utiliser use sur un module générique
with text_io; use text_io;
procedure Exemple9 is
package Int_Io is new Integer_Io(Integer); use Int_Io;
procedure Put2(I :in Integer) is
begin -- petite astuce car notre module attend une fonction
Put(I); -- à un seul paramètre, or la fonction put livrée par
end Put2; -- défaut comporte plusieurs paramètres, par défaut,
-- mais plusieurs quand même.
package Int_Matrix is new Matrixpkg(Integer, "+", "*", 0, Put2);
use Int_Matrix; -- use est utilisable sur la version instanciée
A : Matrix( 1..3, 1..2);
B : Matrix ( 0..1, 2..4);
begin
A := ( others => ( others => 1 ) );
B := ( others => ( others => 3 ) );
A := A + B;
put(a);
end Exemple9;
- Modèle objet
Malgré la richesse de la notion de package, il manquait au langage un modèle objet et plus précisément le concept d'héritage. Plutôt que d'introduire la notion de class brutalement, comme le fait C++, ADA se contente de tirer profit des éléments déjà présents et va préférer aux classes l'extension de la notion de types. C'est pourquoi on parlera de programmation par extension.
V.1. Extension de types
Cette possibilité d'extension va s'appliquer au type record (l'équivalent des structures en C). En l'absence d'enveloppe pour l'objet (la classe) on va appeler méthodes (ou procédures et fonctions primitives) les procédures ou fonctions prenant le type étendu en paramètre ou pour les fonctions retournant ce type. C'est donc sur cet ensemble type plus primitives que va porter l'héritage.
Pour préciser qu'un type record va pouvoir faire l'objet d'extensions on va utiliser le mot clef tagged. Le mot tagged est réellement adapté car en fait le type record se retrouve comme affublé d'un attribut supplémentaire, un tag, permettant de déterminer son type.
Pour étendre effectivement un type, on utilisera le mot clef with en précisant un record additionnel. On peut naturellement étendre un objet sans ajouter aucun attribut en indiquant que le record ajouté est vide.
procedure exemple10 is
type Point is tagged record
X, Y : Float;
end record;
type Circle is new Point with record
R : Float;
end record;
type Sphere is new Circle with null record;
type Box is new Point with record
W, H : Float;
end record;
P : Point ;
c : circle := ( p with 8.0); -- un cercle est un point + un rayon
begin
P := Point(C); -- on peut aussi caster en sens inverse
end exemple10;
V.2. Polymorphisme, Class wide programming
Avec les types étendus on définit une véritable arborescence de types. ADA permet d'accéder en globalité à l'ensemble de ces types. On parle de class wide programming.
Pour désigner le type représentant l'ensemble des types dérivés d'un type racine, on utilise la qualification avec l'attribut class. Une classe en ADA n'est donc pas un type d'objet, mais plutôt la réunion d'un ensemble de types dérivés.
Il est ainsi possible de définir des fonctions opérant sur toute une classe, classe dont certains types peuvent être encore non définis au moment de la conception de la fonction, et ajoutés ultérieurement dans des librairies filles.
Avec ce mécanisme il devient possible de sélectionner les méthodes appliquées au plus tard, au moment de l'exécution, ceci en fonction de la nature effective du paramètre (on parle de late binding).
with Text_Io; use Text_Io;
with ada.tags; use ada.tags;
with ada.Integer_Text_Io; use ada.Integer_Text_Io;
procedure exemple11 is
type Point is tagged record
X,Y : Float;
end record;
type Circle is new Point with record
A
B
D
Is new Is new
Is new
C
A'Class B'Class
R : Float;
end record;
type Rectangle is new Point with record
Width, Height : Float;
end record;
type object is access point'class;
procedure Dispatch(O : in point'class) is
begin
if (O in Circle'Class) then
null; -- on devrait faire qquechose
elsif (O in Rectangle'Class) then
null;
else
Put_Line("Hum, this " & external_tag(o'tag) & "looks weird to
me.");
end if;
end Dispatch;
I : Integer;
O : Object;
begin
Get(I);
case I is
when 1 =>
O := new Point;
when 2 =>
O := new Circle;
when others =>
O := new Rectangle;
end case;
dispatch(O.all);
end exemple11;
V.3. types abstraits
Enfin, ADA comme la plupart des langages, offre la possibilité de définir des types et sous programmes abstraits, ceux-ci permettent de définir un cadre à respecter et implanter par les types dérivés. Naturellement ces types et sous-programmes ne sont pas utilisables en tant que tels.
procedure exemple12 is
type Object is abstract tagged record
X, Y : Float;
end record;
-- Object n'est pas utilisable en tant que tel
-- mais les types dérivés oui.
Type Point is new object with null record;
type Circle is new Point with record
R : Float;
end record;
P : Point ;
C : Circle := ( P with 8.0);
begin
P := Point(C);
end Exemple12;
V.4. Types dérivés et package
On a vu qu'un package peut cacher l'implantation des types définis dans sa spécification. La définition détaillée restant alors protégée dans la zone privée. Dans le cas de la programmation par extension, ce fonctionnement permet de définir avec beaucoup de finesse le degré de visibilité des types définis.
procedure Exemple13 is
package Test1 is
-- on nous cache meme le fait que t est tagged
-- il nous sera impossible de créer des types
-- dérivés
type T is private;
private
type T is tagged record X,Y : Float; end record;
end Test1;
package body Test1 is
end Test1;
--
package Test2 is
-- ici, meme si l'implantation exacte de t est
-- inconnue, on pourra créer un type dérivé
type T is tagged private;
private
type T is tagged record X,Y : Float; end record;
end Test2;
package body Test2 is
end Test2;
-- on etend donc un type opaque (test2.t) avec de
-- nouveau éléments.
type T2 is new Test2.T with record Z : Float; end record;
-- ce qui est aussi possible dans un package tiers :
package Test3 is
type T is new Test2.T with private;
private
type T is new Test2.T with record Z: Float; end record;
end Test3;
package body Test3 is
end Test3;
-- à cela il faudrait ajouter les possibilités propres
-- aux librairies filles.
begin
null;
end Exemple13;
- Environnement multitâche intégré
La prise en compte au sein même du langage du multitâche est une autre originalité d'ADA.
Cet élément clef a d'ailleurs longtemps été mal supporté dans les outils Open Source, notamment parce que les systèmes sous-jacents (Les Unix et GNU/Linux en particulier) ne supportaient pas le multi-threading en natif.
VI.1. Les objets tâches
Dans un programme classique il n'existe qu'une tâche, le sous-programme principal. Celui-ci se déroule en exécutant en séquence ses instructions (et celles des sous-programmes appelés).
En ADA il est élémentaire de créer des tâches additionnelles. Le runtime (seul ou avec l'OS) va alors donner l'illusion que toutes ces tâches s'exécutent simultanément en exécutant successivement des petits morceaux de chaque tâche (C'est le principe du multitâche, tout l'objet du jeu étant alors de correctement réaliser le partage de la ressource CPU).
with text_io; use text_io;
procedure exemple14 is
task T1;
task body T1 is
begin
Put("hello from t1");
end T1;
begin
Put("hello from main");
end exemple14;
Excepté dans le cas particulier d'une tâche créée dynamiquement, toutes les tâches voit leur exécution commencer au begin du bloc où elles ont été déclarées. Cette exécution comporte :
une phase d'élaboration des déclarations (variables, sous-programmes) et l'exécution des instructions proprement dites.
A noter qu'une tâche peut avoir terminé l'exécution de ses instructions (on dit qu'elle est achevée) sans pour autant être terminée. En effet, lorsque plusieurs tâches sont définies au sein d'un même bloc, la terminaison effective de l'ensemble, tâches + bloc, ne survient que lorsque tous sont achevés. Il faut donc prêter attention aux tâches qui pourraient boucler et ne jamais autoriser la terminaison du bloc qui les contient, par exemple une procédure ou une fonction.
La définition d'une tâche en ADA peut être (très grossièrement) comparée à celle déjà détaillée du package. Chaque tâche va en effet disposer d'une spécification et d'une implantation (le
corps).
with text_io; use text_io;
procedure exemple15 is
task T1;
task t2;
task type Simple;
type Simpleptr is access Simple;
T3 : Simple;
T4 : Simpleptr;
task body T1 is
begin
Put_Line("Hello from t1");
end T1;
task body T2 is
begin
Put_Line("Hello from t2");
end T2;
task body simple is
begin
Put_line("hello from simple");
end simple;
begin
Put_line("hello from main");
T4 := new Simple;
Put_line("Created new simple");
end exemple15;
Dans le cas d'une tâche si le corps représente naturellement les instructions qui seront exécutées, les spécifications de leur coté vont préciser l'API de la tâche (on parle de points d'entrée). Cette API décrit les différents appels reconnus par la tâche, appels (qu'on peut comparer aux appels système d'un noyau GNU/Linux par exemple) qui serviront à communiquer avec elle.
En effet en l'absence de points d'entrée une tâche s'exécute dans son environnement indépendamment du reste. Cette situation est rarement souhaitée.