vendredi 29 mai 2009

Initialisations

Dans cet article, un programmeur Java se plaint des pointeurs ou références nuls à cause des exceptions qu'ils causent, et imagine un monde sans eux.

Il soulève en fait deux problèmes: l'un est l'existence des pointeurs nuls. Comme quelqu'un l'indique en commentaire, son "inventeur" lui-même reconnaît que c'était une terrible erreur. En ce qui me concerne, étant donné que le langage que je crée s'adresse à des presque non-programmeurs, je n'introduirai la notion même de pointeur qu'en dernier et ultime ressort.

Le second problème soulevé est plus intéressant. Pour résumer, l'auteur peste contre les pointeurs nuls parce qu'ils provoquent des exceptions dans des fonctions qui "oublient" de tester ce cas. Et de proposer quelque moyen de spécifier une valeur par défaut pour les valeurs qui ne seraient pas explicitement initialisées, afin d'éviter les pointeurs "vers rien" (nuls).

Or, là est le vrai problème: "on" a oublié d'initialiser une variable. Que ce genre d'erreur passe au travers des mailles du compilateur n'est pas propre à Java. un GCC par exemple ne m'a pas toujours paru irréprochable sur ce point. Dans la version que j'utilise au travail pour de la compilation croisée, qui n'est certe pas la plus récente (2.95), il peut laisser passer ce type d'erreur, ou au contraire voir ce genre d'erreur là où il n'y en a pas (en particulier quand une structure switch...case est impliquée). On peut estimer qu'il s'agit de "faiblesses" de GCC, mais il y a un cas où l'absence d'avertissement quant à l'usage d'une variable non-initialisée est plus profonde: ce sont les variables-membre de classes. J'ai plus d'une fois, au cours d'un développement, oublié d'ajouter dans le constructeur d'une classe l'initialisation d'une nouvelle variable membre.
La détection de ce genre d'erreur par un compilateur est sans doute une tâche difficile, particulièrement si le langage s'y prête mal. J'ai de plus l'intuition que c'est un problème insoluble dans le cas général, car certainement équivalent au problème de l'arrêt.

Pour Lama, j'avais pris presque dès le départ la décision d'imposer l'initialisation à la déclaration, ce qui paraissait résoudre le problème. Mais en écrivant un commentaire dans ce sens pour cet article, un petit doute que j'avais au sujet de cet solution a en quelque sorte grandit:
Si j'impose à mes programmeurs de fournir une valeur à la déclaration, et que cela les ennuie parce la situation s'y prête mal, ils vont utiliser une valeur "par défaut" (version "manuelle" de ce que suggère l'article). Cela pourrait déboucher à des situations similaires de bogue où on utilise une variable à laquelle on a pas réellement affecté une valeur pertinente. D'un côté, cette solution rend dans tous les cas la valeur de la variable non-aléatoire, ce qui est un progrès réel car le caractère aléatoire d'une valeur non initialisée est ce qui rend parfois très difficile le déboguage. Mais d'un autre côté, le programmeur n'est toujours pas averti de son erreur.

C'est donc exactement l'inverse qu'il faut faire: ne pas autoriser l'affectation d'une valeur par défaut à la déclaration d'une variable, mais vérifier qu'une variable est affectée avant tout usage.
J'ai indiqué tout à l'heure que cette vérification était probablement impossible dans le cas général. Cela n'exclut pas qu'il soit possible de le faire dans certains cas particuliers, et il faudra prendre garde à ce que Lama se situe dans ce cas particulier. Au pire, on pourra tolérer de fausses alertes: si un compilateur n'arrive pas à prouver qu'une variable est bien initialisée avant son usage, c'est a fortiori difficile pour un humain. Une fausse alerte n'est donc pas une si mauvaise chose dans ce cas, et le programmeur peut toujours facilement y remédier.

Cela rejoint un autre dilemme de conception que j'ai dû résoudre ces derniers jours (et c'est sans doute pour cela que cet article a attiré mon attention en premier lieu). Lama a une petite particularité en ce qui concerne les définitions de procédures. Par exemple:

proc square Int x -> Int y
set y : x*x
end

Ceci définit une procédure nommée "square" qui prend un entier en paramètre et retourne un entier (qui est la valeur du paramètre au carré, pour ceux qui auraient un doute). La particularité est que la variable qui sera retournée (y) est déclarée en première ligne, à l'instar de la variable paramètre x. Cette méthode évite l'usage d'un mot-clef "return", ce qui incite à utiliser un style "stricturé" (c'est-à-dire, une programmation strictement structurée). Soit dit en passant, j'utilise couramment un style moins strict - en particulier pour ce qui concerne l'unicité du point de sortie - et donc je n'exclus pas l'existence à terme d'un équivalent de "return" dans Lama. Mais je pense aussi qu'il manque aux langages classiques une structure de contrôle qui permettrait de répondre aux cas où la programmation stricturée semble compliquer les choses.

Dans cette conception, le problème de l'initialisation se pose pour la variable de retour: il faut s'assurer que l'on n'atteint pas la fin de la procédure sans avoir affecté au moins une fois la variable; de même, il faut vérifier qu'elle n'est pas passée en paramètre à une autre procédure avant son affectation.
Un autre problème qu'au départ je n'avais pas anticipé est que si la valeur retournée est une grosse "entité" (j'évite le terme "objet" car Lama n'est pas un langage à objet; les entités dont je parle sont l'équivalent des structures de C), il faut que cette structure soit allouée quelque part. Le compilateur pourrait le faire automatiquement; mais cela pourrait se révéler dans beaucoup de cas superflu (on peut imaginer d'y remédier par une optimisation, mais cela coûte quelque complexité dans le compilateur). De plus, il lui faudrait initialiser la structure à des valeurs par défaut, une opération qui risque également d'être redondante avec les actions du programmeur, et ne résout en fait pas grand chose: dans l'hypothèse où le compilateur fait tout automatiquement, la valeur initiale par défaut serait probablement zéro, qui peut constituer un casus belli pour certaines procédures ou fonctions.

La tactique de la non initialisation préventive exposée plus haut s'applique parfaitement à ce cas: la variable de retour est simplement déclarée, et le compilateur devra vérifier qu'elle n'est pas utilisée avant d'avoir été affectée, à l'instar des variables locales.

Reste le problème d'initialisation de variable-membre de C++ évoqué plus haut, qui se transpose en problème d'initialisation de champ de structure en C. Sachant que les structures de données dans Lama sont équivalentes à celles de C (ou de Pascal, etc.), propose-t-il une solution?

La réponse est oui. Mais pour exposer cette solution il me faudrait décrire un peu le système de types de Lama. Cet article est suffisamment long pour aujourd'hui, donc j'aborderais le sujet prochainement.

Aucun commentaire:

Enregistrer un commentaire