Philologie de la programmation
La question du langage en informatique a ceci de particulier qu’il s’agit toujours d’écrits. L’examiner relève donc
moins de la linguistique, où l’oralité est incontournable, que de la philologie où elle est absente : les langages
de programmation, malgré leur modernité, partagent avec les langues mortes d’être des langues muettes. En revanche leur épigraphie a peu d’importance, l’archéologie de leurs
supports, perforés d’abord (ruban, carte et listing), performants ensuite, révélant peu d’impact sur leurs
systèmes d’écriture.
Plus important est de distinguer des familles
de langages et de s’interroger sur leurs rapports au travers de 8 problématiques survenues, parfois simultanément, dans l’histoire de la programmation. Une conclusion dégagera une ligne
directrice dans l’évolution de la programmation, qui se termine aujourd’hui en ligne brisée.
Le geste et le texte
- La programmation se distingue de la configuration, en ce qu’il s’agit d’un texte (étymologie de "programme" : écrire d’avance) sous forme d’une suite d’instructions qui effectue le calcul à mesure qu’elle est lue, et non d’une suite de gestes préalable au déclenchement d’un calcul ultérieur ;
- Les instructions sont écrites dans un alphabet dit binaire car formé de deux lettres, traditionnellement représentées par 0 et 1 alors que la réalité matérielle ne l’impose pas : c’est une tension électrique, un champ magnétique ou n’importe quoi d’autre qui est pris comme valeur de référence, tout autre valeur désignant l’autre lettre ;
- Les mots sur cet alphabet binaire sont écrits sans espace pour les séparer, comme cela était fréquent avant l’imprimerie ; la lisibilité est induite par la convention que tous ont même longueur, un multiple de 8 depuis la standardisation des mémoires en octets ;
- Un texte en langage binaire est interprétable de multiples façons (nombres, instructions, image codée, son codé etc), il faut un contexte pour trancher, plus encore que de décider si un texte est du latin ou de l’anglais, langages fondés sur le même alphabet ;
- Une instruction est une suite de mots écrits avec l’alphabet binaire, le premier contenant le CODOP (code de l’opération) qu’il faut connaître pour programmer en binaire, effort de mémoire en contradiction avec ce qu’apporte une machine bourrée de mémoires, d’où l’apparition de langages moins abscons.
Abstraire pour assembler
- Les langages d’assemblage utilisent des
lettres et quelques autres signes pour nommer les instructions de manière symbolique ;
- A l’instar de nombreux systèmes d’écriture primitifs, leur mise en page est en forme de tableau et ignore le couple minuscule / majuscule ;
- L’indispensable codage en binaire d’instructions écrites
en langage d’assemblage est assurée par l’assembleur, utilitaire si indispensable que s’en est suivie la métonymie "programmer en assembleur" et non "en langage d’assemblage", et le remplacement fréquent de
programmer par coder, alors que ce n’est pas le programmeur qui code mais l’assembleur ;
- La diversité de ces langages s’est considérablement réduite avec l’invention du (micro-)processeur, les fabricants d’ordinateurs ne faisant plus que de l’assemblage autour de celui-ci, comme ont disparu des dialectes régionaux suite à la concentration des pouvoirs politiques ;
- La conception de machines virtuelles (qu’il faudrait plutôt nommer à présent processeurs virtuels), destinées à faciliter le portage des programmes a lui aussi contribué à masquer les disparités ;
- Une itération, aussi appelée boucle,
s’effectue par un saut en arrière dans la suite d’instructions exécutées, et une conditionnelle en général par un saut en avant ; au moment de l’écriture du programme la longueur de ce qu’il faut sauter
est déterminable immédiatement pour le saut en arrière, mais pas pour le saut en avant car rien n’est encore écrit. D’où le besoin d’étiquettes permettant une référence en avant résolue par une lecture en deux passes ;
- C’est la première apparition de l’abstraction en informatique.
Langages d’assemblage : la différence est dans le contexte
-
processeur Z80 cadencé à 2Mhz (1976) L’initiale J indique un saut (Jump)
| _P: |
AND |
A,1 |
|
JNZ |
L1 |
|
MOV |
A,2 |
|
RET |
| L1: |
MOV |
B,A |
|
MOV |
A,3 |
|
CMP |
A,B |
|
JZ |
VU |
|
MOV |
D,B |
|
SLA |
D |
| BOUCLE: |
CALL |
MODULO |
|
OR |
C,C |
|
JZ |
L2 |
|
ADD |
B, 2 |
|
CMP |
B, D |
|
JNC |
BOUCLE |
| VU: |
MOV |
A,0 |
| L2: |
RET |
processeur M4 cadencé à ~ 4GhZ (2024) L’initiale B indique un saut (Branch)
| _P : |
TBNZ |
X0, #0, LBB0_2 |
|
MOV |
X0, #2 |
|
RET |
| LBB0_2: |
CMP |
X0, #3 |
|
B.LT |
LBB0_7 |
|
UCVTF |
D0, X0 |
|
FSQRT |
D0, D0 |
|
FCVTAS |
X9, D0 |
|
CMP |
X9, #4 |
|
B.LT |
LBB0_7 |
|
MOV |
X8, X0 |
|
MOV |
X0, #3 |
| LBB0_5: |
UDIV |
X10, X8, X0 |
|
MSUB |
X10, X10, X0, X8 |
|
CBZ |
X10, LBB0_8 |
|
ADD |
X0, X0, #2 |
|
CMP |
X0, X9 |
|
B.LT |
LBB0_5 |
| LBB0_7: |
MOV |
X0, #0 |
| LBB0_8: |
RET |
|
- Ces programmes cherchent le plus petit diviseur d’un entier naturel fourni au départ, de 8 bits pour le premier, de 64 pour le second, information invisible dans le texte ;
- Elle est pourtant incontournable pour gérer le débordement arithmétique, emblématique de la différence entre mathématiques et informatique quant aux nombres.
Programme = données
- L’assembleur est donc un programme qui a comme donnée d’entrée un programme écrit en langage
d’assemblage et fournit en résultat sa forme binaire, qui sera placée en mémoire afin d’exécution ; une suite de bits passe donc du statut de donnée au statut de programme ;
- Ce double point de vue était encore plus au centre des premiers ordinateurs :
- Pour parcourir une suite de valeurs, il faut effectuer
une boucle en passant à la valeur suivante à chaque itération, autrement dit
en incrémentant l’adresse de cette suite ;
- Seul moyen alors, incrémenter l’opérande de l’instruction comportant
cette adresse, autrement dit modifier le texte de son programme au cours de son exécution !
- Un mauvais repérage entrainant une réécriture quasi‐aléatoire d’un tel programme, des alternatives sont apparues : instructions à double indirection (coûteuse en temps) puis ajout aux processeurs de petites mémoires internes, les registres ;
- Suite à cette évolution, toute écriture dans la mémoire où réside le programme est à présent considérée comme une intrusion, volontaire (piratage) ou involontaire (bug) ;
- En conséquence, l’auto‐modification de programme a été définitivement interdite et plus généralement la synthèse de programme directement en mémoire ;
- Il n’en reste pas moins que les ordinateurs ont cette incroyable
faculté de permettre à des textes de s’auto‐modifier lorsqu’ils s’énoncent.
Traduire, c’est expliciter la mémoire
Un langage d’assemblage n’offre qu’une écriture plus commode du langage binaire d’un processeur précis ; les langages de programmation dits évolués ont pour but de dispenser le programmeur de gérer la mémoire, à des degrés divers selon les langages. Ils nécessitent d’être traduits ou interprétés, ces deux termes n’ayant pas en informatique les mêmes rapports qu’en linguistique traditionnelle ;
- interpréter consiste à exécuter des instructions au fur et à mesure que le programme est lu ; en cas de boucle la lecture devra être refaite ;
- traduire consiste à lire le programme en entier, en déduire un programme binaire, sauvegardé sous forme de fichier qui pourra être exécuté éventuellement plusieurs fois ;
- il s’agit cependant de deux extrêmes d’une suite continue :
- le texte est presque toujours d’abord transformé par une analyse lexicale, éliminant les espaces inutiles et remplaçant les noms par leur index dans leur nomenclature, cette transformation pouvant aller jusqu’à une suite d’instructions d’une machine virtuelle, finalement interprétée mais qui pourrait être soumise à un assembleur spécifique ;
- certaines fonctionnalités des langages destinés à la traduction peuvent être interprétées, notamment celles concernant les périphériques ;
- plus généralement un programme est rarement opérationnel tout seul,
il appelle implicitement des utilitaires, mal nommés par le faux ami librairies ;
- c’est pourquoi un fichier exécutable est obtenu d’un programme en langage évolué non seulement grâce à un traducteur, mais grâce à un compilateur qui va gérer la cohabitation de tous les éléments nécessaires, le traducteur produisant un fichier dit en binaire relogeable ;
- les adresses définitives seront calculées par l’éditeur de liens, lors de la production d’un fichier unique, exécutable ;
- à noter que cette création en deux temps permet la compilation séparée des fichiers nécessaires, permettant une économie de temps de compilation si les dépendances entre fichiers sont explicitées, leur description étant à la limite d’une sorte de programmation.
Comparaison entre traduction et interprétation
Un programme en langage
C
avant sa traduction en
M4 ou autre.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
long int prim(unsigned long int a)
{
if (!(a%2)) return 2;
if (a >= 3) {
long int m = round(sqrt(a));
long int i = 3;
while (i < m)
{
if (!(a%i)) return i;
i+=2;
}
}
return 0;
}
Un programme en langage
Lisp
près à l’interprétation.
(dl prim (a)
(if (eq (% a 2) 0)
2
(if (< a 3)
0
((lambda (i m)
(if (eq (% a i) 0)
i
(if (< i m)
0
(self (+ i 2)))))
3
(round (sqrt a))))))
L’analyse syntaxique
L’analyse syntaxique est une opération dont la complexité dépend du langage :
- dans les langages d’assemblage, une instruction est
l’équivalent d’une phrase réduite à un verbe à l’impératif, l’opération souhaitée, et de rares compléments réduits à un nom, les opérandes, toujours écrits après le verbe ;
- avec en plus la contrainte d’une unique instruction par ligne, ces langages sont à la portée de la lecture en une passe de l’analyse lexicale ;
- les premiers langages évolués ont permis des compléments plus complexes mais conservé le mode impératif,
d’où le nom de programmation impérative, l’opération principale étant l’affectation d’une valeur à une variable ;
- accepter des opérandes plus complexes nécessite une analyse syntaxique, qui va devoir lire le texte à plusieurs reprises (ou en mémoriser des passages, ce qui revient au même) ;
- le terme anglais est parser, sa terminaison en "er" incitant les informaticiens français à le conjuguer comme un verbe du premier groupe, alors que le verbe "phraser", en usage chez les musiciens et les acteurs, aurait dû s’imposer tant il dit bien qu’il s’agit de percevoir la structure du discours ; quant au néologisme phraseur, il était visiblement impensable ;
- maîtriser ce problème a nécessité sa modélisation avec le concept mathématique de monoïde, qui diffère du groupe en ce qu’il n’admet pas d’élément symétrique : il y a bien un élément le neutre, le mot vide dans le cas du monoïde syntaxique, mais pas moyen de le produire en composant deux mots ; ce qui est écrit est définitivement écrit ;
- le monoïde syntaxique permet de définir des grammaires formelles sous forme de règles de réécritures d’une suite de symboles en une autre suite, certains symboles étant terminaux (les lettres) d’autres non, en particulier celui nommé axiome à partir duquel s’effectuent les réécritures jusqu’à disparition de tous les non terminaux.
Des grammaires pour qui ?
Quelques exemples de grammaires (les symboles terminaux sont en italiques, l’axiome est le premier symbole terminal employé) :
- Exemple 1, les expressions bien parenthésées, aussi nommé langage de Dyck de niveau 1
S ⟿ (S)S
S ⟿ mot-vide
- Exemple 2, tentative de modélisation d’un sous-ensemble du français
Phrase ⟿ Sujet Verbe Complement
Sujet ⟿ elle
Sujet ⟿ nous
Complement ⟿ le voile
Complement ⟿ ça
Verbe ⟿ ferme
Verbe ⟿ fermons
- Exemple 3, nouvelle tentative, remplacement des deux dernières règles ci-dessus par
elle Verbe ⟿ elle ferme
nous Verbe ⟿ nous fermons
- Exemple 4, le charme du poète, l’enfer de l’informaticien
Phrase → Sujet Verbe Complement
Phrase → Sujet Complement Verbe
Sujet → la belle
Sujet → la belle ferme
Complement → le voile
Complement → le
Verbe → voile
Verbe → ferme
Désambiguez et Décontextualisez, sinon je craque
Il existe toute une série de questions sur les grammaires formelles,
certaines irrésolues :
- il n’existe pas d’algorithme universel déterminant si deux grammaires produisent le même langage ;
- il n’existe pas d’algorithme universel déterminant si une grammaire est ambigüe ou non ;
- il existe des langages inhéremment ambigus, c’est-à-dire qui ne sont définissables que par des grammaires ambigües (exemple : les mots formés de deux palindromes) ;
- les grammaires dont les parties gauches des règles peuvent contenir des non terminaux, appelées grammaires contextuelles n’ont aucune propriété algébrique intéressante, d’où le recours à la statistique pour les langues naturelles (un aveu d’échec déguisé en réussite) ;
- l’ensemble des grammaires acontextuelles, dites aussi algébriques, est fermé par union et concaténation, mais pas par intersection
(a* bn cn et an bn c* sont définissables par des grammaires acontextuelles,
mais pas an bn cn) ;
- l’ensemble des grammaires acontextuelles comporte un sous-ensemble, les grammaires dites rationnelles, caractérisées par des parties droites de règles n’ayant qu’un seul non terminal et en fin de règle ;
- les langages définis par ces grammaires sont analysables par automate fini c’est-à-dire sans consommation de mémoire ;
- pour les autres grammaires acontextuelles , il faut une pile dont la longueur dépend de la donnée d’entrée, autrement dit il faut procéder à une allocation dynamique de mémoire ;
- théorème fondamental : toute grammaire acontextuelle est l’intersection, à un codage près, d’un langage de Dyck et d’une grammaire rationnelle.
De la théorie grammaticale à sa pratique
Cette mathématisation de la linguistique, finalement peu convaincante pour les langues naturelles, a eu un grand impact sur l’informatique. Outre la maîtrise de l’analyse syntaxique des langages de programmation évolués, on lui doit certaines nouveautés :
- un langage défini par une grammaire rationnelle est exprimable par une notation synthétique nommée expression rationnelle (regular expression en anglais) qui constitue une sorte de langage de programmation, incomplet mais très bien adapté à la reconnaissance de motifs ;
- le théorème fondamental a inspiré la définition du méta-langage XML, les langages respectant son formalisme, nommés langages de balisage, bénéficiant de son analyseur syntaxique universel, SAX, auquel la plupart des langages de programmation donne accès ; il ne reste plus alors qu’à écrire un validateur vérifiant que les lexèmes utilisés sont autorisés par la grammaire là où ils apparaissent ;
- HTML a respecté longtemps XML, avant que HTML5, imposé par les GAFAM au W3C, ne l’abandonne, revendiquant ouvertement dans son manuel d’être incohérent malgré ses sous-ensembles SVG et MATHML, antérieurs à lui, qui eux le respectent.
La récursivité
Le mécanisme de pile est aussi apparu lorsqu’il a fallu calculer
une fonction récursive,
c’est-à-dire une fonction dont la définition fait appel à elle-même.
Voici deux exemples canoniques, définis sur les entiers naturels :
- la factorielle
Factorielle(0) = 1
Factorielle(n+1) = (n+1) * Factorielle(n)
- la fonction de RózsaPéter
RózsaPéter(0,p) = p+1
RózsaPéter(n+1,0) = RózsaPéter(n,1)
RózsaPéter(n+1,p+1) = RózsaPéter(n, RózsaPéter(n+1,p))
- à chaque appel il faut mémoriser le calcul à faire au retour, mais cela fait, cette mémorisation est inutile, si bien que la mémoire allouée est aussitôt récupérée ;
- ce mécanisme est donc simple et a fini par être adopté par tous les langages de programmation, après avoir longtemps considéré cet usage comme un snobisme ;
- ce style est appelé programmation fonctionnelle par une traduction approximative de l’anglais, car ne restituant pas le sens de programmation par des fonctions ; cette programmation s’opposant à la programmation impérative, il aurait mieux valu s’inspirer de la grammaire française en l’appelant programmation indicative, ces définitions donnant plutôt des indications qu’une suite d’opérations ;
- certains interprètes ou compilateurs arrivent d’ailleurs à calculer certaines fonctions récursives sans pile ; pour factorielle par exemple, la multiplication étant associative, il est possible de réécrire cette fonction avec un argument supplémentaire et un appel récursif terminal, équivalent au saut en arrière des langages d’assemblage, autrement dit à une boucle ;
- cette dérécursion n’est toutefois pas toujours possible, comme en témoigne la fonction RózsaPéter qui n’a pas de définition non récursive.
La fonction de la fonction en programmation
Ces langages dits fonctionnels sont allés plus loin dans l’abstraction, de la mémoire en particulier mais pas seulement :
Pas d’étreinte fatale en cas d’heureux événement
Les différentes programmations dont il vient d’être question ne se confrontent jamais à la question du temps, ni relatif, ni absolu,
pourtant incontournable pour programmer les
systèmes d’exploitation (curieuse traduction de l’anglais operating system qui sous-entend plus facilement qu’il s’agit d’une machine et non d’un être humain). Ceux-ci sont confrontés à plusieurs problèmes :
- ils doivent permettent à plusieurs processus de s’exécuter sans se gêner les uns les autres, ce qui impose à un processus de recevoir parfois une interruption pour reprendre l’exécution d’un autre, lui-même interrompu plus tard pour revenir au premier, le programmeur devant avoir conscience qu’entre deux instructions qu’il écrit, tout peut arriver ;
- les interruptions peuvent aussi provenir des périphériques, et sont traitées par un preneur d’événement (event handler en anglais), induisant un style spécifique nommé programmation événementielle ;
- les processus ayant accès à des ressources communes (typiquement le disque), il faut en empêcher les accès concurrents, ce qui se fait à l’aide de verrous ou de sémaphores dont la programmation est délicate car pouvant conduire à un interblocage ou étreinte fatale (dead lock en anglais) ;
- des processus travaillant de concert, sur la même machine ou sur plusieurs, se nomment des co-routines permettant notamment de résoudre le problème de la disjonction parallèle (si A ou B est vrai, le résultat du calcul doit être "Vrai" même si A ou B provoque un calcul qui ne peut pas se terminer), insoluble sans preneur d’événement ;
- enfin, cette programmation système permet de programmer des serveurs, programmes volontairement non terminant, attendant qu’un client leur envoie un requête de service ;
- ces programmes non terminant illustrent une autre différence entre mathématiques et informatique : ils ne relèvent de l’algorithmique que dans la question de la consommation de la mémoire, car l’algorithmique exige la terminaison du calcul ; ici intervient une autre problématique, l’accessibilité d’un état, qui doit, ou ne doit pas, être possible ;
- en définitive, cette programmation système s’oppose à la programmation séquentielle comme l’art de la conversation s’oppose au monologue.
Synchronisation et interruption
#!/bin/bash
trap 'echo "Je sors de cet enfer"; exit 2' INT TERM
cmd=$1
shift
for i in *
do
$cmd $i &
PID[$!]=$i
done
status=0
for i in *
do
wait -n -p process
e=$?
echo processus sur ${PID[$process]} sort avec code de retour $e
if [ $e -ne 0 ]
then
status=$e
fi
done
exit $status
De l’échec considéré comme une réussite (à tort)
Les langages évolués ont dispensé le programmeur de gérer la mémoire, la programmation dite fonctionnelle était à la limite de l’affranchir de la conception de l’algorithme, ce que la dénommée intelligence artificielle à chercher à atteindre entièrement. Il convient cependant de clarifier les choses :
- dire qu’"il n’y a pas d’algorithme" pour par exemple le problème du voyageur de commerce est inexact : il y a toujours l’algorithme calculant toutes les combinaisons et les comparant pour trouver la meilleure, simplement le temps de calcul est humainement inacceptable ;
- pour ramener cet algorithme par défaut à un temps raisonnable, mais souvent encore excessif, l’énoncé de règles ou de contraintes permet de condamner d’avance certains sous-ensembles en effectuant un retour en arrière (backtrack en anglais) avant l’hypothèse apparaissant erronée ;
- mais cet énoncé ne permet pas toujours d’éviter un calcul infini lorsque l’ensemble des combinaisons possibles est infini ;
- ce style de programmation est souvent nommé programmation logique, de nouveau par une traduction trop littérale de l’anglais (qui voudrait d’une programmation illogique ?), programmation par règles ou par contraintes semble plus indiqué ;
- malgré un engouement excessif ayant débouché sur une grande déception (le fiasco de la 5e génération japonaise), ce style se survit sur des domaines particuliers et finis : la reconnaissance de motifs, déjà évoquée, et les feuilles de styles en cascade (CSS) utilisées en typographie numérique ;
- aujourd’hui l’intelligence artificielle a renoncé à l’intelligibilité
en se fondant sur des algorithmes statistiques reposant sur des milliards de paramètres.
Fin de l’histoire des langages, début des académies
Quelques langages de programmation sont apparus plus récemment, souvent pour gérer commodément de nouveaux périphériques,
mais aucun nouveau paradigme de programmation n’en est résulté. Les questions de bon usage, autrement dit de méthodologie, sont passées au premier plan. A côté du génie logiciel et des gestionnaires de versions distribués, outils indispensables aujourd’hui mais sans impact sur les aspects linguistiques des textes qu’ils gèrent, deux notions méritent l’attention en philologie de la programmation :
- l’héritage de classe ;
- le contrôle de type.
Héritage de classe
Depuis que les programmes se comptent en milliers de lignes, produits par plusieurs personnes de surcroît, le problème de
la collision de noms de variables ou de fonctions, autrement dit leur homonymie, est devenue fréquente. Pour y faire face, a été introduit le mécanisme d’héritage de classe :
- une classe est un ensemble, désigné par un nom inemployé par les autres classes du programme, de variables et de fonctions dont les noms sont totalement libres ;
- pour utiliser une classe, il faut en construire une instance grâce à un opérateur souvent nommé new ; seules les instances d’une classe auront accès à ses variables et fonctions ;
- il suffit à une équipe de programmeurs de s’entendre sur les noms des classes, bien moins nombreuses que les variables et les fonctions, pour éviter des collisions de noms ;
- une classe peut hériter d’une autre, c’est-à-dire la compléter par d’autres variables et fonctions, mais aussi remplacer certaines des fonctions de la classe mère, ce qui permet de structurer encore plus finement l’ensemble des fonctions d’un programme, une classe ayant une certaine ressemblance avec la notion de champ lexical en linguistique ;
- cette méthodologie a pris le nom abscons de programmation objet et a même revendiqué définir de nouveaux langages, alors que le concept a été repris, sous le nom d’extension objet, par presque tous les langages de programmation, montrant qu’il s’agit d’une méthodologie universelle, et non d’un paradigme nouveau ;
- cependant le phénomène de rémanence des variables de classes est un aspect vraiment novateur de cette programmation, permettant aux fonctions d’une classe de mémoriser des valeurs entre deux appels, ce que la programmation dite fonctionnelle, dans son acception stricte, ne permet pas, et que la programmation impérative ne permet qu’avec un risque important dû à la collision de nom.
Du type à la preuve
Le contrôle de type consiste à vérifier, idéalement une fois pour toutes avant l’exécution, que toute opération s’applique à des arguments conformes. Ce contrôle est plus ou moins difficile selon les langages :
- pour les langages impératifs classiques, il est consiste essentiellement à vérifier que les opérations arithmétiques portent sur des variables déclarées numériques, c’est facile et c’est d’ailleurs apparu très tôt ;
- pour les langages dits fonctionnels ou s’en inspirant, les fonctions d’ordre supérieur induisent un contrôle plus abstrait, par exemple que deux variables sont de même type sans savoir lequel ;
- ce contrôle est limité par la théorie sous-jacente du lambda-calcul, toute fonction récursive n’y étant pas typable : pour éviter la référence en avant des appels récursifs, il faut donner la fonction en argument d’elle-même, d’où une circularité dans le calcul du type ;
- lorsque la récursivité n’est pas utilisée, l’utilisation d’un système de type peut prendre l’aspect d’une programmation par preuves, cas particulier de la programmation dite fonctionnelle ;
- certains langages vont jusqu’à interdire non seulement la récursivité mais aussi les boucles ayant un nombre d’itérations inconnu d’avance ; c’est le cas notamment des langages d’interrogation de bases de données, mais on perd alors en expressivité ;
- le contrôle de type revendique de s’occuper de la sémantique du langage, après son contrôle syntaxique vu précédemment ; mais il s’agit en fait de ne contrôler qu’une cohérence définie par le système de type, le sens du programme échappant à cette opération ;
- cette cohérence définit donc autoritairement un bon usage reflétant les limites des théories de types en cours ; c’est pourquoi que la capture de continuation et la réflexivité sont interdites dans les langages imposant le contrôle de type.
Classer et Typer, c’est tout ce que tu sais faire
Un exemple de classe héritant d’une autre et déclarant le type de ses variables et fonctions :
un validateur XML fondé sur la programmation événementielle de l’analyseur SAX disponible en PHP :
class sax_validateur_min {
protected array $dtd;
protected array $erreurs = array();
function __construct(array $dtd)
{
$this->dtd =$dtd;
}
function ouvrante (XmlParser $phraseur, string $nom, array $attr) : void
{
if (!isset($this->dtd[$nom])) {
$this->errreurs[]="Element $nom inconnu";
} else {
$att = $this->attributs($phraseur, $nom, $attr);
}
}
function fermante (XmlParser $phraseur, string $nom) : void
{}
function texte (XmlParser $phraseur, string $texte) : void
{}
function attributs(XmlParser $phraseur, string $nom, array $attr) : array
{
$ok = array();
$atts = $this->dtd[$nom][1];
foreach ($atts as $att => list($type, $mode)) {
if (isset($attr[$att])) {
$ok[] = $att;
unset($attr[$att]);
} elseif ($mode == 'REQUIRED') {
$this->erreur[]="Attribut $att de $nom absent";
}
}
if ($attr) {
$ids = join(" ", array_keys($attr));
$this->erreur[]= . "$ids attributs de $nom inconnus";
}
return $ok;
}
}
class sax_validateur_contenu extends sax_validateur_min {
protected array $pile = array();
protected array $ids = array();
function ouvrante (XmlParser $phraseur, string $nom, array $attr) : void
{
if (!isset($this->dtd[$nom])) {
$this->erreur[]= "Element $nom inconnu";
array_push($this->pile, 'ANY');
} else {
$ok = $this->pile
? preg_match("@[^\w:-]" . $nom . "[^\w:-]@", end($this->pile))
: ($nom == key($this->dtd));
if (!$ok) $this->erreur[]= "Element $nom incongru";
array_push($this->pile, ' ' . $this->dtd[$nom][0] . ' ');
$att = $this->attributs($phraseur, $nom, $attr);
}
}
function fermante (XmlParser $phraseur, string $nom) : void
{
array_pop($this->pile);
}
function texte (XmlParser $phraseur, string $texte) : void
{
if (trim($texte) AND !preg_match('@#PCDATA@', end($this->pile)))
$this->erreur[] = "texte incongru";
}
function attributs(XmlParser $phraseur, string $nom, array $attr) : array
{
$attr = parent::attributs($phraseur, $nom, $attr);
foreach ($attr as $nom => $val) {
if ($this->dtd[$nom][1][0] == 'ID') {
if (isset($this->ids[$val])) {
$this->erreur[] = "ID $val en double";
} else {
$this->ids[$val] = 1;
}
}
}
return $attr;
}
}
Conclusion : A ma gauche l’expressivité, à ma droite la sécurité
L’histoire de la programmation n’est sans doute pas terminée : certains problèmes restent ouverts et les innovations matérielles n’ont certainement pas fini de l’obliger à évoluer. En revanche, l’histoire de l’expressivité de la programmation est, sinon terminée, en tout cas sur une pente subitement descendante.
La raison en est la priorité accordée à la sécurité, ce que l’on peut
énoncer d’une manière troublante au regard de sa résonance politique :
- la liberté d’expression a été réduite pour sécuriser le
système d’exploitation.