vendredi 11 septembre 2009

Tracks

Un extrait de ma playlist YouTube:

Keep of Kalessin: Ascendant, Armada, etc.

Neuraxsis: The All and Nothing

Arsis: The Face Of My Innocence

Cattle Decapitation - Regret and the Grave

Arch Enemy - Nemesis

The Berzerker - All About You

Kronos - Phaeton

The Crown - I won't follow

Slayer !!!  - Psychopathy Red, Hate Worldwide


dimanche 26 juillet 2009

Réseaux sociaux

Tiens, un lien intéressant; partageons-le.

Alors, devrais-je le partager sur Twitter ou Facebook?

Ou peut-être Delicious, StumbleUpon?

Ou encore reddit? Ou le mettre en partage dans mon Google Reader?

Ou devrais-je utiliser FriendFeeds?

Ou devrais-je attendre Google Wave?


Le cas de FriendFeeds est révélateur: il se propose de centraliser tous les autres. Sa raison d'être est de résoudre l'éparpillement que génère l'usage de tous les autres. 

Le paradoxe est que d'une certaine manière, il ne fait que s'ajouter au problème. Maintenant que le lien est partagé sur Twitter ou sur reddit, il faut aller sur Twitter, reddit et FriendFeeds lire les commentaires.

Revenons au problème de base. Internet est un monde vaste. Le principal défi qu'il pose est d'extraire l'information qui nous intéresse dans tous ces tera-bits de données.

Les moteurs de recherche sont certes une aide précieuse, mais ont leurs limites. Google a beau employer des algorithmes sophistiqués pour indexer le Web, si les résultats de recherche ne sont pas probants sur la première ou deuxième page, ils sont peu nombreux les courageux qui creusent plus profond. Car on se retrouve vite noyé dans un océan de liens.

D'où l'intérêt de se créer un réseau de connaissances. Cela permet de mutualiser la recherche et le filtrage du Web selon nos centres d'intérêt.

On pourrait imaginer que chacun ait une page personnelle, dans laquelle il pourrait communiquer ces réflexions et observations du moment, ou mettre des liens qu'il juge intéressant. On pourrait imaginer pouvoir s'abonner à cette page, pour recevoir un message quand un nouvel article y apparaît. On pourrait imaginer aussi que les gens puissent commenter ces articles. Si on a plusieurs centres d'intérêts, on pourrait ranger les articles dans différentes catégories, ou y apposer différentes étiquettes. Et bien sûr, chacun pourrait indiquer quelles pages il suit, afin de constituer un réseau de connaissances. En cerise sur le gâteau, on pourrait imaginer un index général de ces pages, qui les répertorierait selon les catégories et étiquettes qu'on y trouve.

Ce serait une idée géniale, ne trouvez-vous pas?




mardi 30 juin 2009

Hello, world.

Cela fait quelques temps déjà que Lama a exécuté sa première addition et son premier programme "hello, world". A l'heure actuelle, il exécute en réalité un fichier contenant une centaine de lignes de test.


J'ai successivement franchi le cap de la compilation et de l'exécution d'instructions simples, puis de la création de procédures, puis de la lecture de fichiers sources, puis de la création de types et de structures, puis de la création de blocs conditionnels et de boucles, puis de la création de clôtures, puis de la création d'espaces de noms, puis du chargement d'extensions sous forme de librairies partagées.

Dernièrement, j'ai ajouté la définition de types-union (au sens des langages fonctionnels), qui permettent notamment d'intégrer la mécanique de gestion d'erreur que je souhaitais et dont j'ai parlé il y a quelques mois.

Pour résumer, les types-union permettent d'exprimer le fait qu'une valeur (une variable, un champ de structure, un paramètre ou un résultat) appartient à un type parmi un ensemble de types. Cet ensemble est connu à la compilation, le type effectif de la variable dépend de ce qui se passe à l'exécution. Le compilateur peut vérifier à la compilation que le programme traitera à peu près correctement tous les cas de figure qui se présenteront à l'exécution.

On utilise les types-union pour la gestion d'erreur en créant un type particulier pour le cas d'erreur, par exemple DivisionParZero, et en l'associant dans une union avec le type "normal". Par exemple:

proc inverse Real x -> DivisionParZero/Real y

  if x=0

    set y DivisionParZero

  else

    set y 1/x

    end

  end

Le type DivisionParZero est un cas particulier de type qu'on appelle "type unité" parce qu'il ne représente qu'une seule valeur. Avec Lama, on le définit comme un t-uple vide:

type DivisionParZero []

Dans ce cas particulier, on se moque de la valeur effective associée au type. Elle est simplement représentée par le nom du type lui-même.

Quand on utilise la procédure "inverse" définie ci-dessus, on obtient une valeur qui peut appartenir à l'un ou l'autre des deux types. Pour pouvoir l'utiliser, il faut d'abord vérifier à quel type elle appartient; on utilise la struture "on" pour ce faire:

var r: inverse z {avec z: un nombre quelconque}

on DivisionParZero r

  print "Erreur: division par zero"

  end

on Real r

  print "le résultat est:" ~ (toString r)

  end

Le compilateur vérifiera que tous les types de l'union sont passés en revue par le programme, comme pour un switch...case du C.

Les types-union résolvent une seconde difficulté: dans un langage à typage statique fort comme Lama, les structures de données classiques comme les listes ou les tableaux ne peuvent contenir qu'un seul type de données. Les unions permettent en quelque sorte de cacher plusieurs types dernière un seul type. C'est certes moins flexible qu'avec un langage dynamique, mais c'est aussi plus sûr et plus rigoureux.


Sur le plan de la syntaxe, il ne me reste plus qu'à implémenter les macros. Je prévois un système inspiré de Forth, où les macros sont des procédures particulières exécutées lors de la compilation. Celles-ci peuvent faire appel à certaines parties du compilateur pour remplir leur fonction. Un exemple d'utilisation très élémentaire serait que la macro "bricole" une chaine contenant l'expression voulue et appelle une procédure du genre "eval", ce qui équivaut déjà aux macros du langage C.

Cependant, sur le plan du fonctionnement, les choses sont moins avancées: il manque un gros morceau, la gestion automatique de mémoire (dont on pourra se passer pour une première version de "démo"); l'interpréteur a tendance à se planter facilement sur les erreurs de syntaxe. Enfin, il manque quelques vérifications stratégiques comme sur la structure "on" ci-dessus, ou sur le fait que la variable de retour reçoit bien une valeur dans tous les cas de figure.

PS: les liens pointent parfois vers des pages Wikipédia anglaises parce que la version française n'était pas suffisamment complète dans le cadre de mon propos.


lundi 22 juin 2009

HADOPI et compagnie

Je suis avec interêt les discussions autour de la loi Hadopi. J'avoue lire avec délectation les commentaires sur Clubic, ZDnet ou Tom's Guide, en particulier quand les participants se moquent à juste titre très souvent de l'ignorance technique des personnes en charge du dossier. Il y aussi des commentaires moins drôles, en particulier ceux qui justifient le piratage et ceux qui confondent politique et justice (ou justesse, pour employer un moins grand mot). La confirmation de cette direction par Sarkozy Lundi dernier, "j'irai jusqu'au bout", a évidemment soulevé une vague de commentaires principalement anti-Sarkosistes.

Entendons-nous bien: je n'ai aucune sympathie ou antipathie envers Sarkozy. Mais force est de constater qu'il se fonde sur des principes élémentaires.

Laisser faire un piratage massif est forcément nuisible aux créateurs, parce que cela les ampute mécaniquement d'une partie de leurs revenus.

Le fait que lesdits créateurs soient déjà riches ou que les diffuseurs s'accrochent à une affaire "juteuse" ne change rien au fait que sur le fond, la copie illégale est un vol. Il serait incensé de justifier auprès d'un juge que votre larcin n'en est pas un puisque vous avez volé dans une boutique Dior. Et gageons que son jugement sera identique que vous ayez volé un foulard dans une boutique Dior ou dans le supermarché du coin.

Le pire est que les pirates, quand bien même ils invoquent cette excuse lamentable, ne font sans doute pas preuve d'un tel discernement dans les faits. A force de se convaincre du bien-fondé de la justesse de leurs excuses, certains finissent par mettre tout les créateurs dans le même panier et copient sans remord (et parfois sans le savoir) l'artiste auto-produit qui a joué son va-tout dans l'aventure.

La nature immatérielle de certaines catégories d'oeuvres (musique, livre, film, logiciel, ...) rend insoluble le problème de l'interdiction ou de la limitation de la copie: ces oeuvres doivent nécessairement être produites pour vraiment exister, et donc il existera toujours un moyen de les re-produire. La culture elle-même est basée sur cette transmission. Tout dispositif de vérrouillage ou de limitation est voué à l'echec. 

Le fait que les règles usuelles de la société ne s'appliquent pas sur Internet est un vrai problème, dont la cause est l'anonymat qui conduit à l'irresponsabilité. Les internautes en subissent eux-mêmes tous les jours les désagréments: trolls, spam, mauvaise conduite sur les forums et les "chats". Cela peut prendre une tournure nettement plus malsaine quand des adultes s'incrustent sur des sites fréquentés par des enfants ou des adolescents.

Cela dit, vrai, la loi HADOPI est mal ficelée et probablement vouée à l'échec. Le fait que le conseil constitutionnel l'ait censurée pour des raisons évidentes pour tout un chacun est bien le signe qu'elle est partie sur de mauvaises bases juridiques. A cela s'ajoute la loi contraire qui a été votée au niveau européen. Comme si cela ne suffisait pas, elle est aussi fondée sur de mauvaises bases techniques: elle vous rend responsable de la protection de votre ordinateur, et est censée vous donner les moyens de le faire par le biais d'une certification de logiciels de sécurité. Or, il se trouve que sur un plan théorique, aucun logiciel de sécurité (anti-virus, pare-feu) ne peut être fiable à 100%. Et c'est encore plus vrai, comme on le sait, du simple fait des défaillances techniques (bogues, failles de sécurités).

Les gens se sont beaucoup moqués et scandalisés du principe de l'identification par adresse IP. Cependant, cela me paraît être la voie à suivre, car comme je l'ai dit l'anonymat est le coeur du problème.

Je reprendrais l'analogie un peu vieillote des autoroutes de l'information; après tout, la route est un autre domaine où le français aime à pratiquer le délit occasionnel ou quotidien, pour lequel il a également toujours une très bonne excuse.

Il est admis et reconnu par tous que Internet est une place virtuelle, mais aussi publique, à l'instar d'une route. Ce qu'on y écrit, peut être lu la plupart du temps par n'importe qui. Avec une bonne maîtrise de Google, vous pouvez amasser pas mal d'informations sur une personne qui ne fait pas trop attention. En fait, c'est comme prendre quelqu'un en filature, sauf que la personne n'a aucune chance de vous repérer.

Le piratage est comme un excès de vitesse: faire une pointe à 110 sur une route de campagne dégagée est un peu comme (le risque vital direct mis à part) envoyer une copie d'un CD à un ami pour lui faire découvrir un artiste. C'est une petite entorse si finalement, celui-ci n'aime pas et l'efface, ou au contraire aime et achète le dernier album, ou encore n'en garde qu'un morceau. Faire commerce de DVD pirates par lots de cent s'apparente (toutes proprtions gardées) à doubler sans visibilité en coupant une ligne blanche.

La sécurité routière ne pourrait pas fonctionner sans gendarmes, radars automatiques et surtout les plaques d'immatriculation, qui permettent l'identification, et qui sont d'ailleurs elles-mêmes protégées par la loi.

L'usage d'Internet devrait donc être parfaitement identifiable si besoin par "la police du net". Cela n'est pas si choquant, tant que les limites posées dans la vie réelle s'appliquent également à la vie virtuelle (protection de la correspondance privée, vidéo-surveillance réglementée, écoutes soumises à l'approbation d'un magistrat, etc.). Cela pourrait même être bénéfique aux internautes, si cela sert aussi à endiguer le spam, à contrer les escrocs, à refroidir les hackers du dimanche, à repérer les pédophiles passant à l'acte, etc. Utiliser l'adresse IP est une voie possible, à condition de la protéger de manière à ce qu'elle soit fiable.

Quelle que soit la méthode, une telle identification informatique pose le problème sérieux et de plus en plus aigu de la possiblité d'abus de toutes sortes dans des proportions jamais vues. Il est probable que nous vivons la période où se définit la vie sociale électronique de demain; cela pose des problèmes technico-juridiques inédits et ardus. La loi HADOPI est trop maladroite et trop simpliste pour être une bonne solution.


mardi 2 juin 2009

Emacs contre Vim

Je suis tombé sur ce commentaire sur reddit:

How do you manage files? Using a file manager of course. Vi is a great editor, it's not a file manager. The Unix way is do one thing and do it well, programs that try to do everything suck.

"Comment gerez-vous les fichiers? En utilisant un gestionnaire de fichier, évidemment. Vi est un bon éditeur, ce n'est pas un gestionnaire de fichiers. La philosophie Unix est de ne faire qu'une chose mais la faire bien, les programmes qui essaient de tout faire sont 'nazes'."

Ce commentaire a retenu mon attention car c'est l'un des points de départ de Lama.

La guerre de clochers entre Vim et Emacs est représentative d'un type de choix qui se présente souvent dans l'univers du logiciel: utiliser de petits logiciels spécialisés et performants dans leur domaine, ou un gros logiciel tout-en-un.

Contrairement à ce que pense ce commentateur, il n'y a pas un bon choix et un mauvais choix, car les avantages d'une alternative sont les inconvénients de l'autre.

Les logiciels tout-en-un comme Emacs, Opera ou les suites logicielles ont l'avantage de l'intégration: les différentes parties du logiciel dédiées à certaines tâches s'échangent général facilement les données, et l'interface utilisateur est cohérente. Le revers de la médaille est que ce sont en général des logiciels énormes, parfois limités dans les domaines qu'ils couvrent, et relativement fermés sur eux-mêmes. Si une des parties du logiciel est boguée ou a des capacités trop limitées par rapport à vos besoins, vous perdez beaucoup des avantages du logiciel intégré car vous devez contourner ses handicaps. Bien souvent, il faut attendre longtemps que ces points bloquants soit résolus par une mise à jour, car la taille du logiciel confère une grande inertie au projet.

Les logiciels spécialisés sont plus petits et plus pointus. Ils sont en général conçus pour communiquer avec d'autres logiciels, donc on peut choisir quel logiciel on utilise pour chaque tâche ou domaine. Le revers de la médaille est que la communication n'est pas toujours efficace. Il faut parfois faire soi-même les adaptations nécessaires pour faire communiquer deux logiciels, et le résultat n'est pas toujours satisfaisant. L'interface utilisateur est aussi fortement hétérogène, car chaque logiciel définit son interface utilisateur, malgré les personnalisations possibles. Enfin, bien que chaque logiciel spécialisé soit individuellement beaucoup plus petit que le logiciel tout-en-un, la taille cumulée de tous les logiciels spécialisés peut excéder celle du logiciel tout-en-un.

Comment réconcilier intégration et liberté de choix?
Une solution presque satisfaisante serait de définir une interface de communication entre logiciels vraiment standard. C'est le créneau qu'occupe XML sur le web en ce moment, et qui a été transposé pour certaines applications de bureau. Mais XML est à mon avis le pire standard de communication possible. Certains ont je crois déjà transposé cette remarque désobligeante qui portait à l'origine sur les expressions régulières: "ils ont choisi les expressions régulières pour tenter de résoudre leur problème. Maintenant ils ont deux problèmes". La communication par un format texte comme celui de XML est, pour commencer, complètement inefficace. Donc inadaptée à des gros volumes de données ou à des traitements ultra-rapides. L'idée absurde d'utiliser les syntaxe de balise de HTML n'arrange rien.

Une autre solution est de programmer ces logiciels dans un langage unique, et de faire communiquer ces logiciels par l'intermédiaire d'une programmation dans ce langage, qui servirait également de langage de script et de configuration. C'est je crois l'idée de TCL, qui a connu un certain succès à une époque. Le principe est bon, mais TCL n'est plus en vogue sans doute pour deux raisons: sa syntaxe et son mode de fonctionnement un peu particuliers, et son inefficacité.
Python est sans doute le TCL d'aujourd'hui. C'est un langage que je pourrais utiliser (ou plutôt Lua, qui est moins populaire, mais plus efficace et plus simple - j'en ai déjà parlé), mais je doute que l'utilisateur type de Lama tel que je me le représente l'apprécierait (c'est un langage de programmeur)

Lama est une tentative de reprendre cette idée en l'améliorant: être accessible à ceux qui ne programment pas toute la journée, être efficace et être ouvert.

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.

samedi 7 mars 2009

Erreurs

Programmer ne consiste pas seulement a faire réaliser un tâche particulière à un ordinateur: cela consiste également à prévoir et traiter les circonstances particulières qui peuvent se produire lors de cette tâche, autrement dit gérer les cas d'erreur.
La gestion d'erreur est vécue comme un corvée par la plupart des programmeurs. Une première raison est que cela peut représenter beaucoup travail sans qu'on ait l'impression que la valeur ajoutée au programme augmente: une bonne part de ce travail consiste en effet à gérer des cas dont on a l'impression qu'ils ne se produiront jamais. A tel point que cela pose parfois de sérieuses difficultés: comprendre pourquoi le système renvoie une erreur obscure et décider de la réponse à y donner peut nécessiter beaucoup de recherche et de réflexion. De plus, la gestion d'erreur "défigure" en quelque sorte un programme: si on compare un programme avec une gestion d'erreur minimale et ce même programme avec une gestion plus rigoureuse, on constate qu'il a facilement doublé de volume, et que le programme original est "noyé" dans le code dédié à la gestion des erreurs; la lisibilité s'en trouve grandement réduite.

Toutefois, les programmeurs savent aussi que c'est un mal nécessaire. Un programme qui ne gère pas correctement les erreurs "se plante", avec en prime des pertes de données pour l'utilisateur.
L'impression que tout ce code supplémentaire ne sert pas à grand chose est fausse, parce qu'on pense souvent qu'une erreur qui a peu de chances de se produire ne se produira jamais. Un joueur de Loto n'a qu'une chance très faible de gagner le gros lot, et pourtant il y a régulièrement des gagnants, car il y a beaucoup de joueurs. De même, on peut penser a priori que les chances sont quasi-nulles pour qu'un certain scénario d'erreur se produise; mais si le programme est utilisé quotidiennement par des milliers d'utilisateurs, vous aurez des centaines de gens qui viendront se plaindre sur votre forum.
La gestion d'erreur fait partie de l'intelligence du logiciel, car il s'agit bien de doter le programme d'un comportement sensé quand il rencontre des circonstances inhabituelles; c'est aussi en partie ce qui rend la tâche difficile au programmeur. Un programme qui gère mal les erreurs a l'air de se comporter de manière stupide, et laisse bien souvent l'utilisateur seul avec son problème. Un programme qui gère bien les erreurs donnera, s'il ne peut résoudre le problème lui-même, un diagnostic précis et des éléments de résolution.
Les programmes qui doivent se connecter à un serveur affichent typiquement un message laconique "Impossible de se connecter au serveur" et proposent à l'utilisateur d'essayer à nouveau de se connecter. C'est un cas de gestion médiocre d'erreur. Les programmes avec une bonne gestion d'erreur vérifient si la machine visée peut être contactée, si le client est bien connecté sur le réseau, et si les paramètres réseau sont corrects. Ses diagnostics permettent à l'utilisateur de résoudre le problème plus rapidement; on lui épargne au moins la tâche de faire lui-même ces diagnostics manuellement, et qui peuvent du reste être faux et aiguiller sur une fausse piste (confusion entre adresse IP du client et du serveur, par exemple).

La gestion d'erreur est donc pénible et difficile, mais elle est d'une importance majeure. Une gestion d'erreur éronnée est un comble que les programmeurs ne savent pas toujours éviter. Elle mérite donc qu'on y réfléchisse, notamment afin de définir une stratégie saine. Avec en perspective des possibilités de soutien de cette stratégie par le langage de programmation.

Deux techniques bien connues existent:

  • La technique des codes d'erreur,
  • La technique des exceptions.
La technique des codes d'erreur est la plus basique et la plus ancienne. Quand une erreur se produit, la fonction retourne une valeur particulière et le programme appelant doit effectuer des actions supplémentaires pour connaître la cause précise de l'erreur.
Ce mode de gestion a plusieurs inconvénients: elle nécessite de dédier une des valeurs que peut retourner la fonction pour le signalement de l'erreur, et cette valeur peut varier d'une fonction à une autre suivant les contraintes (parfois c'est 0, d'autre fois c'est -1, parfois c'est une constante symbolique comme EOF, etc.), ce qui est source d'erreur. Un autre inconvénient est que le code d'erreur étant en quelque sorte encodé dans la valeur de retour, la distinction entre la logique de traitement d'erreur et la logique de l'application s'en trouve brouillée. Les langages qui permettent aux fonctions de retourner plusieurs valeurs améliorent un peu les choses, mais cela reste globalement insatisfaisant. Enfin, un reproche qui est souvent fait à cette méthode est qu'il faut faire "redescendre" les codes d'erreur de l'appelé à l'appelant, puis à son appelant, etc.

La technique des exceptions évite ces inconvénients. Avec les exceptions, une fonction qui échoue peut lancer une sorte de signal d'erreur qui peut être intercepté et traité par l'appelant, mais peut aussi être ignoré, auquel cas le signal sera traité par la dernière fonction parente qui aura fait en sorte d'intercepter ce signal. Ce qui permet à une fonction de ne traiter que les erreurs qui "l'intéresse". Cela donne parfois un code plus léger par rapport à la première méthode.
Cependant, il n'y a pas de magie, le code de gestion d'erreur que l'on trouvait dans la première méthode doit nécessairement se retrouver quelque part avec cette méthode. On le retrouve généralement au niveau de la fonction parente qui intercepte les exceptions éventuellement émises par les fonctions qu'elle appelle elle-même; ce code est "délocalisé" en quelque sorte, pour le regrouper avec d'autres gestions d'erreur. Cette délocalisation est un point qui peut être reproché à la technique des exceptions: la gestion des erreurs est déléguée d'une fonction à une autre, ce qui peut constituer une entorse au principe d'encapsulation: une fonction d'un niveau N peut se retrouver à gérer des exceptions du niveau N-2 si le niveau N-1 "oublie" de les intercepter. Cela constitue à mon sens à diluer la responsabilité de la gestion d'erreur. Ce qui se confirme dans les faits, car les concepteurs de Java ont pris des décisions qui ont provoqué une petite polémique, dans le but d'éradiquer ou du moins de limiter les cas de programmes "terminés" sans autre forme de procès par une exception qui est "remontée" jusqu'à la fonction principale parce qu'aucune fonction de la chaine d'appel n'a daigné l'intercepter et la traiter. J'exagère un peu ici: les programmes ne se plantaient pas vraiment: ils consignaient ladite exception dans leur journal d'évènements, et informaient l'utilisateur qu'ils étaient au regret de mettre fin à leur exécution...

La décision polémique que j'ai évoqué paraît pourtant être une bonne idée : que les exceptions émises par une fonction soit déclarées, ce qui permet au compilateur d'offrir un service supplémentaire de vérification. Il est parfaitement justifié d'imposer que les caractéristiques de comportement en erreur fassent partie de l'interface de la fonction au même titre que le type et le nombre de ces arguments. On peut même se laisser aller à dire que c'est le symétrique "en sortie" (avec la valeur éventuellement retournée) des paramètres "en entrée".

Cela impose que pour chaque appel de fonction, il faut intercepter les exceptions. Les concepteurs de Java ont rendu cela obligatoire en générant une erreur de compilation si cela n'est pas fait. Le problème est qu'au final, on en revient de facto quasiment à la même forme de code qu'avec la première technique, pour laquelle un appel de fonction est souvent suivi d'un switch... case sur le code d'erreur retourné. Autrement dit, les concepteurs de Java ont pour ainsi dire adopté la position des détracteurs des exceptions (nota - lien vers un wiki, le contenu peut se trouver modifié), et ont décidé de renoncer à l'un de ces avantages majeurs qui était exactement contraire à un principe (pourtant) de base: le traitement local et au plus proche de la source de l'erreur.
Valider une donnée que l'on reçoit du monde extérieur comme première et indispensable étape du traitement de cette donnée par l'application est une méthode qui est généralement suivie par les programmeurs. Sans quoi, il faut partout dans l'application vérifier que la donnée est valide, et courir le risque d'oublier cette vérification sur un cas obscur ou simplement lors d'une modification anodine. On vérifie que la donnée répond à un certain "format", et la fonction chargée de la vérification retourne une structure ou un objet correspond à la donnée. Dans les langages à typage statique, la donnée acquiert du même un coup un type. Ces informations de type permettent d'éliminer les tests "internes" redondants.
Ne pas traiter une erreur rapidement et au contraire déléguer sont traitement à "quelqu'un" d'autre est très similaire à ne pas vérifier les données entrantes: on multiple les gestionnaires pour être sûr d'intercepter toutes les erreurs, mais un jour on ne voit pas un cas obscur ou on fait un faux-pas sur une modification qui parait triviale, et des exceptions non-interceptées commencent à terminer le programme de manière inattendue.

Le mécanisme des exceptions est une technique populaire, et on peut se demander pourquoi Java est le seul langage à revenir ainsi sur ces exceptions. Les programmeurs Java sont-ils simplement médiocres? Cela me fait beaucoup penser au cas de la gestion manuelle de la mémoire. S'il est effectivement possible de gérer manuellement et correctement la mémoire à force de rigueur et de choix judicieux, en revanche dans certains cas la difficulté de cette gestion peut excéder nos capacités. On commet une erreur bête ou subtile, et la fuite mémoire tant redoutée apparaît. La mise sur pied d'une bonne gestion mémoire peut donc consommer énormément d'énergie, à la fois à la conception et à l'entretien.
Les programmeurs ne sont bien sûr pas des gens parfaits. Pour peu qu'un langage devienne populaire et largement utilisé comme Java, il attire des programmeurs de tous horizons et de tout niveau. Il ne faut pas s'attendre à ce que le programmeur moyen soit un programmeur d'élite. Beaucoup de langages prennent bien cela en considération, mais ce faisant ils attirent un éventail encore plus large de programmeurs. C'est une curieuse variante de la loi de Hofstadter: Le programmeur moyen est toujours un peu plus médiocre que ce que l'on pense, même en prenant en compte cette loi.
Il ne faudrait pas en conclure qu'il faut concevoir les langages (ou les logiciels, d'un manière générale) comme si l'utilisateur était complètement débile et incompétent, mais bien qu'il faut prévoir des dispositions particulières autour du langage (tutoriels, documentation, formation, ...) pour que le programmeur (ou l'utilisateur) moyen ait les compétences attendues. On peut donc concevoir un logiciel (ou un langage de programmation) avec un certain utilisateur-type en tête, mais il faut être conscient du décalage potentiel entre celui-ci et l'utilisateur réel, et que ce décalage se concrétise par un effort supplémentaire pour l'utilisateur et le concepteur en termes de formation.
On pourrait aussi arguer que la gestion d'exception dans Java est faible comme le sous-entend à peine l'article Wikipedia français sur le sujet. En particulier, que le "vrai" système de gestion des situations d'erreur, qui généralise les exceptions, est celui-ci de Lisp est un argument fréquent. Pour être honnête, je ne suis pas sûr d'avoir tout compris de son fonctionnement, et j'ai quelques méfiances vis-à-vis de systèmes qui résistent à la compréhension. La cause de mon incompréhension est que ce système plus complexe est encore pire pour ce qui est de suivre le déroulement du programme dans les cas d'erreur.

Les exceptions coupaient également le noeud gordien de "l'anti-motif de la flèche": dans la programmation structuré stricte ("stricturée"), la gestion d'erreur conduit à des imbrications de tests qui peuvent être tellement difficiles à suivre qu'on considère souvent que c'est une exception à la règle de la programmation stricturée. Par exemple, une fonction de copie de fichier en programmation stricturée donne ceci (pseudo-code; désolé, blogger s'obstine à supprimer mes indentations):
CopierFichier(String Src, String Dst)
HSrc=openFile(Src)
if(HSrc!=0)
{
HDst=createFile(Dst)
if(HDst!=0)
{
CopyContent(Src,Dst)
closeFile(HDst)
Error=NO_ERROR
}
else
{
closeFile(HSrc)
Error=ERROR_DST
}
}
else
{
Error=ERROR_SRC
}
return Error

Pour cette petite procédure, vérifier simplement et rapidement que le code est correct - et c'est rappelons-le ce pourquoi on utilise la programmation stricturée et non des GOTO - n'est pas tout à fait évident. Ceux qui ne sont pas d'accord ont tort d'ils n'ont pas vu que la procédure ne refermait pas le fichier source ;-)
Les exceptions permettent, d'une manière discutable il est vrai, d'éviter ces imbrications car les fonctions qui n'interceptent pas les exceptions des fonctions qu'elles appellent se voient terminées. En s'y prennant bien, on peut n'avoir localement qu'un seul chemin d'execution, tandis que dans notre exemple il y en a de multiples suivant les cas de figure, ce qui rend la vérification de notre exemple un peu difficile.
On peut avoir recours à diverses astuces pour éviter ces imbrication; mon exemple à cet égard est un peu forcé. Mais il n'existe pas dans la plupart des langages de mécanisme générique pour "casser" l'execution d'une procédure et passer à son épilogue (la partie un peu avant la fin faisant le nettoyage avant de quitter la fonction). Les seuls moyens à disposition sont de faire un "return" au beau milieu de la procédure ou d'avoir recours à un "goto".

L'examen de ces deux méthodes permet de tirer quelques conclusions.
D'abord que quelque soit la méthode utilisée, il n'y a pas de miracles: il faudra toujours à un moment ou un autre examiner l'erreur et y apporter la réponse ad hoc, ce qui implique la mise en place de structures équivalentes à un switch..case.
Ensuite que faire abstraction du principe qu'il faut vérifier et traiter une erreur le plus tôt possible est dangereux. Ignorer même temporairement une erreur la possibilité une opportunité de création de bogue, menace qui pèsera sur la maintenance ou l'évolution du programme.

La méthode de gestion d'erreur à privilégier est donc pour moi celle des codes d'erreurs. Un langage de programmation peut apporter son assistance en y apportant quelques éléments tirés pour certain de l'expérience avec les exceptions.
Le premier élément est l'utilisation d'une syntaxe spécifique pour l'examen des cas d'erreur, une structure similaire à un switch...case mais qui permettra dans un premier temps de "démêler" valeur de retour et signalement de l'erreur; de plus étant spécialisée, elle sera syntaxiquement plus "légère" que son modèle.
Mais surtout elle permettra la mise au point d'un dispositif de vérification de la vérification des erreurs (sic) par le compilateur, à la manière des "checked exceptions" de Java. Il serait possible de faire en sorte que le compilateur déduise du code les erreurs que peut retourner une procédure dans notre cas, mais demander leur déclaration manuelle est sans doute plus simple (pas seulement pour le compilateur) et meilleur.
Enfin, éviter le "motif de la flèche" demande la mise en place d'une construction permettant la définition d'un bloc de code, simplement pour pouvoir interrompre l'execution de ce bloc en cas d'erreur.


dimanche 18 janvier 2009

Basic

Le langage Basic a été créé au milieu des années 60 pour permettre aux étudiants non-scientifiques de programmer un ordinateur. Autrement dit, un langage de programmation pour les non-programmeurs.

On peut dire que le langage a remplit sa mission, puisque (avec peut-être un petit coup de pouce de Bill)  il est devenu le langage d'apprentissage par excellence.

Le premier impératif de ce type de langage est d'être simple. Mais qu'est ce que la simplicité? La théorie a une définition interessante, car elle utilise justement la notion de programme pour quantifier la complexité d'une entité, d'un objet ou d'une donnée: c'est la taille minimale du programme qui produit la donnée ou une description de l'entité considérée. Seulement, on sait qu'un tout petit programme peut être long à s'executer et produire une donnée particulièrement complexe (on peut penser à un programme de décompression par exemple); ce qui amène à considérer le temps d'éxécution de ce programme, et à la distinction deux types de complexités: la complexité dite "aléatoire", qui est en quelque sorte liée à la quantité de données que porte l'objet, et la complexité logique, qui représente le degré d'élaboration de l'objet, c'est à dire en quelque sorte la quantité de travail qu'a nécessité sa construction.

La théorie parle d'un programme, donc d'un langage de programmation. Quel langage utilise-t-elle? C? Basic? Haskell? Celui de la machine de Turing? En fait, peu importe, il suffit d'utiliser toujours le même langage pour pouvoir faire des comparaisons. 

Puisque le source d'un programme est une donnée (textuelle), on peut appliquer cette définition aux programmes eux-mêmes. En particulier, on peut utiliser cette métrique pour un même programme écrit dans différents langages. On devrait mesurer ainsi la complexité relative de différents langages. Toutefois, cela revient plu ou moins à compter les lignes de code, autrement dit au fameux indice "SLOC" (Source Lines Of Code), dont on sait qu'il doit être utilisé avec prudence.

Compter les lignes que l'on doit écrire dans un langage pour obtenir un résultat simple comme afficher "Bonjour tout le monde" est quand même révélateur de certaines choses intéressantes. Cela sépare grosso modo les langages en deux catégories:

* les langages qui le font en une  ligne,

* les langages qui le font en plus de lignes.

La théorie et l'expérience concordent: les programmes de la seconde catégorie, qui consistent généralement en l'instruction "afficher 'bonjour'" entourée de lignes de code dont l'utilité est plus ou moins claire pour un non-initié, sont subjectivement et objectivement plus complexes.

Une autre façon de comparer les langages plus qualitative que quantitative est de dénombrer le nombre de concepts utilisés de manière plus ou moins directe dans ces programmes. Basic par exemple, n'en utilise que deux: le concept d'instruction, et le concept de chaine de caractère. Un langage à objets un tant soit peu rigoureux utilise en plus la notion de classe (ou de méthode). Un langage purement fonctionnel utilisera une monade ou imposera de définir une fonction.

Du point de vue du non-programmeur ou du débutant, un langage appartenant à la première catégorie comme Basic semble préférable, parce qu'il permet d'atteindre un objectif de manière simple. Pourquoi s'encombrer de lignes ou de concepts sans rapport direct avec le problème à résoudre? Pourquoi le langage se jette-t-il en travers du chemin?

Il est évident que les langages sont rarement conçu pour être gratuitement complexes. Les complications qu'ils introduisent ont pour but de se conformer à un paradigme, qu'on estime profitable à moyen ou long terme de suivre. Les concepts imposés qui peuvent paraître a priori superflus sont en général des investissements visant à améliorer certaines qualités internes du programme, telles que la fiabilité ou la maintenabilité sur le long terme. 

Mais surtout, ces "complications" permettent d'aller plus loin avec le langage. La théorie dit que tous les langages qui sont capables de simuler une machine de Turing sont tous capables de faire la même chose. Et donc, le langage de la machine de Turing est aussi capable de réaliser les mêmes choses qu'un langage évolué comme Haskell, Lisp ou SmallTalk. On réalise immédiatement ce que cela vaut en pratique: il serait extrêmement difficile par exemple, de trier un tableau de chaînes de caractères avec une machine de Turing. Le même inconvénient frappe plus ou moins les langages simples comme Basic: la réalisation des fonctionnalités devient de plus en plus difficile à mesure que ces fonctions deviennent évoluées. Les langages simples "décrochent", tandis que les langages plus complexes peuvent suivre l'évolution des besoins de l'utilisateur. La comparaison des exemples de programmes "Hello, world" avec ceux réalisés pour un programme un peu plus élaboré comme "99 bouteilles de bière" esquisse un peu les limites de la simplicité à tout prix.

Ces qualités ne sont pas sans interêt pour le non-programmeur. Obtenir un programme qui marche dans les cas prévus est le premier objectif; mais il appréciera sans doute aussi que celui-ci ne plante pas lamentablement en cas d'erreur de saisie. Et il appréciera aussi de pouvoir le faire évoluer. Par définition, un non-programmeur n'a aucune idée de la façon d'obtenir ces qualités - voire à l'extrême, il n'en a pas toujours conscience a  priori. Enfin, le non-programmeur devient, par la force des choses, un peu programmeur et est logiquement amené à faire des programmes plus ambitieux à mesure que ces compétences s'améliorent. Un "guidage" du langage peut être un élément de satisfaction vis-à-vis du langage ("ce langage est super car il sûr et évolutif"), même s'il recent ce guidage comme une contrainte ou un obstacle.

Un exemple emblématique de cela est l'instruction GOTO de Basic, qui est incomparablement plus séduisante que la programmation structurée (voire "stricturée"), mais qui, pour le moins, à conduit plus d'un programme "fait à la maison" à l'effacement du fait qu'il n'était plus possible de le modifier sans engendrer une nuée de bogues. Dans le même registre mais de manière un peu plus subtile, certains estiment que les exceptions, un moyen relativement intuitif et séduisant pour résoudre le problème de la gestion d'erreur, sont des GOTO (à peine) déguisés.

Est-ce à dire que les langages "compliqués" seraient en réalité plus adaptés aux non-programmeurs? Il suffit de prendre un exemple extrême comme le langage Haskell, ou d'une autre manière, Forth, pour se convaincre du contraire. L'un est un langage d'universitaire, l'autre un langage de "vrai" programmeur. Ces langages semblent empreinter des voies très détournées pour atteindre l'objectif de faire un programme qui execute une tâche déterminée. Certes, ces langages "à forte personnalité" excellent à atteindre les objectifs secondaires pour lesquels ils sont construits (forte optimisation et simplicité de conception pour l'un, formalisme rigoureux pour l'autres), et ses objectifs secondaires sont sensés aider à atteindre le but principal et/ou maximiser une ou plusieurs des qualités évoquées plus haut.

Mais le revers de la médaille est que cette volonté d'excellence dans ces domaines secondaires obligent à sacrifier certaines aides directes à l'accomplissement du projet principal (syntaxe inhabituelle ou cryptographique, "surcharge" de concepts ou de connaissances prérequises), alors que l'utilisateur n'a pas forcément besoin d'une extrême fiabilité, optimisation ou évolutivité. Cette excellence est ce qu'on appelle de la "surqualité", une qualité ou niveau de qualité qui n'était pas requis initialement, et qui est en fait néfaste car elle entraîne toujours sous une forme ou une autre un coût qui n'est pas justifié (du point de vue du client ou de l'utilisateur final).

Un compromis entre une simplicité qui rend le langage accessible au premier abord et une complexité qui le rend utilisable sur le long terme est donc à trouver. La formule de ce subtil équilibre a déjà été en grande partie par le langage Lisp:

  1.  les clôtures ("closures"), des entités qui peuvent être difficiles à comprendre mais qui s'avèrent très utiles. Elles permettent notamment de construire un système orienté-objet si on le souhaite (voir CLOS).
  2. un système de "macros" qui tient la route (celui de C/C++ est à côté une mauvaise plaisanterie), et qui permet de rendre plus expressif le langage quand les choses se compliquent.   

Mais il a un défaut majeur: le manque de lisibité de sa syntaxe préfixe et sa célèbre surabondance de parenthèses. La lisibilité d'un langage est une qualité qu'on sous-estime ou sacrifie trop souvent. Un langage lisible permet de communiquer clairement le modus operandi d'un programme. On utilise souvent du "pseudo-code" à cette fin plutôt que d'utiliser le langage d'implémentation, et c'est à mon sens à porter au passif dudit langage. Un langage clair devient un langage de communication aussi avec les autres utilisateurs, plus approprié que la langue naturelle qui parfois trop floue ou ambigüe (pour ne pas paraphraser: "mieux vaut un petit programme qu'un long discours"), ce qui facilite l'apprentissage et permet la coopération.

mercredi 14 janvier 2009

Déclic?

Ce n'est pas la première fois qu'il me prend l'envie de créer un langage, au contraire; je suis sûr de retrouver en fouillant dans quelque vieux carton plusieurs ébauches de spécification de langage datant d'époques plus ou moins anciennes.

Pour tout dire, 4IM a été le seul qui a aboutit. Peut-être parce que je me suis contenté cette fois d'adapter un langage existant (Forth) à ma guise, plutôt que de partir d'une feuille blanche. Les autres ont rarement dépassé le stade de l'esquisse.

Cette envie me prend suite à des choses lues dans quelque livre ou magazine, encore lue sur interne, concernant les langages, les méthodes logicielles, ou les maths. L'idée de départ est donc purement technique.

Cette fois-ci, le déclic c'est produit alors que j'éditais une page Web relative à 4IM avec mon éditeur favori, et je trouvais particulièrement pénible d'avoir à enregistrer d'un côté, puis de passer au bureau suivant pour recharger la page dans le navigateur pour visualiser le résultat. La réflexion m'est venue que, avec toute l'artillerie logicielle dont ont dispose actuellement, il reste impossible d'obtenir aussi simple que celle-là. Il aurait suffit que mon éditeur favori envoie un simple signal à mon navigateur favori.

Il est possible que mon éditeur ou mon navigateur favori ne soient pas les meilleurs dans le domaine de la coopération entre applications, ou que je n'utilise pas les bons outils (pourquoi ne pas utiliser un éditeur spécialisé pour cela?), ou encore que la possibilité existe bien, et que je n'ai pas assez cherché. Même si la solution est bien dans l'une des ces propositions, sa mise en oeuvre demande des efforts ou a des inconvénients disproportionnés par rapport à l'idée simple et intuitive que peut avoir tout utilisateur lambda: que mon éditeur préféré envoie une commande de rechargement de sa page à mon éditeur préféré.

Le fameux adage "qui peut le plus, peut le moins" est si souvent pris à défaut qu'il faudrait à mon avis se décider un jour à adopter son contraire - ou est-ce une fausse impression due à ce que je suis très souvent aux prises avec des logiciels?

Evidemment, des monstres de puissance et de souplesse comme Firefox et Vi ne peuvent malgré tout pas résoudre chaque micro-problème de chaque utilisateur. Ce n'est pas un problème technique, car les auteurs de chacun des deux logiciels pourraient bricoler une solution en cinq minutes. C'est un problème similaire à celui de la "longue traine". En l'espèce, si ces logiciels satisfont les besoins courant d'un grand nombre d'utilisateurs dans un certain domaine, il existe au moins un aussi grand nombre d'utilisateurs avec des besoins particuliers qui ne sont pas satisfaits. En réalité, nous sommes tous un peu dans les deux catégories; c'est ce qui fait que les listes des doléances des logiciels ("les wishlists") ne se vident jamais et que pour la plupart des logiciels, les numéros de version ne cessent de s'incrémenter.

Cette situation était sans doute une fatalité il y a une vingt/trentaine d'années, quand ni l'internet ni le logiciel libre n'existaient, et que les ordinateurs étaient de bien étranges boîtes dont on ne savait pas encore bien quoi faire. Je pense que ce modèle issue de ce qu'on appelle "l'industrie de l'édition du logiciel", une dénomination qui résume parfaitement son mode de fonctionnement, est en cours d'obsolescence. Aujourd'hui, il est de plus en plus possible de trouver quelqu'un sur un forum ou un proche capable d'apporter une solution logicielle à un problème logiciel.

Pour l'instant, c'est encore loin d'être envisageable par le commun des mortels, et est parfois même difficile pour un programmeur de profession. En pratique de nombreuses difficultés font obstacle: différentes applications utilisent différents langages parfois ésotériques ou abscons; il faut faire avec certains "aléas de fonctionnement"; il faut parfois poser maladroitement des questions en anglais sur des forums faute de documentation suffisante.

Et pourtant, ce monde idéal où un utilisateur pourrait modifier son logiciel soit par ces propres moyens, soit avec l'aide d'un autre, pour obtenir la fonction qu'il désire me paraît dans une certaine mesure possible. Mon expérience avec 4IM m' a donné cette impression. Forth est presque à l'opposé de cela, car c'est un langage ésotérique dont l'utilisation exige une connaissance plutôt pointue du fonctionnement d'un ordinateur et une rigueur des plus rigoureuses. Toutefois, pour qui aurait le courage de s'investir dans l'étude de 4IM, il y aurait la récompense d'accéder à quelques possibilités intéressantes, comme la construction d'une interface graphique, la communication en réseau, ou la construction d'univers animés en 3D (dans un version qui risque de prendre quelque temps à sortir...).

Il n'y a là rien de très extraordinaire; beaucoup de langages savent faire cela et bien plus, car il existe des librairies logicielles toutes faites qui réalisent ces fonctionnalités. Certaines librairies que j'ai utilisé sont si bien faites que, en faisant abstraction des connaissances nécessaires pour programmer en C/C++, n'importe qui pourrait l'utiliser.  

Contrairement aux fois précédentes, ce qui m'a motivé à imaginer un langage n'est pas une théorie mathématique ou logicielle glanée quelque part, mais plutôt des considérations sur les logiciels et leurs utilisateurs: les utilisateurs auront toujours des besoins bien particuliers auxquels les logiciels ne peuvent pas répondre, car trop spécifiques; des personnes sur internet ou autour de soi pourraient les aider à obtenir ce que l'on souhaite, mais ces bonnes volontés sont entravées par des difficultés techniques; des briques logicielles simples d'utilisation existent ou peuvent être créés pour réaliser des logiciels avec des fonctions élaborées.

C'est je pense un point de départ très différent de celui de la majorité des langages de programmation, qui partent du principe que leurs utilisateurs sont des programmeurs. Que les langages de programmation soient destinés aux programmeurs est une fausse évidence. Il existe des langages de programmation pour les non-programmeurs ou aspirants programmeurs (Basic par exemple).

Toutefois, beaucoup de ses langages pour être faciles d'accès sont trop simplistes pour faire des choses "sérieuses". Faire un programme nécessite un minimum de rigueur, de logique, d'aisance en maths, un goût pour la résolution de problèmes, autrement dit un esprit scientifique. Supposer que l'utilisateur du langage a ce profil est raisonnable, permet de ne pas sur-simplifier le langage et d'y introduire quelques concepts élaborés qui permettent de faire de "vrais" programmes.

La démocratisation d'Internet et l'explosion du logiciel libre a permis à plusieurs milliers de programmeurs, de concepteurs d'élaborer et de concrétiser autant d'idées si ce n'est plus. A tel point qu'il est difficile de ne pas trouver sur internet un logiciel réalisant une fonction particulière. Je me suis donc mis à la recherche d'un langage conçu dans le même but ou possédant les caractéristiques que j'estimais nécessaires pour l'atteindre. Je suis assez étonné de ne l'avoir pas trouvé.

Plutôt que d'énumérer de manière plus ou moins aride ces caractéristiques ici, il me paraît plus intéressant de faire la critique des différents langages que j'ai examiné. Ce sera l'objet de mes prochains articles.

lundi 12 janvier 2009

Hors-sujet : Zéro.

Le titre de ce blog vient de mon précédant "proto-blog" (http://astrobe.byethost31.com/hs.html).

En jettant un oeil à celui-là, force est de constater que presque tous les articles portent sur un même sujet, la programmation. Même en arguant que le sujet du site est 4IM, mon dialecte du langage Forth, le titre ne semblait pas tout à fait approprié.

Choisir le titre d'un blog est difficile si on est ni monomaniaque (auquel cas le titre s'articulera autour de la passion qui vous obsède), ni égocentrique (auquel cas il suffit de trouver un titre vous mettant suffisamment en valeur), on cherche avant tout un titre passe-partout, mais quand même un peu classe.

"Hors-sujet" est ce genre de titre: il ne véhicule rien de personnel ou de spécifique, mais il a quelque chose de sévère, sérieux et en même temps rebelle. Je pense que le souvenir de rédactions ou dissertations que l'on a cru brillantes ou que l'on a pris plaisir à rédiger, mais finalement frappées de cette condamnation, y est pour quelque chose.

Ce titre a un petit inconvénient: il faut que je choisisse un sujet dont je ne parlerai jamais, car pour pouvoir être hors-sujet, il faut bien définir un sujet. Le sujet de ce blog sera donc la définition du sujet de ce blog.