« Méthodes de génie logiciel avec Ada/Première partie » : différence entre les versions

Contenu supprimé Contenu ajouté
JerePff (discussion | contributions)
Ligne 177 :
 
=Rôle et principes d'un langage de programmation=
Après avoir étudié le rôle des méthodes, nous allons nous intéresser dans ce chapitre à leur prolongement, le langage, et à son influence sur la conception et sur la maintenabilité des applications informatiques. Dans cette réflexion sur le rôle des langages de programmation, nous utiliserons et nous contrasterons principalement Ada d'une part, et le couple C/C++ d'autre part. Ce choix n'est pas seulement dû au fait que ces langages sont les plus utilisés actuellement: nous verrons qu'ils correspondent à deux approches, deux philosophies pourrait-on dire, de la programmation fondamentalement différentes, qui les ont conduit à faire des choix radicalement opposés. Pourquoi ne pas faire la différence entre C et C++? C++ est un perfectionnement technologique du langage C; on l'appelle souvent the C after<ref>Le C d'après.</ref>. Mais malgré l'introduction d'outils de plus haut niveau (notamment l'héritage), les principes, les rapports du programmeur avec son langage, sont restés les mêmes.
<references />
==Le double niveau de lecture==
On considère généralement que le rôle d'un langage est de porter une information, un message, d'une personne qui parle à une personne qui écoute. Dans le cas des langages de programmation, celui qui «parle» est bien entendu le programmeur. Mais qui est celui qui «écoute»? L'ordinateur, pense-t-on généralement. Certes, mais aussi celui qui relira le logiciel plus tard, c'est-à-dire le programmeur de maintenance<ref>Dans le cadre du génie logiciel, on doit toujours considérer que celui qui relit un logiciel n'est pas celui qui l'a écrit.</ref>.
 
Hofstadter [Hof85] soutient que le message ne porte pas l'information, mais ne fait que déclencher l'information contenue dans l'interlocuteur en «appuyant» sur des «boutons» préexistants. Considérons par exemple la phrase suivante (tirée d'un livre de lecture, classe de CM1):
:''Dans la boulangerie, Madame Durand dit à Florence: «Tu n'as pas vu Minet?»''
En lisant cette phrase, vous avez parfaitement compris que
:''La boulangère s'inquiète auprès de la petite fille d'avoir perdu son chat.''
Or aucun des éléments de la seconde formulation ne figure dans la première! Comment savez-vous que Madame Durand est la boulangère? Parce que vous savez que si ce n'était pas la boulangère, la formulation «dans la boulangerie» n'aurait aucune utilité. Qui vous a dit que Florence était une petite fille? Parce que vous savez que la formulation «Madame X» d'un côté, un prénom de l'autre traduit une relation d'adulte à enfant. Qui vous a dit que le chat était perdu? Parce que vous savez que la construction «Tu n'as pas vu...» traduit une inquiétude. Et bien entendu, vous savez que «Minet» est un nom de chat...
Cet exemple montre bien que la compréhension par l'homme d'un texte fait usage de toute une culture extérieure au seul message. Or la lecture d'un texte de programme par un ordinateur ne mettra en jeu que le texte lui-même et les règles du langage. C'est de cette différence de lecture que provient un grand nombre d'erreurs, la plus célèbre étant celle survenue à la NASA dans les années 60. Une sonde destinée à observer Vénus est passée à dix fois la distance prévue, suite à une erreur de calcul dans la trajectoire. Une commission d'enquête a révélé qu'un programmeur avait écrit (en FORTRAN):
<source lang="fortran">
DO 10 I = 1.5
</source>
Dans l'esprit de celui qui avait écrit cette instruction, ainsi que dans l'esprit de tous ceux qui l'avaient relue par la suite, il s'agissait d'une boucle DO. Cette conviction était suffisamment forte pour que l'on ne remarque pas la présence d'un point à la place de la virgule normalement requise dans une boucle DO... Le compilateur qui ne comprend en revanche que ce qui est écrit a relu cette instruction sous la forme (sachant que les espaces ne sont pas significatifs en FORTRAN):
<source lang="fortran">
DO10I = 1.5
</source>
c'est-à-dire comme l'affectation de la valeur réelle 1.5 à la variable de nom DO10I, non déclarée (mais ce n'est pas un problème en FORTRAN où la déclaration de variable n'est pas obligatoire)...
 
Plus prosaïquement, si vous lisez dans un programme:
<source lang="ada">
for I in 1..MILLE loop
...
end loop;
</source>
vous serez persuadé que la boucle sera exécutée 1000 fois, car vous savez que la chaîne de caractères 'M', 'I', 'L', 'L', 'E' a la même signification que le nombre 1000. Le compilateur, lui, ira regarder dans sa table des symboles, où il trouvera que l'identificateur MILLE résulte de la déclaration:
<source lang="ada">
MILLE : constant := 10_000;
</source>
il exécutera donc la boucle dix mille fois...<ref>Cet exemple n'est pas irréaliste; nous l'avons effectivement trouvé dans un projet réel...</ref>
 
La compréhension par le programmeur de son programme s'appuie donc sur des éléments non écrits faisant partie de sa culture propre. Pourquoi est-ce nécessaire? Après tout, le compilateur est bien capable de s'y retrouver sans ces éléments «culturels». Mais l'ordinateur dispose d'un atout complémentaire: une mémoire illimitée<ref>Tout au moins comparée à la mémoire (à court terme) humaine.</ref>; toute information qu'on lui a communiquée une fois reste disponible. Au contraire, le programmeur ne dispose que d'une mémoire à court terme limitée (dont, comme nous l'avons vu, on évalue la capacité à sept «cases»). L'homme va compenser ce manque de mémoire en déduisant les éléments dont il a besoin, recréant ainsi l'information pour éviter de la stocker. Plus prosaïquement, ceci justifie la nécessité (toujours répétée, jamais justifiée, et pas toujours observée) d'avoir des identificateurs parlants: si l'on veut stocker le nombre de lignes imprimées, cela ne fait aucune différence pour l'ordinateur d'appeler la variable XYZ123, NBLN ou NOMBRE_DE_LIGNES. En tant qu'être humain, je dois entièrement mémoriser le rôle de la variable XYZ123. Dans le deuxième cas, je dois mémoriser que NB est une abréviation pour «nombre» et LN pour «lignes». Ce stockage est cependant plus économique, car de nombreuses variables peuvent être construites en combinant ainsi un petit nombre d'abréviations. Avec la troisième forme, je n'ai rien à mémoriser: je peux déduire entièrement la signification de la variable de son nom.
 
Cependant, cette information portée par le nom de la variable échappe totalement au compilateur: rien n'empêche la variable NOMBRE_DE_LIGNES de désigner la distance de la Terre à la Lune! Tout l'art du programmeur va donc être d'écrire son programme de façon qu'il porte deux messages à la fois: un pour le compilateur, gouverné par les seules règles du langage, et un pour le lecteur, gouverné par son fonds culturel, et de faire en sorte que les deux correspondent! La difficulté réside bien entendu en ce que le «fonds culturel» varie d'un programmeur à l'autre. Un exemple typique se trouve dans l'utilisation d'abréviations: par exemple, LN est une abréviation «évidente» de «ligne» pour certains, alors que pour d'autres elle signifiera «longueur» ou «logarithme népérien». Il convient donc d'éviter de façon générale l'usage de tout «fonds culturel» non explicite, et plus particulièrement des abréviations<ref>Ceci n'est pas limité au logiciel... Le français qui arrive aux Etats-Unis reste en général perplexe devant les panneaux annonçant «PED. XING». Il ignore que la lettre X, qui représente une croix, est souvent utilisée comme abréviation pour le mot «cross» (croix). La signification du panneau est donc «Pedestrian Crossing» (passage piéton).</ref>.
 
Un exemple caractéristique de la non-compréhension de ce double rôle du langage de programmation peut être trouvé dans les langages C et C++: dans ceux-ci, deux identificateurs qui ne diffèrent que par l'usage des minuscules ou des majuscules sont différents. Ainsi, Nombre_de_Lignes ne désigne pas la même variable que NOMBRE_DE_LIGNES. Du point de vue du compilateur, il s'agit de deux chaînes de caractères différentes, donc de deux identificateurs différents. En revanche, la signification «culturelle» pour l'être humain ne dépend pas de la façon d'écrire les mots, donc la compréhension de la signification est la même. On oblige le programmeur C à raisonner avec la vue de l'ordinateur, donc à mémoriser un nombre d'informations plus grand pour comprendre la signification d'un programme.
<references />
==Niveau sémantique des langages==
On entend souvent l'expression «langage de haut niveau». Nous avons dit précédemment que les méthodes de conception s'arrêtaient lorsque l'on avait atteint le «niveau» du langage de programmation. Mais comment définit-on le niveau d'un langage? La meilleure définition se rapporte à la position du langage dans le processus de développement: plus la formulation sera proche de la définition du problème, plus nous dirons que le langage est de haut niveau. Inversement, si le langage exprime les contraintes de la réalisation sur machine, nous parlerons d'un langage de bas niveau.
 
Nous allons illustrer cette notion par un exemple simple, et voir comment différents langages permettent de prendre le relais des méthodes plus ou moins tôt (cet exemple est critiquable du point de vue des algorithmes utilisés; son but n'est que de montrer la démarche d'abaissement du niveau sémantique dans un cas simple).
 
Supposons que nous voulions trouver tous les diviseurs d'un nombre donné N. La spécification du problème (point de départ) peut s'exprimer comme:
:''Calculer la liste de tous les nombres P tels que P divise N''
La première étape consiste à exprimer la description de la solution sans faire référence à un quelconque algorithme informatique. Un langage comme SETL nous permet de nous situer directement à ce niveau. On écrirait:
<source lang="ada">
LISTE := [P in [1..N] | N mod P = 0];
</source>
qui se lit «Mettre dans la variable LISTE la suite des nombres P de l'intervalle de 1 à N tels que N modulo P soit égal à 0». Noter qu'à ce niveau, on ne spécifie pas d'algorithme: on exprime le résultat souhaité, et c'est le travail du compilateur de déterminer comment obtenir ce résultat<ref>Ce principe de description du résultat à obtenir et non du moyen d'y parvenir est également à la base du langage SQL dans le domaine des bases de données.</ref>. Nous ne nous préoccupons pas non plus de problèmes annexes, comme de savoir quelle quantité d'espace mémoire réserver: là encore, le système n'a qu'à se débrouiller.
 
La deuxième étape va consister à trouver un algorithme décrivant comment obtenir la solution désirée, ce qui nous obligera également à introduire les structures de données nécessaires à sa réalisation. L'algorithme s'exprimera au moyen de constructions de haut niveau (boucles, tests, aiguillages...) et les structures de données pourront être relativement abstraites (piles, listes...). Si nous ne disposons pas d'un langage comme SETL<ref>Ou si nous ne sommes pas prêts à payer en temps d'exécution le prix de la facilité offerte par ce langage...</ref>, nous devrons exprimer notre conception à ce niveau. En Ada, ceci s'exprimerait comme:
<source lang="ada">
declare
type Index is new Positive range 1..N;
Prochain : Index := 1;
 
subtype Diviseurs is Natural range 0..N;
Non_Alloué : constant Diviseurs := 0;
 
Liste : array (Index) of Diviseurs
:= (others => Non_Alloué);
begin
for P in 1..N loop
if N mod P = 0 then
Liste (Prochain) := P;
Prochain := Prochain+1;
end if;
end loop;
end;
</source>
Nous avons choisi de représenter notre liste au moyen d'un tableau, ce qui nous pose des contraintes d'implémentation. Pour pouvoir ajuster nos dimensionnements, nous supposerons que la variable N est déclarée du type Integer<ref>Plus vraisemblablement, dans un programme correctement écrit, elle serait déclarée d'un type dérivé d'Integer.</ref>. Nous ne savons pas a priori combien il y aura de diviseurs, mais il ne peut y en avoir plus de N. Nous allons déclarer un type pour repérer les éléments dans le tableau; bien entendu, la nature de ce type n'a aucun lien avec le type de N: nous choisissons donc un type différent, et non un sous-type. Pourquoi avoir choisi de le dériver de Positive plutôt que de Integer? Tout type servant à compter ne peut intrinsèquement avoir que des valeurs positives; en revanche, le fait que ce type soit limité à N provient de notre problème particulier. Il est donc logique d'exprimer cela en faisant dériver le type Index de Positive, puis en le contraignant à l'intervalle 1..N.
:Natural et Positive sont deux sous-types prédéfinis de Integer, comportant respectivement les nombres positifs ou nuls et les nombres strictement positifs.
En revanche, les diviseurs que nous allons stocker sont de même nature que N: nous préférons donc déclarer Diviseurs comme un sous-type de Natural, et nous exprimons qu'en plus tous les diviseurs sont inférieurs ou égaux à N. Pourquoi avoir autorisé la valeur 0? Nous avons fait ici le choix de conception d'initialiser à 0 les éléments non significatifs du tableau (encore une fois ce n'est ni le seul, ni même certainement le meilleur choix possible). Nous devons donc autoriser cette valeur «de garde». Cependant, pour exprimer ce rôle spécial de la valeur 0, nous déclarons la constante Non_Alloué, qui nous servira plutôt que la valeur 0 elle-même là où ce sera nécessaire: nous exprimons ainsi que nous ne considérons pas 0 comme un diviseur.
 
Il ne nous reste plus qu'à exprimer qu'une liste est un tableau repéré par un Index de Diviseurs garni au départ uniquement de valeurs Non_Alloué. Nous avons choisi d'appeler Prochain la variable servant à repérer l'endroit où mettre le diviseur, pour bien marquer qu'elle désigne le prochain élément à remplir, et non le dernier rempli<ref>Noter qu'il existe un cas particulier où le programme tel qu'il est écrit lèvera l'exception Constraint_Error. Nous laissons à la perspicacité du lecteur le soin de le découvrir...</ref>. Remarquez qu'à ce niveau, notre préoccupation principale a été d'exprimer autant que possible au moyen du langage Ada la connaissance que nous avions du domaine de problème et des propriétés de la solution, notamment au niveau du typage.
 
Les notions utilisées à ce stade (tableau dynamique, boucle à compteur, types abstraits) sont encore assez éloignées de ce que peut connaître la machine. Celle-ci ne manipule que des types élémentaires (adresses, octets, mots mémoire) et des instructions très simples (tests et branchements). L'étape suivante va donc consister à représenter les éléments abstraits au moyen des entités machine. Si nous devions maintenant écrire ce programme en langage C, il deviendrait:
<source lang="c">
main()
{ int liste[100];
int p;
int *prochain;
for (prochain = liste;
prochain < liste+100;
prochain++)
*prochain = 0;
prochain = liste;
for (p = 1; p <= n; p++)
if (n % p == 0)
{*prochain = p;
prochain++;
};
}
</source>
Nous avons dû faire ici un choix d'implémentation supplémentaire: notre besoin est celui d'un tableau de taille choisie dynamiquement. C ne fournit pas cette possibilité<ref>Tout au moins pour des variables locales classiques, comme le permet Ada. Il faudrait faire appel à l'allocateur et gérer un niveau de pointeur supplémentaire.</ref>; nous devons donc la réaliser au moyen d'un tableau de taille fixe. En général, l'espace supplémentaire sera perdu. Si au contraire l'espace est insuffisant, nous courons le risque de déborder du tableau et d'écraser les zones mémoire qui le suivent. Une bonne programmation exigerait donc de vérifier que le tableau ne déborde pas; mais que ferait-on alors? C ne nous fournit pas non plus de mécanisme d'exception. Nous devrions donc adopter une politique particulière de traitement d'exception (comme d'avoir systématiquement une variable de retour pour signaler si la fonction s'est bien terminée), politique qui devrait être traitée par l'appelant... On voit que le traitement complet des cas exceptionnels nous entraînerait vite très loin, et c'est pourquoi beaucoup de programmeurs C préfèrent prévoir largement les tableaux et prier pour qu'ils ne débordent pas...
 
Le fait que nous voulions une initialisation particulière du tableau ne peut plus être donné simplement: nous devons écrire une boucle pour expliquer à l'ordinateur comment initialiser le tableau. Il nous faut même expliquer comment réaliser cette boucle: initialisation, test de fin, incrément... Enfin, les différentes propriétés que nous connaissons sur les types de données sont perdues: nous ne manipulons plus que des adresses (pointeurs) et des int, c'est-à-dire des entiers machine.
 
Cette description utilise donc un niveau adapté au fonctionnement de l'ordinateur, mais encore suffisamment abstrait pour être indépendant d'une architecture machine particulière. La dernière étape consiste à traduire le programme en instructions machine correspondant à l'ordinateur cible, ce que l'on fait lorsque l'on travaille en assembleur. A ce niveau, toute notion de structure disparaît: des notions aussi simples que des boucles doivent être réalisées au moyen de tests et de branchements. La description appropriée est donc l'ordinogramme: un graphe représentant le parcours exact de la machine. De même, toute notion de typage disparaît, puisque l'on ne fait même plus de différence entre des nombres entiers, flottants ou des pointeurs, ni même entre structures de programmes et structures de données: les seules notions restantes sont des instructions, des adresses et des mots mémoire. La notion de tableau, par exemple, devra être réalisée au moyen de l'indexation pour accéder à des mots mémoire consécutifs. Le langage n'est plus à même d'effectuer aucun contrôle, puisque l'on n'est même pas sûr que ce qu'exécutera l'ordinateur correspond au texte du programme (un programme peut se modifier lui-même).
 
Nous voyons donc que la descente de niveaux sémantiques est essentiellement un passage du «quoi» (expression de besoin) au «comment» (solution au besoin). Bien entendu, il n'est nécessaire de spécifier le «comment» que si le langage ne fournit pas directement l'outil adéquat. Par exemple, nous avons eu l'enchaînement: besoin: liste (OK en SETL), réalisée par des tableaux de taille variable (OK en Ada), réalisés par des tableaux de taille fixe (OK en C), réalisée par une zone mémoire indexée. Remarquer que nous nous sommes arrêtés là parce que nous avons supposé que la machine fournissait directement l'abstraction nécessaire: sur une machine plus élémentaire, nous aurions pu devoir réaliser la notion d'indexation par un calcul d'adresse explicite et une indirection. Remarquons également que la descente dans les niveaux d'abstraction s'accompagne d'une perte d'information: la version SETL exprime quasiment directement le besoin; en Ada, nous ne voyons plus explicitement la notion de liste. En C, toute l'information sur les relations logiques entre les différentes données est perdue. Enfin en assembleur, les variables deviennent totalement indifférenciées. Les physiciens (et les théoriciens de l'information) diraient que l'entropie du système augmente.
 
Si cette information n'est plus visible, il est possible de la reconstituer, de même qu'il est possible de faire diminuer l'entropie: mais comme en physique, cela coûte beaucoup d'énergie: il faut reconstruire le comportement dynamique du système (ce qui se passe à l'exécution), puis indentifier quelle structure abstraite de plus haut niveau pourrait avoir ce comportement pour implémentation. Inutile de dire que ce processus est nettement plus difficile que la démarche inverse qui consiste à passer de la structure abstraite à son implémentation. Ce qui nous amène à une autre règle d'or:
:''Tout comportement doit être décrit au moyen de la structure de plus haut niveau disponible.''
C'est dans ce cadre que l'on peut également régler la fameuse «polémique du GOTO», qui fit rage dans les années 70, mais qui n'est pas encore totalement évacuée pour certains. En gros, le GOTO est généralement considéré comme un ordre contraire aux bons principes de programmation, mais d'aucuns argumentent que finalement, les structures de contrôle ne sont que des GOTO déguisés. C'est exact: en fait le rôle d'un langage de haut niveau est précisément de cacher les structures de bas niveau, en leur attribuant un plus haut niveau sémantique. Donc si vous travaillez en un langage de bas niveau (Le défunt FORTRAN IV, le Basic «standard»), vous devrez utiliser des GOTO, et il n'y a pas de honte à cela, à condition que dans une étape de conception précédente vous ayez exprimé votre algorithme au moyen de concepts de plus haut niveau.
 
Insistons sur un point: quel que soit le langage de programmation utilisé, toutes les étapes que nous avons décrites devront être accomplies... mais pas nécessairement par le programmeur. Le compilateur travaille en procédant de la même façon: une phase d'analyse de haut niveau, suivie d'une phase d'expansion destinée à abaisser le niveau sémantique de la description, suivie enfin d'une phase de génération de code. Le niveau d'un langage correspond donc au niveau sémantique à partir duquel un outil automatique (le compilateur) est capable de prendre le relais du programmeur. Un langage est d'autant plus haut niveau qu'il prend le relais du programmeur plus tôt, c'est-à-dire qu'un plus grand nombre de ces étapes seront accomplies automatiquement.
 
Automatiquement, certes, mais accomplies tout de même! Ceci explique pourquoi il est normal que, toutes choses égales d'ailleurs, il faille plus de temps pour compiler un programme écrit dans un langage de plus haut niveau. [Ber89] rapportent qu'il faut quatre fois plus de temps pour compiler un programme Ada qu'un programme Pascal faisant la même chose! D'ailleurs, les compilateurs simples ne se contentent-ils pas d'une ou deux passes alors que les compilateurs Ada en ont souvent près de huit? C'est simplement que partant de plus bas, les premiers ont moins de travail à faire. Ceci ne signifie pas que le travail correspondant à l'abaissement du niveau sémantique ne doive pas être fait: simplement il doit être accompli par le programmeur au lieu d'être pris en charge par le compilateur. Pour être honnête, il faut donc ajouter au seul temps de compilation des langages de bas niveau l'effort de conception supplémentaire (et le coût des erreurs introduites) dû à ce plus bas niveau. A ce moment, les avantages du haut niveau redeviennent évidents: [Ber89] rapportent que depuis que les équipes sur lesquelles ont été effectuées leurs mesures ont abandonné Pascal pour Ada, les factures mensuelles passées en temps de compilation ont diminué... simplement grâce au fait que de nombreuses erreurs qui n'étaient diagnostiquées qu'à l'exécution (et qui nécessitaient plusieurs cycles de recompilation pour être identifiées) sont maintenant «piégées» dès la première compilation.
 
Notons enfin que le gros risque avec les langages de bas niveau est de court-circuiter des étapes. Nous avons vu comment abaisser progressivement le niveau sémantique d'un projet. Mais l'outil final (le langage) étant en général accessible au concepteur, celui-ci tendra à passer directement de l'expression de haut niveau au codage. Cela peut marcher pour des petits projets, mais lorsque la taille du saut (sémantique) augmente... on finit par se casser la figure.
<references />
==Apport des langages de haut niveau==
Les premiers langages de programmation étaient orientés machine: on forçait en quelque sorte le programmeur à penser avec le même vide culturel que la machine. Au moins était-on sûr qu'il n'y avait pas de risque d'interprétations divergentes. En fait, c'était bien entendu extrêmement pénible, ce qui a conduit à l'apparition de langages de plus haut niveau, c'est-à-dire plus proches de la façon de penser du programmeur. Ce faisant, on a également éloigné le niveau de lecture du programmeur du niveau de lecture de l'ordinateur: on a donc accru le risque de divergence entre les deux. Quel est donc l'intérêt des langages de haut niveau?
==Et l'efficacité ?==
 
Il provient essentiellement de ce que ces langages permettent de communiquer à l'ordinateur une partie du «savoir» supplémentaire que possède le programmeur. Autrement dit, au lieu de forcer le programmeur à comprendre le programme comme le fait le compilateur, on a la possibilité d'indiquer à celui-ci un certain nombre d'éléments «culturels» qui rapprocheront son analyse du texte de celle effectuée par l'être humain. Ceci permettra au compilateur d'effectuer des tests beaucoup plus nombreux, et surtout situés à un niveau sémantique bien supérieur. Le programmeur devra donc prendre soin de tenter de décrire, non plus les éléments de la machine dont il a besoin, mais les éléments de son domaine de problème. C'est pourquoi les types prédéfinis, qui appartiennent au domaine de la machine, devront (en général) être évités. Prenons un exemple.
 
Dans beaucoup de programmes, et quel que soit le langage de programmation, il est fréquent de trouver des déclarations comme:
<source lang="ada">
Compteur : Integer;
</source>
Nous prétendons que cette déclaration est trompeuse, non maintenable, dangereuse, non portable, inefficace, et pour tout dire, opposée aux principes même du génie logiciel! Pourquoi?
* Elle est trompeuse et non maintenable, car elle autorise des valeurs négatives pour Compteur, alors qu'un compteur ne peut contenir que des valeurs positives ou nulles. Bien sûr, le lecteur humain sait qu'un compteur ne peut être négatif, mais cette information n'est pas fournie au compilateur. Il n'y a donc aucune garantie que la variable sera effectivement utilisée comme compteur. Le programmeur de maintenance avisé devra alors résoudre une question difficile: est-ce que le programmeur d'application a utilisé le type Integer par simple paresse, ou n'y a-t-il pas quelque endroit où il a mis une valeur négative dans Compteur pour réaliser une «grosse astuce»? Le seul moyen de résoudre la question est d'aller inspecter toutes les utilisations de la variable.
* Elle est dangereuse et non portable, parce qu'elle ne se préoccupe pas de la question de la borne supérieure. Tout type de donnée possède nécessairement une limite aux valeurs qu'il peut stocker. Il est vraisemblable que le programmeur n'a voulu qu'une variable «entière», et n'a même pas songé à cette limite, dont la valeur dépend ici de l'implémentation. Trop souvent, les programmeurs déclarent des variables du type Integer simplement pour éviter d'avoir à penser au problème de la borne supérieure, comme s'il s'agissait réellement d'entiers mathématiques. Un tel programme, qui fonctionne parfaitement sur une machine 32 bits, peut avoir un comportement imprévisible sur une machine 16 bits.
* Elle est inefficace, parce qu'une définition plus précise aurait permis au compilateur d'utiliser la mémoire de façon plus efficace, et même de générer un meilleur code. Supposons que cette variable serve à indexer un tableau de 10 éléments; si elle avait été déclarée avec une contrainte correspondant à sa vraie utilisation, le compilateur aurait été capable de n'utiliser qu'un octet de mémoire (au lieu de 2 ou 4), et surtout aurait pu éliminer toutes les vérifications de débordement lorsqu'elle était utilisée pour indexer le tableau.
* Plus que pour toutes ces excellentes raisons, elle est contraire aux principes du génie logiciel parce que tout le savoir qu'avait le concepteur des propriétés de sa variable n'a pas été écrit, et ne sera donc pas transmis aux personnes chargées de la maintenance.
Qu'aurait-il dû faire? Etudier le domaine de problème et refléter ses connaissances dans l'expression du langage, ce qui aurait donné:
<source lang="ada">
type Valeurs_à_compter is range 0..40_000;
Compteur : Valeurs_à_compter;
</source>
Noter que le compilateur aurait utilisé la précision de cette déclaration pour réserver juste l'espace nécessaire: 16 bits si la machine disposait d'entiers non signés, 24 ou peut-être 32 sinon, mais de toute façon le type le plus économique permettant de répondre au besoin exprimé.
 
Ceci n'est pas possible avec un langage qui travaille au niveau machine; quel type aurions-nous choisi en C, s'il nous fallait être portable? Certainement pas int (et encore moins short), car même si l'on peut supposer qu'il soit au moins sur 16 bits sur la plupart des machines, on ne peut supposer plus, et c'est insuffisant ici. unsigned? Le langage n'offre aucune garantie qu'il dispose d'une étendue plus grande que int. Nous aurions dû nous rabattre sur long (ou mieux unsigned long), avec le risque de réserver 64 bits par variable sur certaines machines, là où 16 auraient suffi.
===Les types de données abstraits===
Puisque nous voulons désormais travailler sur les éléments du domaine de problème, il nous faut utiliser non des types machine, mais des abstractions d'éléments du monde réel. Les types de données correspondants sont appelés types de données abstraits. Un tel type est caractérisé par un ensemble de valeurs et un ensemble d'opérations qui lui sont applicables.
 
Si cette notion de type de donnée abstrait est commode pour l'utilisateur, elle est fort éloignée de ce que peut traiter un ordinateur: des nombres. Par conséquent, chaque attribut et chaque opération doivent être implémentés au moyen d'éléments de plus bas niveau. Il existe donc deux vues différentes d'un type de donnée abstrait: la vue abstraite, ou externe, qui exprime les propriétés utilisables de l'abstraction (ce que l'on appelle dans la terminologie Ada la spécification) et les mécanismes utilisés pour concrétiser cette abstraction (pour Ada, l'implémentation). Comme pour tout élément logiciel, la solution au problème de l'implémentation d'une abstraction donnée n'est jamais unique; cependant, toutes les solutions possibles doivent être considérées comme équivalentes du point de vue de l'utilisateur, tant qu'elles fournissent des abstractions sémantiquement équivalentes<ref>Sauf éventuellement du point de vue des performances.</ref>.
 
En principe, il devrait donc être possible de remplacer n'importe quelle implémentation d'une abstraction par une implémentation différente sans perturber les utilisateurs. Si une spécification dépend malencontreusement d'un mécanisme d'implémentation particulier, cela s'appelle une surspécification: la spécification est trop précise, car elle mentionne des éléments qui n'appartiennent plus à la vue abstraite. Attention: une spécification peut imposer des contraintes (en temps d'exécution, en mode de gestion de la mémoire) qui interdisent certaines implémentations: il n'y a pas surspécification tant que ces contraintes résultent du besoin extérieur, et non d'une vue a priori d'une méthode d'implémentation particulière.
 
Inversement, si un utilisateur fait appel à des propriétés d'un type de donnée abstrait qui ne font pas logiquement partie de l'abstraction, on dira qu'il viole l'abstraction. Dans un cas comme dans l'autre, il devient impossible de remplacer une implémentation par une autre, et l'indépendance entre spécification et implémentation est perdue.
<references />
===Le langage comme outil de vérification===
Eviter toute surspécification est une tâche difficile qui requiert un grand talent d'abstraction, mais prévenir les violations d'abstraction peut être obtenu par les vérifications effectuées par le langage. Est-ce si important que le langage vérifie cela? Et à quoi cela sert-il après tout que le langage effectue des vérifications?
 
Reprenons le problème à la base. A un moment donné, l'ensemble des valeurs de toutes les variables d'un programme constitue ce que l'on appelle le vecteur d'état. Le plus vaste vecteur d'état comporte toutes les combinaisons de bits possibles pour toutes les variables du programme. Nous appellerons cet ensemble les états matériels. Mais seul un sous-ensemble de celui-ci est autorisé par les règles d'un langage de programmation de haut niveau, puisque le compilateur effectue (au moment de la compilation ou de l'exécution) un certain nombre de vérifications, qui empêchent certaines combinaisons de se produire. Nous appellerons ce sous-ensemble les états autorisés. A l'intérieur de ce sous-ensemble, un ensemble encore plus réduit est constitué des états accessibles par une exécution correcte du programme. Nous appellerons ce dernier ensemble les états corrects. Ces différents états sont résumés dans la figure 8.
 
Figure 8: Les différents états d'un programme
 
Les états matériels doivent nécessairement englober les états autorisés, autrement le langage ne serait pas compilable (sur cette machine). De même, les états autorisés doivent inclure les états corrects, autrement le programme désiré ne pourrait être écrit dans le langage. Notez cependant que les états autorisés ne peuvent coïncider exactement avec les états corrects: cela reviendrait à prouver formellement le programme. Il y a donc nécessairement des états incorrects, correspondant à la zone des états autorisés n'appartenant pas aux états corrects, qui peuvent être atteints en fonction des seules règles du langage; c'est la responsabilité du programmeur de faire en sorte que ceci ne se produise jamais durant l'exécution du programme. D'une certaine façon, un «bug» est une brèche par laquelle on atteint un état incorrect.
 
C'est sur ce schéma que l'on comprendra le mieux la différence fondamentale de philosophie entre les langages C/C++ et Ada. C a été conçu comme une alternative à l'assembleur; ses concepteurs l'ont même qualifié d'«assembleur portable». Comme en assembleur, l'idée était de permettre de faire faire «ce que l'on voulait» à la machine; la crainte était que le langage empêche le programmeur de faire ce qu'il souhaitait. Le langage a donc cherché à étendre au maximum les états autorisés pour les rapprocher au maximum de l'ensemble des états matériels. C'est en ce sens que beaucoup considèrent que C est un langage très puissant: il permet de tout faire.
 
Tout, y compris des catastrophes... L'approche suivie par Ada est radicalement différente. Le but était d'avoir un langage fiable et de haut niveau pour des applications critiques. Il fallait pour cela non seulement fournir des outils permettant la définition de vues abstraites, mais également interdire l'accès aux détails de l'implémentation afin d'éliminer la plus grande partie des états incorrects. Le but recherché était l'inverse de celui de C: il s'agissait de resserrer l'ensemble des états autorisés autour des états corrects, éliminant ainsi autant d'états incorrects que possible. Vu ainsi, on peut (et même on doit) se poser la question: à quoi cela pourrait-il servir d'autoriser des états incorrects? La vraie question n'est donc pas pourquoi faudrait-il contraindre, mais quel pourrait être l'intérêt de ne pas contraindre.
 
Cette approche présente tout de même un risque: il peut arriver qu'à force de restreindre les états autorisés, on ampute une petite partie des états corrects. Autrement dit, le langage ne laisse pas, au nom de règles de sécurité, le programmeur faire ce qui lui est nécessaire. Certaines échappatoires (Unchecked_Conversion par exemple, nous y reviendrons) ont été prévues au niveau du langage, mais malgré tout il peut arriver que le langage interdise certaines pratiques qui bien qu'utiles ou même nécessaires dans certains cas, seraient trop dangereuses en général. Le programmeur devra donc changer sa conception pour se plier aux règles générales. Cette nécessité (qui paraît révoltante au programmeur C!) ne paraîtra choquante que dans le monde du logiciel. Toutes les autres branches de l'industrie sont coutumières du fait, et savent qu'elles doivent ajuster leurs conceptions à des règles de sécurité dont l'applicabilité est parfois discutable compte tenu des particularités du contexte, mais qui ont force de loi. Songez par exemple que le défunt Concorde était muni de doubles commandes mécaniques, dont il était de notoriété publique qu'elles étaient quasiment inutilisables dès que l'avion avait pris une certaine vitesse... et qui ont coûté très cher dans le bilan de poids de l'appareil. Mais personne n'aurait osé prendre la responsabilité de les supprimer, et c'est normal, car il s'agissait d'un dispositif de sécurité fondamental.
 
Quel est l'avantage d'une telle approche? Si on recherche une erreur dans un programme qui compile, on peut être assuré que les règles du langage sont respectées. Autrement dit, on peut garantir que le vecteur d'état du programme appartient toujours à l'ensemble des états autorisés<ref>Sauf en cas de bug du compilateur. Ceux-ci sont suffisamment rares pour que l'on puisse faire cette hypothèse, mais c'est ce qui rend les erreurs dues aux compilateurs si difficiles à trouver.</ref>. En réduisant le nombre d'états autorisés mais incorrects, un langage rigoureux diminue la probabilité d'erreurs (moins d'états incorrects sont accessibles) et facilite la maintenance (moins d'états incorrects sont à envisager lors de la recherche d'erreurs). De plus, le compilateur pourra faire usage de cette connaissance supplémentaire pour optimiser le programme. Pour prendre un exemple concret, si l'on doit manipuler des couleurs, il est possible depuis Pascal (le langage !) de définir un type énumératif:
<source lang="ada">
type Couleurs is (Noir, Rouge, Bleu, Vert,
Jaune, Magenta, Cyan, Blanc);
</source>
En faisant ainsi, on garantit qu'il n'est pas possible de multiplier deux couleurs, ni d'affecter à une couleur une valeur numérique incorrecte. Il paraît équivalent de déclarer:
<source lang="ada">
Noir : constant := 0;
Rouge : constant := 1;
...
</source>
Après tout, n'est-ce pas ce que fait le compilateur? Et cela marchera aussi bien que le type énumératif, tout au moins tant que personne ne tentera de violer l'abstraction. Ce que l'on perd, c'est la vérification par le compilateur que seules les propriétés abstraites sont disponibles. Représenter des couleurs par des nombres est une façon d'implémenter la notion abstraite de couleur, ce qui est le travail du compilateur. Si vous le faites vous-même, vous autorisez l'utilisateur de l'abstraction à travailler au niveau de l'implémentation plutôt qu'au niveau abstrait, et vous ouvrez ainsi une voie vers des états incorrects.
 
Enfin, l'avantage peut-être le plus important de l'approche de haut niveau est le contrôle qu'elle peut exercer sur la conception. Puisque maintenant le compilateur a de l'information sur les éléments de plus haut niveau du problème, une faute de conception se traduira souvent par une incohérence au niveau du typage qui sera interceptée par le compilateur. Combien de fois avons-nous vu des programmeurs nous appeler à la rescousse pour un problème de langage, alors qu'il s'agissait en fait d'un problème de conception! En voici un exemple typique:
 
Un étudiant débarqua un jour dans mon bureau en grognant: «Ada est vraiment un langage stupide. Regardez ça.» Il travaillait sur le simulateur d'un ordinateur un peu particulier. Il avait utilisé un tableau d'Integer pour représenter la mémoire. La mémoire était divisée en pages de 512 mots. Il avait donc les déclarations suivantes:
<source lang="ada">
type Mémoire is array (0..32767) of Integer;
type Page is array (0..511) of Integer;
 
M : Mémoire;
P : Page
</source>
Le problème était que le compilateur refusait l'affectation suivante:
<source lang="ada">
P := M(0..511); -- Tranche des 512 premiers
-- éléments de M
</source>
alors que la boucle suivante, qui était sémantiquement strictement équivalente, était acceptée:
<source lang="ada">
for I in 0..511 loop
P (I) := M (I);
end loop;
</source>
Evidemment, du point de vue du langage, il y avait de bonnes raisons. La première affectation s'effectuait entre une variable de type Page et une expression de type Mémoire; comme il s'agissait de deux types différents, l'affectation était interdite. Dans le second cas, on n'affectait que des composants, tous du type Integer, et chacune des affectations était autorisée.
 
Je ne tentai même pas de m'aventurer dans une telle explication, car l'étudiant aurait pensé (comme le lecteur le pense actuellement) que le typage fort ne fait qu'apporter des complications au malheureux utilisateur en l'obligeant à écrire une boucle explicite. La conversation continua comme ceci:
 
:''Moi:'' Pourquoi avoir déclaré le type Page?
:''Etudiant:'' Parce que j'en avais besoin!
:''Moi:'' Pensez-vous qu'une page soit une entité de nature différente d'une mémoire?
:''Etudiant:'' Hmm... Non, c'est une sorte de mémoire.
:''Moi:'' Quelle est donc la différence?
:''Etudiant:'' Une page fait toujours 512 mots, alors que la mémoire peut avoir n'importe quelle taille a priori.
:''Moi:'' Ah! Comment peut-on exprimer ceci en Ada?
:''Etudiant:'' J'y suis: une Mémoire est un type tableau non contraint, et Page est un sous-type contraint de Mémoire!
Il modifia son programme de la façon suivante:
<source lang="ada">
type Mémoire is array (Natural range <>) of Integer;
subtype Page is Mémoire (0..511);
 
M : Mémoire(0..32767);
P : Page;
</source>
et dès lors, son affectation fonctionna sans problème, puisque P et M appartenaient au même type. Que pouvons-nous conclure de cet exemple? Le programme de l'étudiant comportait une faute de conception, puisque l'utilisation qui était faite des types ne correspondait pas aux abstractions souhaitées, ce qui avait provoqué l'erreur de compilation. Le mouvement naturel de l'étudiant avait été de s'en prendre au langage et de chercher un moyen de tourner le contrôle. Il y était arrivé en ne travaillant pas globalement sur les objets, mais au niveau des composants. Remarquez que cela était possible parce que le type Mémoire était visible: s'il s'était agi d'un type privé, il n'aurait pas pu s'en sortir aussi aisément. En fait, il était descendu d'un niveau d'abstraction, puisqu'il ne travaillait plus globalement sur la Mémoire, mais sur la façon dont cette mémoire était représentée. A ce plus bas niveau, la sémantique était plus pauvre, et il avait pu violer l'abstraction.
 
Cet exemple montre bien comment un langage très strict peut piéger les fautes de conception; si une telle situation était apparue en C++ (en C, aucun contrôle de ce type n'est possible), il est vraisemblable que l'étudiant se serait contenté d'un vigoureux forçage de type (type cast) pour obliger la compilation à passer... et cacher le problème de conception. Dans le contexte du génie logiciel, où fiabilité et facilité de maintenance sont primordiaux, le langage de programmation doit fournir des contrôles rigoureux pour éliminer un maximum d'états incorrects dès l'étape de compilation. On peut résumer ceci ainsi:
:'''Ce qui fait la valeur d'un langage de programmation, ce n'est pas ce qu'il autorise, c'est ce qu'il interdit.''
<references />
===Autres formes de protection===
La programmation est un domaine plein de pièges subtils, dont certains requièrent une grande attention pour être évités... à moins que le langage ne prenne en charge les problèmes. Si les types abstraits sont l'outil de base de protection du programmeur, il existe d'autres cas où une formulation de plus haut niveau permet d'obtenir une meilleure sécurité.
 
Un exemple caractéristique est celui des boucles en C. Prenez un programmeur C moyen (ou même expérimenté), et demandez-lui comment il écrirait l'équivalent de la boucle Ada:
<source lang="ada">
for I in A .. B loop
...
end loop;
</source>
Il vous répondra immédiatement:
<source lang="ada">
for (I = A; I <= B; I++) {
...
}
</source>
Demandez-lui alors ce qui se passe si, par exemple, I est un octet (ou plus exactement un char, puisqu'en C les caractères sont des nombres!), que A vaut 0 et B vaut 255. Ce cas de figure n'a rien d'exceptionnel: il s'agit d'une simple boucle sur l'ensemble des caractères. Horreur! Lorsque I atteint 255, il repasse à 0, car il n'y a pas de notion de débordement en C<ref>En toute rigueur, la norme C n'interdit pas de «planter» le programme dans ce cas, mais la plupart des compilateurs ne le font pas. Inutile de penser à un traitement d'exception (même en C++, qui a pourtant les exceptions, les débordements ne sont pas traités).</ref>, et la boucle ne s'arrête jamais! Ce qui est surprenant (et inquiétant), c'est que nous avons tenté cette expérience avec de nombreux programmeurs C, et qu'aucun n'a jamais pensé au problème! On est donc en droit de penser que tous les programmes C marchent par hasard, parce qu'on ne s'aventure jamais trop près des cas limites... Bien entendu, des tests de la suite de validation Ada assurent que la boucle produite par le compilateur marche toujours correctement dans ce cas de figure. Le programmeur Ada est donc protégé par le langage d'une erreur grave, sans même avoir à s'en préoccuper.
 
Cet exemple montre bien que le langage peut effectivement fournir un degré de sécurité supplémentaire qui n'a rien à voir avec un quelconque problème de conception. Remarquons qu'encore une fois, ce résultat est obtenu par le fait que la description Ada se situe un niveau d'abstraction au-dessus de la description C: le programmeur décrit ce qu'il veut obtenir (une boucle de 0 à 255) et non comment l'obtenir (par une affectation, un test et un incrément – algorithme dont nous venons de montrer qu'il était faux)<ref>Signalons au lecteur qui chercherait la solution correcte en C qu'il n'est pas possible de coder une boucle for (au sens d'Ada) au moyen d'une simple boucle while (dont le for C n'est qu'une variante). Il faut nécessairement un exit (break en C).</ref>.
<references />
==Et l'efficacité?==
Si l'utilisation de fonctionnalités de haut niveau améliore indiscutablement la lisibilité, la sécurité et la maintenabilité, l'on est en droit de se demander si cela n'a pas un effet adverse sur l'efficacité. En fait, l'efficacité est souvent invoquée comme raison d'utiliser des langages de plus bas niveau. Voyons donc plus précisément ce problème.
 
Tout d'abord, il faut savoir de quelle efficacité l'on parle. Il existe souvent des contraintes temporelles au cahier des charges: celles-là doivent être impérativement respectées, car un programme qui ne répond pas à son cahier des charges est un programme faux. La question de l'efficacité se pose seulement après: si le programme vérifie largement ses contraintes, comment va-t-on utiliser la marge supplémentaire? Les machines utilisées au début de l'informatique étaient incroyablement lentes par rapport aux machines (mêmes individuelles) actuelles: on disait alors que le bon programmeur était celui dont les programmes allaient le plus vite possible. A l'époque, les programmes étaient de taille plus modeste qu'aujourd'hui, et l'on se souciait peu de la maintenance... Cette mentalité n'a plus lieu d'être aujourd'hui: un bon programme, c'est d'abord un programme évolutif, fiable et facile à maintenir. De plus, la puissance des machines rend acceptables des solutions autrefois impossibles. Ceci ne signifie pas que l'efficacité ne doive pas être recherchée, mais seulement que, encore une fois, il faut trouver le bon compromis, car ce que l'on gagne en efficacité, on le perd sur d'autres aspects, tels que lisibilité et portabilité. Par exemple, imaginons un programme interactif (comme une interrogation de base de données). S'il donne la réponse au bout de dix secondes, c'est inacceptable (et vraisemblablement trop pour les contraintes du cahier des charges): il faut faire quelque chose. Si on améliore les performances pour que les réponses parviennent en moins d'une seconde, c'est bien; l'attente est sensible pour l'utilisateur, mais acceptable. Si l'on fait passer ce temps à un dixième de seconde, c'est parfait: la réponse semble instantanée. Si l'on poursuit dans cette voie pour obtenir un temps de réponse d'un centième de seconde, on perd son temps: l'utilisateur ne sera pas sensible à la différence, et on risque de le payer cher sur d'autres aspects.
 
D'autre part, il n'est pas du tout évident qu'une description de plus haut niveau se traduise nécessairement par une perte d'efficacité; au contraire, le compilateur est capable d'utiliser le supplément d'information pour mieux optimiser le code. Considérons cet exemple:
<source lang="ada">
I : Integer; -- Ce qu'il ne faut pas faire !
S1 : String(1..10);
S2 : String(1..10);
begin
I := Calcul_compliqué; -- (1)
S1(I) := S2(I); -- (2)
</source>
Il n'y a pas de vérification au moment de l'affectation à I en (1), mais comme l'on ne sait pas a priori quelle est la valeur retournée, il faut vérifier à chaque utilisation (deux fois en (2) ) que la valeur est correcte. Si nous informons le compilateur de notre intention d'utiliser cette variable pour indexer des chaînes comme ceci:
<source lang="ada">
subtype Index is Integer range 1..10;
I : Index;
S1 : String (Index);
S2 : String (Index);
begin
I := Calcul_compliqué; (1)
S1(I) := S2(I); (2)
</source>
alors le compilateur effectue une vérification lors de l'affectation en (1), mais il n'est plus nécessaire d'en effectuer lors des utilisations en (2). Comme on utilise les variables plus souvent qu'on ne les modifie, cette approche est généralement préférable. C'est tellement vrai que beaucoup de programmeurs sont déçus lorsqu'ils tentent d'accélérer leurs programmes en les compilant «sans vérification»: le gain excède rarement quelques pour-cents. Ceci signifie simplement que l'optimiseur a été capable d'éliminer de lui-même tous les tests redondants... et donc que le (petit) gain d'efficacité se fait certainement au détriment des tests réellement importants, donc avec un impact maximal sur la fiabilité! Conséquence: en général, en Ada, on laisse toutes les vérifications même dans la version finale, commercialisée, du programme.
 
Il faut savoir que les techniques modernes d'optimisation permettent d'obtenir des résultats surprenants. Nous avons ainsi connu un programmeur qui avait écrit un programme de façon «ignoble», persuadé que cela irait plus vite. Il reçut du compilateur le message suivant:
:WARNING: frame of control too complicated; optimizer gives up.
Du coup, le programme allait moins vite que s'il avait écrit son code proprement et laissé l'optimiseur faire son travail... A travers cet exemple, on notera la différence d'état d'esprit: le rôle du compilateur C est de fournir une traduction directe vers les instructions machine, l'optimisation étant du ressort du programmeur. Du coup, un compilateur C, même relativement simple, produira un code acceptable – d'où la réputation d'efficacité traditionnellement attachée au C. Inversement, le rôle du compilateur Ada est de libérer (autant que possible) le programmeur des contraintes de bas niveau; un compilateur sans optimiseur performant serait catastrophique, mais le langage a été conçu pour permettre des optimisations très perfectionnées. Il est donc possible d'optimiser beaucoup plus du code Ada que du code C (cf. [Tar93] pour un exemple de programme initialement développé en C qui n'a pu tenir ses performances qu'après un recodage en Ada).
 
Enfin, l'on ne saurait trop rappeler que la recherche d'efficacité doit se faire d'abord par la recherche d'algorithmes performants. Les bénéfices que l'on peut en retirer sont de plusieurs ordres de grandeur supérieurs à ce qui peut être obtenu par des «astuces» de codage. Le programmeur doit toujours garder à l'esprit la règle des 90/10: un programme passe 90% de son temps dans 10% de son code; or ces fameux 10% sont en général très difficiles à identifier. La seule constante que nous ayons rencontrée dans des projets où se posait un problème d'efficacité est que le point critique n'était jamais là où le supposait le programmeur. Il est indispensable de disposer d'outils de mesure lors de toute recherche d'amélioration des performances.
 
En conclusion, nous dirons que l'efficacité est importante, mais que ce n'est plus le seul critère à prendre en compte pour juger de la qualité d'un programme, et que les principes du génie logiciel continuent à s'appliquer même pour l'écriture de programmes critiques sur le plan des performances. Quand l'on voit la fréquence avec laquelle l'efficacité sert de justification abusive à des pratiques peu recommandables, on ne peut que conclure par une paraphrase:
:''Efficacité, que de bugs on commet en ton nom!''
==Conclusion==
Le typage fort et la vérification par le langage des abstractions sont des dispositifs de sécurité indispensables: ils ne peuvent être pris à la légère. Cette démarche est universellement adoptée dans d'autres branches de l'industrie. Par exemple, une machine dangereuse comme un massicot ne peut fonctionner que si l'opérateur appuie simultanément sur deux boutons, disposés de telle façon qu'il soit obligé d'utiliser ses deux mains: on est ainsi assuré qu'il n'y a pas une main qui traîne sous la lame. On pourrait penser que l'opérateur d'un massicot a une perception beaucoup plus directe du risque qu'il y aurait à laisser une main dans la machine que le programmeur d'utiliser un forçage de type; il est cependant connu qu'en l'absence de dispositif de sécurité, des accidents se produisent. Il n'y a pas de raison de supposer que les choses se passeraient différemment en programmation. Remarquons au passage que l'utilisation de dispositifs de sécurité dans l'industrie ennuie les opérateurs et diminue bien souvent la productivité; mais l'on considérerait comme inacceptable de sacrifier la sécurité à de tels impératifs.
 
Donner au développement de logiciel un caractère industriel demande que l'on reconnaisse la nécessité d'un contrôle plus pointu, et que la sécurité des systèmes complexes ne peut pas ne dépendre que du talent des individus qui les conçoivent. Nous conclurons en illustrant cette différence fondamentale dans la perception du rôle du programmeur par le rapprochement de deux citations tirées respectivement d'introductions aux langages C et Ada:
:''C a été conçu dans l'idée que le programmeur est quelqu'un de raisonnable et qui sait ce qu'il fait.''
:''Ada a été conçu en tenant compte du fait que le programmeur est un être humain.''
==Exercices==
# En C++, il est possible de définir des classes fournissant la notion de tableau avec contrôle de débordement. Quels en sont les inconvénients par rapport à des tableaux vérifiés par le compilateur? Discuter sur le plan des principes du génie logiciel et sur le plan des optimisations possibles du code généré.
# Rechercher dans les langages autres qu'Ada les restrictions qui sont d'ordre méthodologique et celles dues à la technique du compilateur. Que peut-on en conclure?
# Nous avons mis en garde le lecteur contre l'utilisation du type Integer. Ces arguments sont-ils applicables au type String? Au type Boolean?
# Ecrire au moyen d'une boucle loop simple l'équivalent exact de la boucle for d'Ada. Attention, cet exercice est plus difficile qu'il n'y paraît!
 
=Liaison entre méthode et langage=
==Le problème de la documentation==