dimanche 31 mai 2009

Types

Les articles se succèdent rapidement à mesure que le développement de Lama avance...
Parfois une petite avancée se fait au prix de quelques heures de deboguage, parfois ça fonctionne du premier coup, comme par miracle. Dans les deux cas, ce serait l'occasion d'un cri guttural (à la manière du genre de musique que j'écoute) de victoire, si je n'avais pas de voisins.
Cela tombe bien, la dernière avancée concerne un aspect dont j'avais dit que je le développerais plus tard dans le message précédent: les types.

Dans Lama, il y aura quatre types primitifs: entier, réel, booléen et chaîne. Des librairies permettront d'ajouter d'autres types; ce seront essentiellement des types opaques ou abstraits comme les flux ("streams").
Le programmeur peut créer à partir des types de base ces propres types selon le mécanisme classique d'agrégation qui donne les "structures" que l'on voit, avec diverses syntaxes, dans la plupart des langages.
Ces structures sont d'un point de vue mathématique des produits cartésiens d'ensembles. Par exemple, un mathématicien dirait d'une structure "Point" constituée de deux champs pour les coordonnées X et Y, qu'elle est une paire de réels, autrement dit l'ensemble résultant du produit cartésien de R par R (R étant l'ensemble des réels).
Mais en programmation, les types sont un peu plus que cela: Le même produit cartésien de R par R peut aussi bien être utilisé pour représenter un point, un vecteur ou un complexe. Et il n'est pas question d'ajouter un vecteur à un point par exemple. Le type en programmation porte une information supplémentaire ayant trait à la nature de l'entité. En mathématiques, on n'ajoute pas non plus un vecteur à un point car l'opération + : Point, Vecteur -> Point n'est normalement pas définie.
Pourtant, aussi bien l'ensemble des points que l'ensemble des vecteurs sont tout deux des sous-ensembles de l'ensemble des paires d'entier. L'interdiction demeure parce que Point ou Vecteur ne sont pas vraiment des ensembles; ce sont plutôt des structures algébriques, qui combinent ensembles et opérations sur les éléments de ces ensembles.
Les types sont en fait des structures algébriques: ils sont constitués d'une partie structurelle (définie par le mot-clef "struct" en C, "type" en Lama) et d'une partie "opérationnelle", qui est l'ensemble des fonctions qui s'appliquent à ces éléments. La distinction entre élément (d'ensemble) et opérateur des mathématiques rejoint la distinction informatique entre programme et donnée.

Pour clore ce chapitre, remarquons qu'assez étrangement, le potentiel anti-bogue des systèmes de types semble sous-exploités. Je me souviens d'un merveilleux exemple donné dans un tutoriel du langage Anubis, qui illustre cela. Le thème est le calcul de grandeurs électriques; la puissance électrique, qui se mesure en Watts, est le produit de la tension (en Volts) par l'intensité (en Ampères), autrement dit P=U x I. L'exemple montrait comment utiliser les types pour éviter les courts-circuits (je transcris en Lama):

type Watts Real

type Volts Real

type Amps Real

proc calculerAmperage Volts u, Watts p -> Amps i
set i : p/u
end

let tensionGen : 12 as Volts

let puissanceGen : 1 as Volts

let amperageGen : calculerAmperage(tensionGen, puissanceGen
)

Le fin mot de l'histoire est que, en utilisant des types spécifiques pour les volts et les ampères plutôt que de simple réels comme on le fait souvent, une interversion entre les deux paramètres de calculAmperage ne fera pas d'étincelles lorsque l'on mettra le tout sous tension. Cela a l'air d'être trop simpliste pour être efficace? Et pourtant (anglais - crash d'une sonde martienne due à une confusion entre système métrique et unités anglo-saxones)

En Lama, le mot-clef "type" définit à la fois une structure et un nom de type - une combinaison du typedef et du struct de C:

type Complex [ Real: realOf , Real: imaOf ]

Ceci définit le type "Complex" qui est structurellement une paire de nombres réels. Dans l'exemple précédent sur les unités de mesure, la syntaxe était différente parce que les types définis étaient de simples "variantes" d'un type déjà existant. Il est possible que j'utilise plutôt un mot-clef spécifique pour cela (par exemple "subtype") par la suite, car dans ce cas le type défini, du fait de sa "parenté" directe avec un autre type, pourrait bénéficier de quelques privilèges.
Les symboles "realOf" et "imaOf" sont deux identificateurs de champs. Ce sont des quasi-fonctions définies automatiquement. "quasi fonctions" parce qu'on les utilise syntaxiquement comme des fonctions, mais qu'il est permis de leur affecter une valeur. "realOf" et "imaOf" sont définies comme prenant en paramètre un Complex et retournent un Real. Voyons leur usage concrètement sur la procédure d'addition de deux complexes:

proc + infix Complex a, Complex b -> Complex z
set z : [ realOf a + realOf b , imaOf a + imaOf b ] as Complex
end

Les crochets utilisés dans la définition des types sont également utilisés pour sa contrepartie, la formation des t-uples (ici une simple paire).
La partie "as Complex" est une syntaxe qui permet de doter une expression d'un type particulier. Ici, elle est utilisée pour donner le type "Complex" à notre paire d'entiers. Le compilateur n'acceptera de faire une telle opération que si le type original de l'expression à la même structure que le type que l'on veut lui donner.

Ce mot-clef "as" permet, vous l'aurez compris, de "transtyper" une expression, autrement dit de court-circuiter (en partie) la vérification des types. Sans cette syntaxe, il serait impossible de créer une entité de type Complex (plus généralement, il n'aurait pas été possible de créer des variables dotées d'un type défini par l'utilisateur). Il existe certainement des alternatives, mais toute brutale qu'elle soit, cette méthode est sans doute la plus élégante.
Notamment parce qu'elle permet de résoudre le problème de "l'initialisation oubliée" dont je parlais dans un précédent message. Le fait que le compilateur vérifie la compatibilité structurelle, contrairement au transtypage de C, permet d'intercepter ce genre d'erreur: si on ajoute un champ à la définition du type, toutes les opérations de transtypage deviennent incorrectes. Le programme ne se compilera pas tant qu'on aura pas ajouté ce champ dans tous les transtypages. Ce n'est certes pas parfait car la permutation de deux champs identiques passera entre les mailles du filet, mais c'est un progrès notable.


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.

dimanche 24 mai 2009

Premiers pas

Lama, le langage de programmation sur lequel je travaille actuellement, vient d'exécuter ces premières instructions.

En soit, ces premiers mots n'ont rien d'extraordinaire, puisqu'il a additionné 1+1 et a affiché le résultat. Le source correspondant est:

bind + infix high Int x, Int y -> Int z  : add

bind print Int x -> () : print

print 1+1

Les deux premières lignes sont de "bas niveau", puisqu'elles ont pour but de déclarer les symboles "+" et "print" comme des procédures implémentées directement par des fonctions C ("add" et "print").

Les mot-clefs "infix" et "high" qui suivent "+" sur la première ligne permettent respectivement d'utiliser la notation infixe, et de donner une précédence supérieure pour ce symbole. La déclaration des procédures est assez proche de ce modèle, et en particulier ces deux mot-clefs seront utilisables.

Conférer une précédence haute à "+" n'est en réalité pas correct, car c'est la multiplication qui devrait l'avoir pour reproduire la précédence naturelle entre l'addition et la multiplication. Mais il me permet de montrer quelque chose sur la dernière ligne: les parenthèses autour des arguments ne sont pas obligatoires. Sans la précédence supérieure pour '+' toutefois, il faudrait quand même mettre des parenthèses (car sinon le compilateur comprend (print 1)+1, ce qui n'a pas de sens).

Un motif de satisfaction supplémentaire est qu'en fait, une grande partie du système est déjà là: l'analyse syntaxique "comprend" les définitions de procédures, de variables, de types et les expressions; la vérification de types est en grande partie opérationnelle; la mécanique pour compiler cela est présente, et la machine virtuelle qui doit exécuter ce qui a été compilé est en place. Mais ils ne sont pas pour autant terminés, et encore moins débogués.

D'où vient le nom du langage? Je n'ai pas d'affection particulière pour les lamas. C'est au départ un acronyme, "Library Assembly and MAsh-up" (assemblage et mixage de bibliothèques), que j'ai trouvé par la suite pas si terrible. Je l'ai gardé à cause de sa proximité avec le mot anglais lame ( "boîteux" - lame descent d'ailleur du vieil-anglais lama) m'amuse. Je lui est aussi trouvé une nouvelle signification: "Lame Attempt to Making an Acronym" (tentative boîteuse de faire un acronyme), qui m'amuse tout autant.

Pour résumer, j'ai choisi un nom délibéremment vide de sens voire un peu comique, par opposition à un nom ronflant, bouffi de sens ou qui cherche à impressionner. Ce choix est, finalement, plutôt cohérent avec la philosophie générale du langage.

PS: J'avais en tête l'image d'un animal assez peu charismatique, mais quelques recherches sur le sujet m'ont détrompé.

jeudi 14 mai 2009

Lua

Le bricoleur pur et dur a toujours son couteau suisse dans la poche; le programmeur pur et dur a toujours son langage favori sous la main.

Ce genre de langage-couteau-suisse se distingue par certaines caractéristiques:
* la gestion de la mémoire y est automatique,
* c'est un langage interprété,
* de nombreuses librairies sont disponibles,
* sa syntaxe est simple,

C'est un langage avant tout pragmatique: les créateurs ne recherchent pas la pureté absolue (pur objet ou pur fonctionnel), ou la performance absolue, mais plutôt une ergonomie optimale. Oui, l'ergonomie ne s'applique pas seulement aux postes de travail, aux appareils electro-ménagers ou aux gadgets électroniques.

Pour ma part, je recherche une qualité supplémentaire: que le langage soit tout-terrain. Il doit fonctionner aussi bien sous Linux que Windows, mais aussi sur les systèmes embarqués tels que ceux sur lesquels je travaille (à base de processeurs 32bits tournant à100-200MHz, disposant de seulement quelques Mo de RAM et faisant tourner un système Linux léger).

Cela impose indirectement certaines contraintes de performance. Cela met en fait hors course la plupart des candidats, à commencer par Python, Ruby et TCL. En fait, il ne reste quasiment que Lua, qui est le seul langage interprété à concurrencer Forth en matière de performances. Il est certes nettement plus gros que lui, mais il n'y a guère qu'un autre système Forth pour concurrencer Forth sur ce point. En contrepartie, Lua offre la gestion de mémoire automatique et une syntaxe plus naturelle.

En fait, Lua est assez proche du langage "orienté utilisateur" que je cherche à créer. Pour commencer, il a toute ma reconnaissance pour ne pas être encore un de ces langages orientés objet. Il est difficile d'en trouver un qui ne l'est pas ces temps-ci, comme si cela était une condition sine qua non. Le seul pas en direction de l'orienté-objet est une facilité de syntaxe dont l'utilité se justifie d'elle-même: si elle s'avère pratique pour le programme que vous écrivez, c'est que le style orienté-objet est certainement le plus adapté. Si vous ne savez que penser en termes d'orienté-objet, et bien, vous devriez envisager de vous faire trépaner, mais vous pouvez construire un système de classes etc, ou adopter l'un de ceux créé par d'autres.

Ensuite, l'attention qui a été prêtée à la facilité d'intégration des librairies C avec le language. C'est un point important parce que ce type d'intégration est parfois difficile car s'y cumulent les défauts de la librairie et du langage-hôte. Lua simplifie les choses dans ce domaine en présentant une interface compréhensible et compacte. Cela incite à faire ces intégrations, et plus de librairies intégrées signifie plus de fonctionnalités disponibles pour le langage. C'est l'une des clefs du succès de Python.
A cela il faut ajouter que Lua donne le choix entre une intégration par librairie dynamique ou statique de manière transparente. L'un permet que l'interpréteur garde une taille raisonnable, et l'autre peut être plus pratique, plus compact, mais aussi permet le fonctionnement sur des plateformes qui ne savent pas gérer les librairies dynamiques.

Enfin, Lua est doté d'un ensemble équilibré et efficace de caractéristiques: clôtures, syntaxe avec juste ce qu'il faut de facilités, coroutines, et les metatables.


Lorsque j'ai commencé à avoir une idée claire du langage que je voulais, j'ai fait quelques recherches: l'expérience laisse à penser que tout ce que vous pouvez imaginer en matière de logiciel existe quelque part sur la toile. Pour le coup, il ne s'agissait pas d'écrire un interpréteur simple comme Forth, car ce que je veux implique de l'analyse de syntaxe, de la gestion automatique de mémoire, et de la vérification de type. Mes recherches ont été infructueuses; Lua, que je connaissais déjà est ce qui s'en rapproche le plus.
Je suis en train d'implémenter très lentement l'interpréteur pour mon langage; la difficulté de cette implémentation est à la auteur de mes craintes. Pourtant, je continue cette implémentation plutôt que d'adopter Lua car celui-ci a, à mes yeux, des défauts importants.

Ceux qui connaissent Lua auront remarqué qu'une des différences d'avec ce que je souhaite est la vérification de types; Lua est un langage dynamiquement typé alors que je veux un langage fortement et statiquement typé, une différence qui reste aujourd'hui le sujet de guerres de clocher virulentes. L'un et l'autre ont leurs avantages et leurs inconvénients. La raison de ma préférence pour le typage statique tient probablement au fait que je suis, le jour, programmeur pour système embarqué, et que le typage statique constitue une barrière de plus contre les bogues.
Le typage dynamique de Lua, couplé avec son principe de fonctionnement basé sur les tables, amène un inconvénient majeur: une simple faute de frappe peut amener un disfonctionnement incompréhensible. C'est l'assurance de perdre bêtement de temps à autres une heure ou deux pour une simple faute de frappe. Cela peut être à la limite acceptable quand on est prévenu de ce problème; après tout, quiconque c'est fait "avoir" deux ou trois fois par un point-virgule mal placé ou un "break" manquant en C apprend à être vigilant sur ces pièges.
Un autre problème chez Lua est la qualité des librairies. Un symptôme assez représentatif est l'usage à tort et à travers de chaines de caractères au lieu de codes numériques, notamment pour les codes d'erreur. On peut même le voir dans les fonctions intégrées du langage: la fonction type() renvoie la chaine "string", "table", etc. selon le type de l'argument passé. Outre le problème de la faute de frappe, c'est selon toute vraissemblance inefficace: si l'égalité peut être déduite efficacement en comparant les adresses des chaines (moyennant une optimisation de compilation que Lua fait certainement, sans quoi cela devient franchement stupide), si les adresses sont différentes le système doit en passer par une comparaison du contenu des chaînes (direct ou indirect - en tout cas c'est moins efficace que de comparer deux entiers).
Dans les librairies qui sont des contributions externes, j'ai vu tantôt des fonctions qui retournaient des chaines en guise d'erreur, tantôt un message d'erreur et un code d'erreur. Dans un autre registre, dans une autre librairie on peut voir un usage arbitraire des" metatables"; par arbitraire j' entend que cet usage n'est en rien justifié ou justifiable. Pour terminer, il faut noter que ce n'est que récemment que Lua c'est inspiré de Python, et offre une distribution incluant un éventail relativement convainquant de librairies (où j'ai d'ailleurs vu ces problèmes).
Globalement, le problème est que les auteurs de Lua semblent ne s'occuper que du langage et se désinteresser de la qualité des librairies. Or, cette qualité égale en importance les caractéristiques propres du langage. Il faudrait pour Lua que quelqu'un (ou un groupe) s'occupe de définir les règles de bonne conception des interfaces pour les librairies, évalue la qualité de leur implémentation et de leur documentation, choisisse entre plusieurs librairies qui offrent les mêmes fonctionnalités. On peut trouver quelque chose qui va dans ce sens sur le Wiki de Lua, mais cela reste très embryonnaire.
Pour conclure, il faut qu'une entité régisse les librairies. "Regir" a pour racine latine rex ("roi"). Sur d'autres projets Open Source, on se plaint parfois du caractère autoritaire de l'équipe de développement en charge du projet, car elle refuse tel ajout ou telle modification. Mais je pense que l'inverse, une équipe beni oui-oui qui fait ce qui suggéré sans discernement, est bien pire.