blog.nraynaud.com Langages

Aller au contenu | Aller au menu | Aller à la recherche

lundi, janvier 18 2010

La règle de nraynaud

Au cours d'une discussion avec un ancien collègue, il m'a rappelé une règle que j'avais édicté à l'époque dans l'équipe (surtout parmi les seniors) : quand un développeur te demande de l'aide, tu lèves ton cul et tu vas t'assoir à côté de lui.

Tu ne tentes pas un diagnostique depuis ton poste à coup de questions, tu ne restes pas debout à côté de lui. Mais tu t'impliques et tu consacre du temps à tes juniors, en particulier quand ils viennent de galérer 2h sur un pb tout seuls.

Bien entendu, aujourd'hui j'ai oublié cette règle et je l'ai enfreinte. Il va falloir me rattraper demain matin. Être senior c'est aussi être responsable de ses actes.

jeudi, novembre 26 2009

Les transactions

Les transactions en développement sont souvent négligées, alors qu'il existe des domaines où elles ne devraient pas. Le mot "transaction" vient du commerce, une transaction c'est l'échange de d'un produit ou service contre de l'argent (ou contre un autre produit ou service). C'est une action équitable, personne doit perdre ou gagner dans l'histoire. Dans la plupart des cas, l'intégrité des transactions n'est pas assurée car 1) c'est assez coûteux en ressources, 2) du fait de leur peu d'usage, peu de développeurs savent les utiliser.

Exemple

Imaginons un cas simple : dans un jeu vidéo il existe un supermarché, quand le joueur achète son objet, il faut qu'à la fin de la transaction l'objet soit dans son sac, et que l'argent soit retiré de sa bourse. En cas de problème (soit problème informatique, soit un problème simple tel que le joueur n'a pas assez d'argent) lors de l'échange, il faut que l'objet soit toujours dans le supermarché, et que l'argent soit toujours dans son sac. Il faut donc protéger la séquence : je vérifie la bourse, je vérifie la disponibilité de l'objet, je débite la bourse, je crédite le supermarché, je retire l'objet du stock, je rajoute l'objet dans le sac du joueur. Et il est interdit de s'arrêter en route, si une de ses étapes échoue, il faut que tout revienne comme avant.

Je propose une règle simple pour l'usage des transactions : toute fonction impliquant de l'argent réel de l'entreprise doit être transactionnelle et proprement isolée ("sérialisable"). Ceci est un minimum, d'autres cas peuvent aussi en bénéficier, mais au moins tous ceux-ci doivent être transactionnels.

Petit contre-exemple

Je suis intervenu il y a quelques temps dans une entreprise où le paiement était pilotée depuis du flash sur le navigateur du client. C'est un risque majeur pour l'entreprise. D'une part, le lien entre le navigateur web et le serveur n'assure pas l'intégrité des transactions et le simple fait d'exposer par le serveur web les différentes étapes de la transaction est un risque. Un utilisateur un peu débrouillard aurait tendance à vouloir appeler la partie de la transaction qui met les objets dans son sac en oubliant d'appeler la partie qui débite sa bourse. Il ne faut exposer qu'une seule fonction qui déroule toute la transaction ou échoue de manière atomique.

En résumé

S'il y a de l'argent en jeu, on utilise une transaction en "sérialisable" pour vérifier le montant du compte, transférer l'argent, transférer le bien en une seule étape. Il ne reste plus qu'à lire la doc de votre base de données et celle de votre couche d'accès aux données :)

vendredi, novembre 13 2009

Le manque d'un moteur de templates utile et abordable en java

Récemment j'avais un projet qui semble pourtant simple au premier abord : mon client voulait générer des fichiers PDFs. Le use-case est le plus bête de la terre, l'utilisateur rempli un (assez long) formulaire, et suivant les réponses on lui génère 5-6 fichiers PDFs dans un répertoire.

Je pensais qu'il s'agirait d'un projet simple et j'avais tort. Il n'existe pas d'éditeur de templates réellement utile pour ce cas. Le client m'a envoyé ses exemples de fichiers en format word, et il fallait les remplir et les générer en PDF, sauf que les systèmes que j'ai étudiés (iText, FOP, jasper) nécessitaient tous de re-saisir intégralement le layout des fichiers du client, aucun ne permettaient de simplement importer le fichier word, insérer des "trous" dedans et de l'envoyer à java. Il est possible dans des cas limités de remplir des champs de formulaires dans un PDF, mais ce cas ne permet pas de "reflow" du document, ce dont j'avais besoin.

Au final, j'ai donc choisi d'utiliser un connecteur OpenOffice.org (j'ai converti les .doc en .odf à "trous"), un choix de développeur plus que d'utilisateur, car il simplifiait ma vie de développeur, mais livre un système lent et tendant à crasher à l'utilisateur (il ne voit jamais l'interface d'OpenOffice.ord lors de l'opération). Étant donné le contexte économique entourant le projet, cela je pense que c'était la décision la moins pire, mais clairement je suis frustré de livrer ce type de système.

D'où mon appel : il manque un outil de ce type pour java, peut-être "simplement" un moyen d'exporter des fichiers compris par FOP ou iText ou Jasper depuis OpenOffice.org, mais clairement il manque un maillon entre le .doc du client et le template du développeur.

mercredi, octobre 7 2009

3 langages clamant implémenter IEEE 754

Après quelques recherches, je suis tombé sur ce post qui explique Factor et cite D et SBCL.

Pour jouer la mijaurée, je dirais que je ne suis pas fortement intéressé par ces langages, dommage :)

vendredi, avril 3 2009

IEEE 754 : on sait pas compter et on s'en fout.

Je viens de passer une petite semaine dans des docs à propos d'IEEE 754 et le moins qu'on puisse dire, c'est que le résultat n'est pas brillant.

Qu'est-ce que c'est ?

Il s'agit d'une norme décrivant une représentation des nombres à virgule flottante et des règles de calcul entre eux.

Différentes tailles de mots

La norme définit 4 tailles de mots, avec comme règle : plus c'est petit plus c'est rapide, plus c'est grand, plus c'est précis. Un compromis plutôt intuitif. Pour tous ces mots, elle définit la manière dont on encode les valeurs dedans.

Des valeurs spéciales

Afin de produire un système de calcul consistent, il n'existe pas que des nombres dans ce système.
  • les nombres dénormalisés : le système permet de garder encore un peu de précision même quand un nombre est trop près de zéro pour être encodé normalement
  • les infinis : + et - l'infini sont représentés, et sont utilisables dans toutes les opérations, atan(+inf) donne pi/2, 1/+inf donne zéro etc.
  • le zéro signé : en IEEE 754, le zéro est signé, ce qui permet de maintenir une approche "latérale" sur les fonctions qui ont une asymptote verticale en zéro, par exemple 1/+0 donne +inf
  • NaN (not a number) : quand une opération est illégale, cette valeur est retournée. Par exemple 0/0 ou sqrt(-1) renvoient NaN.

Des arrondis

La norme en outre défini des arrondis, qu'on peut demander concernant le calcul. En interne, les calculs sont effectués avec un peu plus de précision que nécessaire, et au moment de couper le nombre pour le faire rentrer dans la taille de mot de l'utilisateur, il faut respecter la règle d'arrondi courante.
  • Vers zéro (troncature)
  • Vers +infini (ceiling)
  • Vers -infini (floor)
  • Au plus proche (arrondi). La définition est un poil plus précise ça à cause de certains cas particulier, elle s'appelle round to even. C'est le mode par défaut.
Changer le mode d'arrondi a essentiellement 2 usages : tester la robustesse d'un algorithme ou faire du calcul d'intervalle. La robustesse d'un algorithme c'est savoir "si je change un peu mes données de départ, est-ce que ça change beaucoup mon résultat ?" si la réponse est oui, alors le calcul sera imprécis. Il s'agit de faire exactement l'inverse de Lorentz, on lance un calcul avec plusieurs modes d'arrondis et si les résultats sont trop différents, le calcul n'est pas robuste.
Le calcul d'intervalle consiste à faire tourner 2 fois le calcul, une fois avec un arrondis vers l'+inf et l'autre vers -inf et on sait que la valeur réelle se situe entre les 2. On constate que si l'intervalle est grand, on a l'algorithme n'est pas robuste.

Des évènements

Lors d'un calcul, un certain nombre d'évènements peuvent se produire :
  • division par zéro (cet évènement est mal nommé) : il est signalé lorsqu'un infini apparait au résultat alors qu'il n'y avait pas d'infini dans l'entrée. L'exemple classique est 1/0 mais log(0) aussi devrait le produire.
  • calcul inexact : cet évènement est très courant, le système passe son temps à arrondir, mais si, par hasard, il lui arrivait de ne pas massacrer un nombre, il ne devrait pas signaler cet évènement.
  • opération invalide : l'exemple type est 0/0 mais aussi sqrt(-1) etc.
  • overflow : assez explicite, le nombre est trop grand (positivement ou négativement) pour rentrer dans la case, même avec un chausse-pied.
  • underflow : lorsqu'un nombre est trop proche de zéro et qu'il va passer en dénormalisé
Lors qu'un évènement survient, la norme suggère 3 types de comportements, par complexité croissante pour l'utilisateur :
  • renvoyer une valeur adaptée. Par exemple 0/0 -> NaN, 1/0 -> +inf, un overflow renverra un ±inf etc.
  • des flags sont disponibles pour l'utilisateur, après chaque calcul il peut les inspecter, et les remettre à zéro après les avoir vus.
  • un listener d'évènement où l'utilisateur peut enregistrer son propre code qui sera exécuté à ce moment.

Un peu de positif

  • pour la partie calcul, à peu près tous les langages savent compter avec des nombres
  • en général, rallonger une variable (passer de 4 à 8 octets par exemple) offre bien plus de précision

Mais aucun langage ne l'implémente !

  • Les développeurs de langages ne sont pas intéressés par IEEE 754.
  • Le compromis précision/vitesse n'est en général pas respecté (on peut avoir plus de précision gratuitement)
  • Aucun langage n'expose correctement IEEE754 à ses utilisateurs.
  • La partie IEEE 754 de la norme C99 n'est pas prise en compte par gcc (face à un exemple qui fonctionnait, un développeur de gcc m'a dit : "c'est de la chance").
  • Exposer les évènements de calcul sous forme de signal unix rend leur traitement très compliqué (quel thread à lancé ça ? à quel endroit exactement ?)

Des dangers

Normalement, si un NaN apparait en cours de calcul, les règles font qu'il est propagé. Mais il a un risque de ne pas être vu. En effet, les comparaisons ne propagent pas les NaN dans le calcul. Ainsi 3 < NaN est toujours faux, de même que NaN = NaN qui est toujours faux. D'ailleurs il peut pas en être autrement un booléen est un booléen même chez IEEE 754, il n'existe pas de "NaB" Not a Boolean, donc la comparaison renverra toujours une réponse, cependant, attention à son interprétation.
D'autre part, en ne prenant pas soin du signe des zéros on peut faire apparaitre des infinis avec le mauvais signe, et avoir des résultats abérents.

samedi, décembre 24 2005

Une super notation pour les grammaires

On y pense pas mais l'ergonomie c'est important pour les grosses grammaires.

Lire la suite...

mercredi, août 10 2005

Intervenir sur un framework tier

C'est assez étrange, on parle souvent d'utiliser des frameworks, de les concevoir, mais jamais de les modifier.

Prenons un exemple courant : les frameworks de composants web. Sur les 3 étudiés (WebForms, JSF et Plumtree), aucun ne propose de rajouter des balises spécifique dans le <head> tels que CSS, styles en ligne ou meta tags. Certains on tout juste de quoi rajouter du javascript.
Imaginons qu'on veuille créer l'infrastructure manquante, les solutions sont alors minces, car ces frameworks sont fait pour être utilisés, pas étendus. D'autre part, comme le disait M. Fowler, les framework ont le contrôle des opérations donc c'est bien de leur responsabilité de faire cette opération.

Dans les frameworks objet, les composants sont créés en mémoire, assemblés, reçoivent divers évènements qui les intéressent puis ils sont rendus sur le flux de sortie. On peut donc décider à la réception des évènements de mettre des chose dans le header, le rendu ne sera pas encore commencé.

Imaginons qu'on créé un "centre d'enregistrement" global pour tout ce qui ira dans la balise <head> au cours de la requête courante. Nos composants personnel sont au connaîsent ce centre et l'utilisent à bon escient dans leurs handlers d'évènements. Il suffit donc en début de rendu de pousser le prologue du fichier, puis le contenu du centre d'enregistrement, puis le rendu récursif des composants.

On pourra tenter d'étendre la classe faisant le rendu des caractères dans le flux de sortie, mais cela peut être complexe. Il faudrait arriver à redéffinir une des méthodes écrivant dans l'entête du fichier HTML (entre la balise ouvrante et la balise fermante de l'élément head), rien ne dit qu'il en existera une, quand à savoir si le point d'extension trouvé sera élégant, c'est une autre histoire. D'autre part, il faudra arriver à enregistrer une instance de la nouvelle classe concrète partout où il y avait l'ancienne, (ce qui élimine d'office Plumtree qui utilise un template method généralisé, et des dizaines de classes étendent la classe possédant la méthode de rendring), sur cet aspect, JSF peut bien s'en sortir par ses renderers externes.

Si on a accès au code source on peut, suivant ses ambitions, soit prévoir un point de branchement complet (mais en garder la maintenance) en refactorant le code existant, soit mettre un quick-hack (pour maintenir un minimum de code externe). Mais tout le monde ne donne pas son code source.

Après ... ben après, j'attends vos idées, car je suis confronté au problème et je sèche.

dimanche, décembre 19 2004

La guerre de la Simulation

Depuis mon dernier post, j'ai trouvé du boulot, en rapport avec les logiciels d'électronique.

Je suis en pleine guerre. Traditionnellement, les simulateurs n'étaient utilisés que pour des besoins précis. Depuis la puissance des machines faisant, on s'est mis en tête d'arrêter progressivement de tester sur des protos au profit de la simulation. Parallèlement, la complexité des systèmes intégrant de l'électronique a augmenté, et les interactions entre unités, et donc les risques afférents aux couplages entre modules. La nécessité de penser "global" au lieu de "module" est apparue, avec des postes dédiés à la pensée interraction entre module, impact global etc. En particulier, le crash d'ariane 5 a conduit à un rencentrage sur cette fonction.

Sont donc montés en puissance des simulateurs (enfin UN simulateur) non plus d'électronique, mais ayants une vision plus globale de la chose, des simulateurs "système" cassant même la barrière du domaine de l'électronique en allant simuler de la chaleur, du numérique, de la mécanique simple, un peu d'hydraulique ... Bref un menu composé de réseaux d'énergie et de lois de commandes.

C'est là que la bataille des langages de simulation s'engage ; il y a trois concurrents avec 3 approches différentes :
  • MAST, le langage du leader, qui n'a aucun avantage technique sauf celui d'être le premier historiquement. Utilisé par Saber

  • VHDL-AMS (VHDL avec une extension Abstract Mixed Signal) qui a le gros avantage d'une approche structurée, sûre et proche du métier. Utilisé par Simplorer, SystemVision et bientôt SaberHDL

  • Modelica qui est issue d'une étude formelle sérieuse et intègre même les unités comme je les ai définies il y a quelques temps. Plutôt proche du matheux que de l'ingénieur. Utilisé par Dymola.


  • Le seul vrai problème dans cette guerre qui s'annonce, c'est que d'une part, le langage on s'en fout, pour être simulés, les modèles sont transformés en un sous-langages très simple intégré au simulateur, d'autre part, ce type de choix est plus fait sur des critères marquetting que sur des critères techniques, la preuve : la mise en avant des langages utilisés sur les sites web des différents simulateurs.

    Enfin, il y a bien longtemps que les modèles transférés entre entreprise sont cryptés. A quand la première centaine de morts dûs à un modèle crypté foireux ? Qui en prendra la responsabilité ?

    lundi, avril 12 2004

    Types et unités physiques

    Quelques réflections sur le sujet.


    Les types statiques ont prouvé leur efficacité dans la détection d'erreur a-priori. On peut tenter d'appliquer cette détection précoce à la physique en faisant correspondre un type à chaque unité physique un type.



    Types Ada

    Fin '70, Ada a tenté de faire cette correspondance directe, étant prévu pour des applications physiques, c'était l'idée la plus simple. En effet, Ada permet de définir de nouvaux types non structurés à partir des types de base. Il permet de le faire de 2 façons : par sous-typage, où le nouveau type est valide partout où l'ancien l'était (la réciproque étant fausse) ; ou par création d'un type imcompatible, le nouveau type récupérant les caractéristiques de l'ancien mais il est incompatible là où l'ancien l'était.



    illustration
    type meter is new Integer;
    type Amper is subtype Integer;


    La physique

    En physique, l'analyse des unités est une technique de détection d'erreurs des plus simples, elle est enseignée au secondaire, et est largement efficace. le principe consiste à affecter à chaque valeur une unité. Il existe des unités de base, définie dans le système internationnal mksA (mètre, kilogramme, seconde, ampère) qu'on manipule à l'aide de l'opérateur . formant une loi de groupe.



    illustration
    m.s^-1 correspond à l'unité de vitesse
    A.V correspond à l'unité de puissance

    Un certain nombre d'unités "primitives" possèdent un ou des équivalents composés.



    illustration
    V.A = N.m/s = J/s = W
    La puissance est le produit de Volts et d'Ampères, ou encore le produit de Newtons par des mètres divisé par des secondes ou encore le quotient de Joules par des secondes et bien sûr des Watts.

    Ainsi, pour vérifier la cohérence d'une équation, on vérifie que ses unités à gauche et à droite sont équivalentes.



    Les règles sont les suivantes :

    • l'addition est homogène (les 2 opérandes doivent être de même unité et le résultat est de la même unité)

    • les unités munies de la multiplication sont un groupe, dont l'élément neutre est le coefficient sans unité

    • les fonctions transcendantes sont homogènes

    • les règles de correspondances entre unités viennent de la théorie physique utilisée (par exemple, le principe fondemental de la dynamique nous dit que m.s^-2 < => N/kg)

    Les unités "Ingénieur"

    Les ingénieurs, appellés à manipuler énorméments de données dans un domaine spécifique on ajouté des unités confort qui sont essentiellement de deux types : soit des unités proportionnelles aux unités du système international leur correspondant, ainsi un cheval vapeur vaut 736 Watt ou bien des unité dérivées de façon "transcendante" des unités du système international tel le décibel qui est proportionnel à au logaritme de la valeur sans unité.




    Unités physiques et types statiques

    Les opérateurs dans les langages courants sont quasiment unanimement homogènes, en particulier la multiplication ; c'est le premier rempart à une vérification des unités par les types statiques.



    Le second rempart est l'absence d'une loi de groupe sur les types de façon qu'on puisse en construire de nouveaux selon les règles de la physique.



    D'autre part, il faut un système de combinaison des contraintes informatiques avec les contraintes physiques, ainsi, on peut par exemple vouloir additionner deux puissances exprimées en Watt et en nombres à virgule flottante simple précision puis garder la somme en double précision (à des fins d'intégration fine par exemple).



    Enfin, il faut un système de contrôle tel que les unités ingénieur soient, elles-aussi vérifiées alors que d'un point de vue physique elles peuvent être équivalentes.

    samedi, décembre 6 2003

    des fois je me demande ...

    #include <sys/sem.h>
    int get_semaphore(semid,event,value)
    int *semid, *event, *value;
    {
    int status;
    status = semctl((*semid),*event,GETVAL,value);
    return 0;
    }
    int *a,*b,*c;
    int
    main ()
    {
    if(get_semaphore(a,b,c))
    exit 0; 
    ;
    return 0;
    }

    Il existe réellement des plateformes où ça compile ça (notez le manque d'includes et la tronche du exit) ? Tiré du code généré par : http://cvs.sourceforge.net/viewcvs.py/autoopencas/OCC/OCC/configure.ac?rev=1.49&view=auto

    Je sens qu'il est pas près d'être compilé ce CasCade !

    jeudi, septembre 25 2003

    Icon

    C'est un langage que je ne conaissait que de nom, il a une sémantique tout-à-fait marrante. Je vous laisse la découvrir ici.

    lundi, août 18 2003

    C'est pas moi qui l'ai dit


    The language F unfortunately allocates cells on the stack, which complicates the implementation of capturing mutable variables in closures. So F re-introduces the concept of immutable variables and restricts free variables in closures to be immutable:
      { final int x = 3          -- immutable
    ; int y; y = 3 -- mutable
    ; void f(int z){ y = x+z } -- ERROR, y is mutable
    ; ....
    }

    While I think it is a good thing to be able to distinguish between mutable and immutable variables in the same language, I find it dreadful that when variables are mutable by default, closures can only capture immutable free variables.

    Je vois pas, mais alors pas du tout de quel langage il pourrait parler ... Franchement, faudrait être à moitié fatigué pour mettre le mot-clef final pour désigner une variable immuable !


    static public ObjectFactory factory(final Connection connection) {
    return new ObjectFactory() {
    public ObjectInterface createObject(String objectID) {
    return new ObjectWrapper(objectID, connection);
    }
    };
    }

    Ah bah tiens ! Je l'utilise même !
    Au passage, notez la superbe curryfication du constructeur à 2 arguments.

    lundi, août 4 2003

    entrées-sorties en Haskell

    Haskell possède un système assez particulier d'entrées-sorties. La raison en est simple, en Haskell, l'unique chose qu'on peut faire est donner des noms à des expressions utilisant d'autres expressions ou d'autres noms, dans ce contexte, la notion d'ordre entre différentes actions est complètement absente. Le problème, c'est qu'à un moment ou à un autre, il faut sortir de la machine virtuelle. Objective Caml propose tout simplement une notation spéciale qui brise le principe d'ordre d'évaluation indifférent, allant même jusqu'à proposer des références explicites (le mal absolu quoi !). Haskell a fait le pari de rester intégriste (pour pouvoir en exploiter les avantages) et utilise donc un système assez déroutant. Le principe est le suivant : on a besoin de spécifier l'ordre entre 2 actions, hors la notion d'ordre existe déjà en paradigme intégriste : on sait que le résultat d'une fonction dépend de ses arguments, il faut donc tenter d'accrocher une syntaxe fasse correspondre "je veux que action1 se déroule avant action2" et "le résultat d'un appel de fonction dépend de son argument". C'est là qu'arrive un super-papier : Tackling the awkward squad: monadic input/output, concurrency, exceptions, and foreign-language calls in Haskell.

    Là où c'est très fort, c'est lorsque par pure transformation syntaxique, on commence à avoir une notation qui ressemble à de l'impératif.
    Je vous livre un petit exemple tiré un d'un concours C++ de Hardware.fr.
    L'énoncé est le suivant : trouver tous les anagrames d'un mot contenus dans un dictionnaire.
    Voici une solution :

    -- anagrames
    -- utilisation : anaProg mot dictionnaire

    module Anagrames where
    import List
    import IO

    estAnagrame :: [Char]-> [Char]-> Bool
    estAnagrame motTrie mot2 = motTrie == sort mot2

    traiteFichier :: Handle -> [Char] -> [[Char]] -> IO [[Char]]
    traiteFichier fich motTrie liste = do
      f <- hIsEOF fich
      if f
        then return liste
        else do
          ligne <- hGetLine fich
          if (estAnagrame motTrie ligne)
            then traiteFichier fich motTrie (ligne:liste)
            else traiteFichier fich motTrie liste

    montrerListe :: [[Char]] -> IO ()
    montrerListe l = case l of
      [] -> putStr "--"
      (x:xs) -> do
        putStrLn x
        montrerListe xs

    anaProg :: [Char]-> [Char]-> IO()
    anaProg mot nomFichier = do
      fichier <- openFile nomFichier ReadMode
      liste <- (traiteFichier fichier (sort mot) [])
      montrerListe liste

    On y voit plein de IO dans les types inférés, qui dénotent des actions situées "en séquence" (de façon que les entrée-sorties se produisent dans l'ordre voulu) dans le programme. En effet, la notion de séquence se retrouve aussi dans le type des expressions empêchant ainsi une sortie de séquence involontaire ; il est possible de sortir de la séquence mais c'est dangereux donc cherchez-vous-même comment on fait, c'est marqué dans le papier sus-cité en plus.

    Le choix du langage ne s'avère pas très intelligent, on est loin d'exploiter les avantages de la fainéantise dans ce cas précis.

    mercredi, juillet 23 2003

    De l'utilité de la fainéantise ...

    Il y a longtemps, avant même que je sache se qu'est un blog, Seb, le même que déjà cité, m'avait fait part d'un petit défit auquel il avait été confronté lors de l'écriture d'un compilateur.
    Comment créer un arbre dont les noeuds connaissent leur parent sans utiliser de dangereux effets de bord ? Un autre pote autour de la table (oui c'était dans un restau, on décroche jamais) m'a suggéré que la solution tournait sûrement autours de la fainéantise (Lazyness).

    Précisément qu'est-ce que la fainéantise ? C'est le fait de ne pas effectuer une opération avant que son résultat ne soit nécessaire.

    exemple :
    let f arg1 arg2 = arg1 + 3
    let r = f <expression1> <expression2>


    Un langage courageux (eager) comme C ou Objective Caml va évaluer expression1 et expression2 (dans un ordre indéterminé) puis lancer l'appel de f sur ces 2 arguments. Et découvrir que le calcul d'expression2 était inutile (dans le cas d'un exemple aussi simple, le compilateur peut optimiser s'il a la preuve que expression2 ne contient pas d'effet de bord).
    Un langage fainéant (lazy) va directement passer à la fonction les expressions non évaluées, elle se débrouillera pour évaluer ce qu'elle veut si elle a besoin d'un résultat. Ainsi, expression2 ne sera pas évaluée dans cet exemple.

    Bien, c'est super, mais c'est pas super utile jusqu'ici, on peut faire mieux. Tout n'est calculé qu'à la demande, par exemple les éléments d'une liste chainée ne sont calculés qu'à la demande donc pour les grandes listes dont seul le début nous intéresse, c'est super, rien de trop ne sera calculé ni même instancié.

    exemple :

    let liste n =
      let rec liste' m = if m >= 0 then n - m :: liste' (m-1) else []
      in liste' n
    let l = liste 10000000
    let result =
      let rec calc x::xs n =
        if n > 0 then x + calc xs (n-1) else 0
      in calc l 10

    La notation tete::queue est la notation des listes.
    Avec cet exemple, dans un langage avide, le calcul de l est obligatoire, quelquesoit sont utilisation réelle. Sauf que dans cet exemple, un "stack overflow" a toutes les chances de survenir avant que le résultat n'arrive (le calcul n'est volontairement pas récursif terminal). Dans un langage fainéant, pas de problème ! on calcule la somme des 10 premiers entiers.
    Et puisque qu'on sait écrire des choses tecniquement incalculables, allons-y à fond, il est même possible décrire des choses théoriquement infinies.

    exemple :

    let rec suivants n = n::suivants (n + 1)

    qui représente la liste des entiers suivants n. Tant que vous n'essayez pas d'en calculer des trop grands, tout va bien dans un langage fainéant.

    Revenons à notre problème d'arbres.

    Prenons simplement un parent et un enfant (liste chainée de 2 éléments), si on construit le père d'abord et qu'on le passe au fils, le père ne connaitra pas son fils, car on a pas le droit "d'affecter" le père pour y enregistrer le fils, trivialement, dans l'ordre inverse, ça ne marche pas non plus.
    Il faut donc construire simultanément les 2. Ce qui consiste à retarder la construction de l'un jusqu'à la création de l'autre, et réciproquement, d'où une belle boucle infinie. Hors la fainéantise permet de manipuler l'infinité, tant qu'on cherche pas à calculer sa valeur (qui reviendrait à tourner en rond indéfiniment entre le père et le fils dans ce cas).

    petit exemple :
    en langage Haskell qui est purement fonctionnel

    data noeud = Element noeud

    let rec parent = Element enfant
    and enfant = Element parent


    ce qui en Objective Caml donne plus ou moins :

    type noeud = Element noeud Lazy.t

    let rec parent' rien = Element lazy (enfant ())
    and enfant' rien = Element lazy (parent ())

    parent = Lazy.force parent'
    enfant = Lazy.force enfant'

    Dans le cas d'Objective Caml, on ne peut faire du récursif qu'avec des fonctions, on utilise donc des fonctions qui prement un argument inutile et on leur donne () (l'unité) comme paramètre. Le mot-clef lazy transforme l'expression qui suit en valeur non-évaluée, le mot-clef Lazy.force, calcule l'expression non évaluée passée en paramètre.

    Bien, on peut donc maintenant généraliser :


    (* arbre de départ, qui va être converti *)
    type 'a ltree = LLeaf of 'a | LNode of 'a ltree * 'a ltree
    (* arbre d'arrivée, qui possède des référence enfants->parent *)
    (* solution à parent lazy *)
    type 'a rtree = RRoot of 'a rtree | RNode of 'a rtree * 'a rtree * 'a rtree Lazy.t | RLeaf of 'a * 'a rtree Lazy.t

    let convert t =
      let rec root = lazy(RRoot(tree t))
      and tree t' = go root t'
      and go pn = function
        | LLeaf(v) -> RLeaf(v, pn)
        | LNode(f, s) ->
          let rec top = lazy (RNode(lson f, rson s, pn)) (* l'image du noeud *)
          and lson f' = go top f' (* l'image de son fils gauche *)
          and rson s' = go top s' (* l'image de son fils droit *)
          in Lazy.force top
      in Lazy.force root


    et voiloù, la solution complète ici, avec un exemple et une fonction d'affichage faite par Seb pour vérifier.

    À noter que ce post est un large recyclage d'un post que j'avais fait sur HFR.

    jeudi, juillet 17 2003

    CDuce, langage de manipulation XML

    Un petit langage dans le style ML, destiné à la manipulation de données XML en typage statique. On y trouve un système de types assez poussé, avec polymorphisme, hiérarchie, surcharge de méthodes. A noter qu'il laisse plutôt froid un intégriste du XSLT, pour des raisons non-complètement élucidées (sachant de plus que ce langage n'est pas exactement destiné à remplacer XSLT) une raison est qu'il n'est pas normalisé.
    En gros, vous pouvez faire du XSLT avec mais le but est bien la génération et la manipulation de XML dans tous les sens et non spécifiquement la conversion de document.

    Ce qui ne résout pas l'aspect verbeux et surchagé de XML en général !

    lundi, juillet 14 2003

    Les types

    En glandant sur CiteSeer, je viens de trouver un papier sur la théorie des types, il faudrait que je le lise.
    Je suis à la recherche d'un papier expliquant le système de types F^{omega} et ses potes (et toute la notation qui va avec). Si quelqu'un a ça sous le coude, merci de me le marquer dans les commentaires.


    Au passage, sur la page des documents les plus demandés de CiteSeer (car je pense que ça doit être un pilier de l'info ce document), je suis tombé sur la définition du recuit-simulé. J'avais 4 ans quand le gus a écrit ça !

    jeudi, juillet 10 2003

    sémantique compositionelle

    Un très bon cours, trouvé gràce à google. Le monsieur part des langages naturels et arrive à la logique. Prenez la version images si vous voulez avoir les symboles grecs.

    edit : oulà en fait j'ei décroché au 35, il faut que je reprenne ça plus tard (le temps de décanter le début)

    mercredi, juillet 9 2003

    La preuve

    Il y a des choses comme ça qui me donnent les boules, la concurrence au sein des applications en est un exemple, la recherche sur la preuve avance, on sait que c'est un grand piège (la complexité part vite en vrille, les cas vicieux pullulent et les tests sont largement inutiles) mais les outils de preuve sont assez discrets sur le marché et personne ne les utilises.
    À quel point ? Au point que les mecs de Sun se sont plantés dans un article concernant le multithreading et l'exclusivité d'accès aux ressources. Les 2 erreurs ont été trouvée par preuve formelle.
    Ça confirme ce que pensais : ne fait pas à la main ce qu'une machine fait mieux que toi (la preuve qu'un programme marche), ça peut être dangereux !
    j'ai trouvé ce papier à partir de cet outil (lui-même trouvé à partir du site haskell.org applications-> applications des langages fonctionnels).

    Et dire qu'on développe encore des noyaux (eux-mêmes préemptifs) de système d'exploitation de plus de 100Mo de code en C non prouvé !

    jeudi, juin 26 2003

    Récursivité terminale en O'caml

    Un très bon article (2ème point) dans le caml weekly news de la semaine dernière, qui ne part pas dans un grand discourt théorique, mais donne, en O'caml, structure par structure la position où doit être placé l'appel récursif pour qu'il soit terminal.

    vendredi, juin 20 2003

    Quel chameau ce Seb !

    J'ai reçu un mail de mon pote Seb :

    Ou comment bien se prendre le chou pour calculer la longueur d'un
    tableau connaissant ses limites dans un style Haskélien...

    let ( <*> ) f g x = f (g x) (* Composition de fonctions *)
    let flip f x y = f y x (* Inversion des arguments *)
    let uncurry f (x, y) = f x y (* Décurryfication d'une fonction *)

    let bounds a = 0, Array.length a - 1
    (* Un tableau commence toujours à 0 en OCaml *)

    Et enfin, la formule magique :

    let length a = ((( + ) 1) <*> (uncurry (flip ( - ))) <*> bounds) a

    Car, comme chacun sait, la longueur d'un tableau c'est m - n + 1 où n
    et m sont les indices extrêmes dans l'ordre. Limpide, non ?

    Et voilà,

    Seb.

    P-S : Question pour voir si vous suivez (vous ne pensez tout de même pas
    que j'allais vous lâcher comme ça) : Pourquoi n'écrit-on pas "uncurry
    <*> (flip ( - ))" à la place de "uncurry (flip ( - ))" ?


    J'ai bien galéré 2 heures dessus (en tentant de réécrire <*> mais c'était de toute façons vicié) !
    première chose, vérifier qu'il a bon, un peu de réecriture pour virer les opérateurs relous :

    let minus = ( - );;
    let plus = ( + );;
    let plus_one = plus 1;;

    la nouvelle définition devient :

    let length a = (plus_one <*> (uncurry (flip minus)) <*> bounds) a

    en développant le <*> gauche :
    let length a = plus_one (((uncurry (flip minus)) <*> bounds) a)
    puis, celui qui reste :
    let length a = plus_one (uncurry (flip minus) (bounds a))
    on développe le bounds:
    let length a = plus_one (uncurry (flip minus) (0, Array.length a - 1))
    on développe le uncurry (en faisant gaffe à remettre des parenthèse en virant la virgule) :
    let length a = plus_one ((flip minus) 0 (Array.length a - 1))
    on développe le flip :
    let length a = plus_one (minus (Array.length a - 1) 0)
    on développe le minus et on vire les parenthèses inutiles (associativité du -) :
    let length a = plus_one (Array.length a - 1 - 0)
    on simplifie :
    let length a = plus_one (Array.length a - 1)
    on développe le plus_one :
    let length a = 1 + (Array.length a - 1)
    on bidouille et on a bien :
    let length a = Array.length a

    Bravo Seb !

    Pour la question :
    (uncurry (flip moins)) (x, y)
    devient :
    uncurry (flip moins) (x, y)
    qui devient :
    (flip moins) x y
    qui devient (parenthèses inutiles) :
    moins y x
    donc :
    y - x

    alors que
    (uncurry <*> (flip moins)) (x, y)
    devient :
    ((flip moins) (x, y))
    et ça le vérifiateur de type, il aime pas.
    Je développe un peu :
    flip renvoie un 'a alors que uncurry veut un couple 'b*'c donc uncurry trouvera pas de couple à éclater.
    de même, l'écriture tente d'appliquer le couple (x, y) au deuxième argument du moins, il sait rien en faire !

    edit :
    réponse de Seb face à cette entrée : " Ouais, c'est bien expliqué, bravo !"

    - page 1 de 2