- Un calculateur programmable automatique et universel
- Machine, langage, données, mémoire
- Langages, métalangages, traduction
- Machine universelle
- Des données aux résultats
- Traduire les langages, traiter les données
- Création d’un compilateur
- Langages laxistes
- Langages rigoureux
- Maintenance du logiciel
- Ada
Alors que la science informatique est au cœur d’une industrie dont dépendent toutes les autres, l’homme de la rue comme le dirigeant politique en ignorent tout. L’expliquer devrait être le rôle de l’Éducation nationale, mais elle n’a commencé à le faire que très récemment, avec réticence et des moyens dérisoires. Alors je m’y emploie ici depuis quelque temps.
C’est l’industrie principale de notre siècle, puisqu’aujourd’hui la valeur de tout objet industriel, avion, automobile ou radiateur électrique, comporte 40% d’informatique et de micro-électronique. Les répercussions mondiales de la récente pénurie de composants électroniques ont démontré ce rôle crucial. Alors voici ma contribution à cet effort.
L’ordinateur est un appareil pour une science, qui se nomme informatique, en cela il n’est pas une machine comme les autres. On désigne par le mot ordinateur la machine complète, munie de ses accessoires tels que clavier, écran, prise réseau, disque dur, etc. Lorsque l’on veut parler plus précisément de l’organe qui exécute les programmes, on dit le processeur, ou l’unité centrale, c’est-à-dire le cœur de l’ordinateur, réalisé aujourd’hui par un seul composant électronique d’un ou deux centimètres-carrés, le microprocesseur. Cette unité centrale comporte l’unité arithmétique et logique (ALU) qui contient les circuits logiques des instructions, l’unité de contrôle (ou de commande) qui supervise l’enchaînement des instructions dans l’ordre indiqué par le programme, et les organes d’entrée-sortie qui commandent les échanges de données avec la mémoire centrale et les organes périphériques, sans oublier l’horloge qui assure le synchronisme des opération. Pour le dire autrement, l’unité centrale d’un ordinateur, qui il y a soixante ans pouvait peser une ou deux tonnes et était constituée de milliers de composants, se présente aujourd’hui sous la forme d’un composant unique de quelques grammes, le microprocesseur. Aucune industrie humaine n’a connu un progrès aussi rapide soutenu pendant aussi longtemps.
Emmanuel Saint-James a pu écrire dans son livre La Programmation applicative (de Lisp à la machine en passant par le λ-calcul) que « la programmation est une transmission d’un raisonnement à une machine, capable de le reproduire. Par cette reproduction, sans laquelle il n’est point de science, l’ordinateur s’affirme comme un instrument d’objectivation du raisonnement. Il s’inscrit dans la lignée des appareils qui ont permis de passer de l’expérience empirique à l’expérimentation scientifique : l’informatique se distingue des mathématiques par un outil d’expérimentation permettant d’observer, vérifier, réfuter un raisonnement. »
Cette « transmission d’un raisonnement à une machine » s’effectue par le truchement d’un langage : l’ordinateur a un langage en lui, constitué d’instructions qui effectuent des opérations sur des données. Les instructions sont des objets physiques, en l’occurrence des circuits électroniques qui réalisent ces opérations conformément aux règles de l’algèbre de Boole [1]. Quand on dit que l’ordinateur a un langage en lui, ce n’est pas une métaphore. Le langage constitué de ces circuits électroniques sera appelé langage machine.
Les données sont enregistrées dans la mémoire (l’anglais storage, moins anthropomorphique, serait moins source de confusion). Le texte du programme, qui mentionne les instructions à effectuer et les données auxquelles elles s’appliquent, est lui aussi enregistré dans la mémoire, l’unité de commande (ou de contrôle) parcourt ce texte et exécute les instructions selon la séquence convenable. Enregistrer le texte du programme dans la même mémoire que les données est l’idée la plus révolutionnaire de l’architecture inventée par John von Neumann, elle transforme le programme d’objet matériel (les cartes perforées des métiers à tisser de Vaucanson et de Jacquard) en description textuelle d’un raisonnement abstrait, ce qui permet le développement pratique d’une science informatique dont Alan Turing avait formulé le principe théorique.
On peut programmer en langage machine : c’est difficile, laborieux, extraordinairement inefficace, et de plus chaque modèle d’ordinateur a son propre langage, qu’il faut apprendre à chaque fois. Aussi, depuis les années 1950, les informaticiens ont-ils dépensé une part considérable de leur énergie à créer des langages plus commodes, plus abstraits, moins liés aux caractéristiques physiques du processeur. Tous ces langages sont des métalangages du langage machine. Quand on a écrit un programme dans un de ces langages, afin qu’il puisse être exécuté, il faut le traduire en langage machine, le seul que le processeur puisse interpréter. Cette traduction est effectuée par un programme nommé compilateur. Il existe une hiérarchie de ces langages, du plus concret (proche du langage machine, « de bas niveau ») au plus abstrait (« de haut niveau »). Au bas de cette hiérarchie se trouvent les langages assembleurs, dont les instructions correspondent à celles du langage machine (il y a donc un assembleur différent pour chaque modèle de processeur), mais écrites plus commodément.
Plus on monte dans la hiérarchie des langages, plus on gagne en abstraction, c’est-à-dire que l’on a moins à se préoccuper du fonctionnement technique de l’ordinateur, et que les programmes peuvent fonctionner indifféremment sur des ordinateurs de types différents, mais cette abstraction a un prix qui se paie en efficacité. Et pour écrire les programmes du système d’exploitation, qui établissent le lien entre ce que perçoivent les programmeurs et ce qui se passe dans les circuits, on aura toujours besoin de langages de bas niveau, tels que C, Rust ou les langages assembleurs.
Cette propriété de l’ordinateur, avoir un langage en soi, en fait une machine universelle. L’ordinateur est un automate qui peut traiter tous les problèmes pour lesquels existent des solutions calculables (il y a des problèmes sans solution et des solutions incalculables). La méthode de calcul d’une telle solution, cela s’appelle un algorithme ; la réalisation pratique de cet algorithme est un programme informatique. La programmation est l’art de réaliser des algorithmes pour calculer ces solutions. Entendons-nous bien : depuis Pascal et plus encore depuis Leibniz nous savons que le calcul ne se limite pas aux nombres, il s’étend à la logique, et Leibniz a été suivi sur ce terrain par Boole, Frege, Church et Turing, pour finalement donner naissance au versant théorique de l’informatique. Pour le versant pratique, que Pascal et Leibniz ont également parcouru, on nommerait plutôt von Neumann.
Voilà pourquoi l’ordinateur n’est pas du tout une machine comme les autres.
Afin d’expliquer la même chose sous un autre angle, on peut dire que pour obtenir d’un ordinateur qu’il vous fournisse des résultats, par exemple les tableaux statistiques du recensement de la population, il faut lui fournir des données, en l’occurrence les bulletins individuels recueillis par les enquêteurs de l’Insee et dûment enregistrés par des humains sur un support lisible par ordinateur, jadis des cartes perforées, plus tard des bandes magnétiques, puis toutes sortes de supports magnétiques. Avoir les données sur support informatique ne suffit pas, il faut également fournir à l’ordinateur les instructions précises et détaillées qui décrivent les opérations à effectuer pour obtenir les résultats voulus à partir des données disponibles.
Ce processus de description des opérations à effectuer, comme mentionné ci-dessus, se nomme la programmation, on écrit des programmes qui lisent les données et énumèrent, dans un langage spécial, les opérations qui mènent aux résultats (enfin on l’espère). Ces langages spéciaux, ou langages de programmation, diffèrent radicalement des langages humains : ils ne tolèrent ni l’ambiguïté ni le sous-entendu que l’ordinateur serait bien en peine d’interpréter, ils sont rigoureusement univoques, et très limités dans leur expressivité. À la fin des années 1960 les principaux langages de programmation se nomment Fortran, Cobol, Lisp et PL/1, plus tard apparaîtront C, Java, Ada, Python et Rust, peu importe, ils sont plus ou moins agréables selon les goûts mais finalement tous équivalents. Le travail de l’informaticien consiste donc, au premier chef, à écrire des programmes pour obtenir des résultats à partir de données. Un programme est un texte qui décrit les opérations qui mènent des données aux résultats. La programmation est proprement magique : il suffit de soumettre ces phrases à l’ordinateur pour qu’elles déclenchent des actions. Ainsi, dire c’est faire, ce qui relève d’un pouvoir divin.
Pour obtenir d’un ordinateur qu’il fournisse un résultat il faut donc écrire un programme, et il existe pour ce faire des langages de programmations. On peut choisir un langage de programmation parce qu’il est bien adapté au problème à résoudre, mais les questions de tournure d’esprit du programmeur, voire ses goûts personnels, jouent aussi leur rôle. Les controverses relatives aux langages sont une des principales distractions des informaticiens.
Une question importante en programmation est celle du typage des données : pour faire simple, disons que si nous avons des données de type carotte et des données de type salade, il faudra veiller à ne pas additionner des carottes à des salades. Sauf s’il existe une méthode pour convertir par un procédé chimique inédit des salades en carottes, auquel cas on effectue la conversion, puis on peut faire l’addition.
Quel que soit le langage, le programme, pour être exécuté, doit in fine être traduit dans le langage machine de l’ordinateur utilisé, le seul que cet ordinateur sache interpréter. Le programme qui effectue cette traduction s’appelle un compilateur. Pour chaque langage de programmation il doit exister au moins un compilateur pour le traduire. L’écriture du compilateur doit respecter la définition du langage. La définition du langage comporte notamment la description des types de données disponibles, et celle des opérations autorisées sur ces données.
Il faut un compilateur pour traduire en langage machine les programmes écrits dans un certain langage. Mais le compilateur est lui-même un programme, alors il a fallu soit l’écrire directement en langage machine, tâche exténuante, soit le compiler, mais pour cela il a fallu un compilateur, ce qui nous renvoie au problème initial. Comment cela a-t-il bien pu se passer ?
Entre le langage machine et les langages compilés il y a un niveau intermédiaire, le langage assembleur. À chaque instruction du langage machine correspond une instruction du langage assembleur, écrite sous forme littérale au lieu du code binaire du langage machine. Il est ainsi possible à un être humain normal d’écrire un programme en langage assembleur, je dirais même que quiconque envisage le métier d’informaticien doit avoir essayé, même si c’est plus laborieux qu’avec des langages d’un niveau d’abstraction plus élevé. Le programme qui traduit le langage assembleur en langage machine se nomme... assembleur, et il est beaucoup plus simple que le compilateur d’un langage plus abstrait. À l’origine des temps, quand il n’y a que le langage machine et les dispositifs matériels de chargement du programme en mémoire, il faut écrire en langage machine un micro-assembleur, qui ne saura traduire qu’un sous-ensemble du langage assembleur, mais qui permettra d’écrire dans ce langage incomplet un assembleur plus puissant, et après quelques itérations un assembleur complet. En pratique, aujourd’hui, si on veut créer un assembleur pour un nouveau modèle de processeur, on utilise un ordinateur existant et on écrit pour cet ordinateur un assembleur qui génère du langage machine pour le nouveau processeur, et de façon générale on sait écrire des compilateurs qui compilent sur une machine de type A des programmes destinés à s’exécuter sur une machine de type B, complètement différente, cela s’appelle la compilation croisée. Tout cela est possible grâce à l’architecture de von Neumann, qui permet de considérer le texte d’un programme comme des données que l’on peut créer et transformer au moyen d’un autre programme.
Certains langages sont laxistes, leur définition n’exige pas que le compilateur vérifie que le programmeur n’a pas additionné des carottes à des salades. C’est au programmeur de savoir ce qu’il fait. Et d’ailleurs ce laxisme peut avoir des avantages : si j’écris un programme d’optimisation du remplissage du coffre de la voiture au retour du marché, et que je sais qu’un kilo de carottes occupe autant de place qu’une salade, additionner salades et carottes sans me soucier de l’éventuelle méthode de conversion des salades en carottes peut satisfaire ma paresse. Mais la plupart du temps ce laxisme engendre des erreurs de programmation, par défaut d’attention du programmeur, parce que l’enchaînement des opérations peut être compliqué, réparti entre plusieurs sous-programmes (qui sont comme des chapitres du programme d’ensemble) d’un logiciel de grande taille. Une proportion importante des erreurs de programmation sont des erreurs de typage. Une erreur de programmation se manifeste soit par l’arrêt brutal du programme sans produire de résultat, soit par le fait que le programme ne s’arrête jamais (sauf à couper l’alimentation électrique de l’ordinateur), soit par la production d’un résultat faux. Dans tous les cas, s’il s’agit par exemple du programme de pilotage de la fusée Ariane, c’est plutôt embêtant, mais même s’il s’agit du logiciel comptable d’une PME, cela peut être catastrophique pour la PME.
D’autres langages sont moins laxistes, leur définition exige que le compilateur vérifie que le programmeur n’a pas additionné des carottes à des salades. Formulé ainsi cela semble trivial, mais pour un programme constitué d’un grand nombre de sous-programmes avec des données de structures complexes (comme des tableaux à plusieurs dimensions) ce n’est pas évident. Alors, chaque fois que le programmeur aura tenté d’additionner carottes et salades, ou d’appliquer à des salades une opération réservée aux carottes, le compilateur lui enverra un message d’erreur plus ou moins compréhensible, et refusera de traduire le texte de son programme (son code) en langage machine. Au premier abord cela rend la programmation plus laborieuse et c’est pénible, mais en définitive cela évite beaucoup d’erreurs à l’exécution. On dit qu’avec de tels langages il est plus difficile d’obtenir un texte accepté par le compilateur, mais qu’une fois que « ça compile » il y a de bonnes chances que le programme soit juste, parce que sa cohérence logique a été vérifiée par le compilateur, dont la vigilance n’est pas limitée par les capacités de concentration finies de l’être humain.
Certains programmeurs préfèrent les langages laxistes aux langages rigoureux, parce qu’ils leur donnent une sensation de plus grande liberté. Parfois c’est simplement par paresse, parce que les langages rigoureux ont, pour cette raison même, une syntaxe plus complexe.
Un logiciel de taille moyenne, comme le compilateur Bigloo (version 4.3h) que j’utilise pour mes enseignements, comporte 2 838 262 lignes de code (texte de programmes) réparties dans 5711 fichiers (on peut considérer qu’un fichier correspond à un sous-programme). Les logiciels les plus grands sont les systèmes d’exploitation, tels que Windows, Linux, macOS, Android, qui comptent plusieurs dizaines de millions de lignes de code. Il ne serait pas concevable que des ensembles d’une telle complexité ne comportent pas d’erreurs. Programmer sans erreurs est pratiquement impossible.
En principe les erreurs de syntaxe sont détectées par les compilateurs. Restent les erreurs de logique, souvent des incohérences entre sous-programmes dont les auteurs respectifs se sont mal compris. Lorsque ces erreurs se manifestent, éventuellement à l’occasion de concours de circonstances exceptionnels, elles doivent être corrigées par une opération de maintenance, qui pourra se traduire par une nouvelle version du logiciel, qu’il faudra distribuer aux utilisateurs. En effet, un logiciel est généralement écrit pour être utilisé à de nombreuses reprises, dans des contextes variables, de nature à susciter des circonstances imprévues.
L’exécution d’un programme informatique peut aussi échouer pour une raison qui n’est pas à proprement parler une erreur de programmation, mais qui doit néanmoins retenir l’attention, parce que ce type d’incident est très fréquent : ce sont les échecs provoqués par le contexte technique. Ainsi, un programme qui écrit des données sur disque peut échouer si le disque est plein. La saturation de la mémoire est une autre cause d’échec courante. En principe ces circonstances sont prévisibles par celui qui lance le programme, mais pas par celui qui l’écrit.
Mais une fois vaincues ces erreurs techniques, restent les vraies erreurs de programmation ; elles résultent d’une mauvaise interprétation de l’énoncé du problème à résoudre, ou du choix d’un algorithme inapproprié, ou d’une programmation fautive de l’algorithme, c’est-à-dire que le programmeur aura écrit un texte qui commande des actions qui ne conduisent pas au résultat voulu. Il y a un dicton informatique pour exprimer cela : « l’ennui, avec les ordinateurs, c’est qu’ils font ce qu’on leur demande, pas ce que l’on voudrait qu’ils fissent ».
En fait, un logiciel doit subir des opérations périodiques de maintenance, même en l’absence d’erreurs, parce que le contexte de son utilisation évolue : l’interface avec le système d’exploitation ou avec le système de gestion de bases de données est modifiée, le cahier des charges est révisé, la spécification du langage de programmation évolue.
Il ne fallut pas longtemps après l’invention de l’ordinateur pour que l’on s’aperçoive du coût élevé de la programmation, et différentes méthodes furent essayées pour le réduire. J’ai tenté de décrire ces méthodes dans le livre La Pensée aux prises avec l’informatique disponible en accès libre sur ce site, on peut en dresser la liste :
– inventer de meilleurs langages de programmation et de bonnes méthodes de programmation ;
– réfléchir à une meilleure organisation du travail du programmeur ;
– inventer des outils pour aider le programmeur à écrire et à manipuler ses programmes ;
– inventer des systèmes de description de programmes, qui peuvent être graphiques, ou textuels, ou les deux, pour améliorer le travail de conception ;
– organiser la production de composants logiciels réutilisables ;
– etc.
Toutes ces méthodes furent appliquées, et toutes apportèrent des améliorations, mais aucune ne permit de réduire la programmation à une activité industrielle. Certaines de ces méthodes, notamment celle des systèmes de description de programmes, postulent la possibilité d’avoir, préalablement à la programmation, une activité de conception, à l’issue de laquelle la programmation serait une tâche mécanique : il n’en est rien, parce que la programmation elle-même est une activité de conception, irréductiblement semble-t-il.
Les chercheurs ont même créé des systèmes de preuve de programme, ce que l’on appelle des méthodes de preuve formelle, destinées à conférer à l’écriture de programmes la même capacité auto-vérificatrice que les équations mathématiques sont censées posséder. La recherche dans le domaine de la démonstration automatique de théorèmes n’a jamais cessé d’être active. Il serait erroné de nier l’influence notable que ces recherches ont exercée sur l’orientation prise, notamment, par la conception des langages et des outils de programmation. En fait, les logiciels évoqués ci-dessus comportent un si grand nombre de lignes de texte (un programme, c’est un texte) qu’il serait irréaliste d’en entreprendre la preuve, si tant est que ce soit possible ; on se contente d’essayer de « prouver » les parties jugées les plus critiques, quitte à s’apercevoir plus tard, lorsqu’une erreur survient et amène la destruction d’Ariane V et de sa charge utile, que la criticité n’était pas là où on l’attendait... et l’erreur pas dans le logiciel (cf. Vol 501 d’Ariane 5). Bref, le logiciel, par nature, comporte des erreurs, encore et toujours.
En 1974 le département de la Défense des États-Unis (DoD) constate que ses équipes informatiques et les entreprises sous-traitantes emploient des dizaines (voire des centaines) de langages différents et que cela lui coûte très cher. En effet, pour chaque nouveau langage les programmeurs sont par définition débutants, la qualité des programmes s’en ressent, les coûts de développement aussi, mais surtout les budgets de maintenance s’envolent au fil des ans et excèdent la dépense initiale par un facteur supérieur à 10. De cette réflexion est issu en 1977 un cahier des charges pour un langage unique, universel et rigoureux, adapté aussi bien à la comptabilité qu’au guidage de missiles balistiques, et qui bannisse absolument l’addition de carottes et de salades, ou, en termes plus techniques, qui soit doté d’un typage des données strict et vérifié. L’appel d’offres subséquent est remporté par une équipe française de CII-Honeywell-Bull dirigée par Jean Ichbiah, avec Véronique Donzeau-Gouge que je connaîtrai plus tard au Conservatoire national des Arts et Métiers (Cnam). Le langage qui en résulte est nommé Ada, en l’honneur d’Ada Lovelace, fille du poète Byron, sans doute la première programmeuse de l’histoire lors de sa collaboration avec Charles Babbage, inventeur de plusieurs machines à calculer dans la première moitié du XIXe siècle, précurseur de l’informatique moderne.
On l’aura compris, Ada appartient à la famille des langages rigoureux. Il est adapté à la réalisation de programmes temps réel, c’est-à-dire dont on peut garantir la terminaison dans un intervalle de temps borné, fixé à l’avance. Pour le système de pilotage d’un avion, par exemple, les délais se comptent en dizaines de microsecondes. Ada permet également la programmation d’activités concurrentes, c’est-à-dire qui se déroulent simultanément et de manière coordonnée, en échangeant des informations entre elles. Et tout ceci en conservant les propriétés de cohérence qui garantissent la justesse des résultats.
Ada s’imposera pour les applications critiques, dans l’aérospatial, les systèmes de transport tels que la ligne 14 du métro parisien, les installations nucléaires. Il n’aura pas le succès qu’il mérite dans les autres domaines, d’abord parce que sur les ordinateurs des années 1980 le compilateur, complexe du fait de ses ambitions, était lent, ensuite parce qu’il était cher : les compagnies qui en réalisaient visaient le marché militaire et spatial, dont la solvabilité paraissait sans limite. Le compilateur Ada de Digital Equipment que j’ai acheté en 1983 pour l’Ined, pour la plus petite machine possible, avec 80% de réduction en tant qu’organisme de recherche public, avait coûté 500 000 francs, soit 175 000 euros de 2022. Il existe aujourd’hui un compilateur Ada logiciel libre du projet GNU, Gnat, ce qui le met gratuitement à la disposition de chacun.