[Atom] [Mail] [Twitter]
Liens : git · hacks · divers · cabale · planète · à propos +
Au menu
\/

Cohérence des effets de bord

#. Par bluestorm dans Réactions. Publié le 22.01 2010 à 22:59. 4 commentaires.
<. Détection automatique de bugs
effets_de_bord langages réaction

Ce week-end, je me suis promis que je rédigerai, pour le prochain article que je lirai en entier, un court résumé de deux ou trois paragraphes, pas plus, pour cesser d’écrire des réactions à la longueur démesurée. Voici le résultat.

Coherent Reaction (pdf, 8 pages) est un papier de Jonathan Edwards (pas le prédicateur américain), un type intéressant dont le blog s’appelle Alarming Development (Dispatches from the Programmer Liberation Front). C’est aussi le créateur du langage de programmation visuelle Subtext.

Problématique

En travaillant sur Subtext, il s’est rendu compte que l’ordonnancement des effets de bords pose problème dans un programme interactif (avec une interface utilisateur, comme par exemple des formulaires Web). Son exemple, dans un cadre MVC classique, est le suivant : imaginez qu’on travaille sur trois valeurs, qui pour avoir du sens doivent vérifier une contrainte de correction commune (par exemple la troisième, 5, doit être la somme des deux premières, 2 et 3). C’est le contrôleur qui vérifie ces contraintes : il dispose pour chaque valeur d’une fonction de vérification.

Maintenant, si on change deux de ces valeurs en même temps (la première à 4 et la troisième à 7), il y a risque de "fausse erreur" si les vérifications ne sont pas faites dans le bon ordre : si on modifie la première valeur à 4, puis on vérifie, on a une erreur (la contrainte n’est plus vérifiée, car 4 + 3 ne vaut pas 5) alors que si on avait modifié les deux ensemble tout se serait bien passé.

Il y a aussi des situations ou l’ordre de modification est important : fondamentalement, on voudrait que chaque modification s’effectue en parallèle, indépendamment, mais les langages impératifs effectuent les effets les uns après les autres, donc il y a toujours un risque d’interférence.

Les solutions classiques proposent que le modèle donne plus d’informations sur les contraintes liées au données : quelles sont les données liées, par exemple, pour qu’on les modifie toujours toutes en même temps au lieu d’une par une. Leur inconvénient, d’après l’article, est une perte en modularité, puisque cela révèle une partie de l’implémentation du modèle.

Coherent Reaction

Pour Edwards, le problème de fond est la difficulté à coordonner les effets de bords dans les langages impératifs classiques : plus les contraintes sont complexes, plus elles sont difficiles à gérer et donnent lieu facilement à des erreurs de programmation difficile à repérer. Il propose de libérer le programmeur des questions de coordination avec sa notion de cohérence, qui est une forme d’atomicité : le programmeur ne précise pas dans quel ordre faire les modifications, et c’est le programme qui le découvre par essais/erreurs : si des modifications produisent un état inconsistent ou introduisent des interférences, on les annule (rollback) pour les recommencer dans un ordre différent.

Il a aussi conçu, comme expérience pratique, un langage de programmation basé sur cette idée. C’est un langage dynamique centré sur les données (plutôt que les comportements, comme la POO), mettant en avant deux concepts :

dérivation

une façon de déduire des champs du reste de l’objet : on peut dire par exemple qu’un champ c dérive de la somme de deux champs a et b. Si l’un de ces champs est mis à jour, le champ c sera modifié en conséquence, pour maintenir cet invariant.

réaction

propagation de modifications dans le sens inverse de la dérivation : une réaction est une sorte de callback qui est exécuté quand le champ auquel elle se rattache est modifié

Ces deux mécanismes de transmission des modifications permettent de maintenir simplement l’état des données; pour les combiner, c’est le programme qui se charge de trouver un ordre des dérivations/réactions qui évite les interférences.

Remarques personnelles

J’ai apprécié cet article, mais peut-être un peu déçu : pour la réinvention des effets de bord, je m’attendais à quelque chose de plus profond. La discussion des travaux similaires, étendue dans la version longue du papier, est très intéressante.

Je ne suis pas tout à fait convaincu que cette idée peut être reprise dans le cadre d’un langage généraliste, mais elle pourrait être appropriée comme paradigme spécialisé des parties interactives d’un programme. La comparaison avec la programmation fonctionnelle réactive serait certainement intéressante, mais c’est un sujet que je ne connais pas (bien que j’envisage de m’y intéresser prochainement) donc je ne pourrais pas en dire plus.

Enfin, l’article se termine quasiment par un paragraphe, en réaction à une citation très classique d’Alan Kay, qui me semble tout à fait obscur. Je ne pense pas le comprendre, c’est bien trop flou/littéraire/psychologique pour moi, mais je pense qu’il pourrait intéresser certains lecteurs :

Smalltalk’s design—and existence—is due to the in- sight that everything we can describe can be repre- sented by the recursive composition of a single kind of behavioral building block that hides its combina- tion of state and process inside itself and can be dealt with only through the exchange of messages.
– Alan Kay : The early history of smalltalk

The conceptual model of Coherence is in a sense opposite to that of Object Oriented languages. As Alan Kay’s quote above indicates, the central metaphor of OO is that of mes- saging: written communication. The central metaphor of Co- herence is that of observing a structure and directly manip- ulating it. These two metaphors map directly onto the two primary mechanisms of the mind: language and vision.

[ tag:blog.huoc.org,2009:bluestorm/9 ]
Voir les commentaires · Commenter

/\ \/

En bref aussi

#. Par bluestorm. Publié le 27.11 2009 à 03:09. 8 commentaires.
coq images réaction typage

Les temps sont durs, et le temps nous manque…
Mais nous sommes toujours là ! bluestorm et moi. —rz0

Comme rz0 l’a admirablement fait remarquer, le blog est plutôt mort ces temps-ci. Sans surprises, je suis occupé depuis le début de l’année, et le temps pour poster des billets en a diminué d’autant. Je me suis fixé l’objectif de ne pas passer plus d’une heure à écrire ce billet (écriture seulement, il y aura sans doute de la relecture ensuite), et ça me paraît un pari assez raisonnable, bien que nettement plus ambitieux que les billets précédents. La qualité en souffrira certainement, mais j’essaie de trouver un équilibre.

Je remarque d’ailleurs que je ne suis pas le seul à être occupé : tous ces gens qui avaient promis juré que oui, ils feraient des efforts pour poster des commentaires de temps en temps, histoire qu’on conserve la motivation, ils ont visiblement un travail fou. Tant pis ; de toute façon je n’ai pas les moyens (temps, motivation et envie) d’écrire de gros billets bien préparés en ce moment. Si j’écris celui-ci, c’est parce que j’ai honte de laisser cet espace tellement vide alors que rz0 a fait tant d’effort pour m’installer un petit coin bien douillet. Et j’espère aussi que ce que j’ai à vous raconter pourrait vous intéresser.

Débuts en Coq

Cette année, j’ai décidé de me lancer dans le vaste monde des assistants de preuve. En un mot, ce sont des programmes qui permettent d’écrire des preuves mathématiques, et qui les valident : ils savent dire si oui ou non, une preuve est correcte.

Je leur vois deux intérêts principaux. D’une part, ils permettent de se redonner confiance en mathématiques ; si vous connaissez le malaise de celui qui écrit un brouillon de preuve, mais sans savoir vraiment si elle est juste ou contient une erreur désagréable que vous n’aviez pas vue, vous comprendrez ce que je veux dire. Plus généralement, c’est un peu comme la différence, en informatique, entre écrire du pseudo-code, et écrire dans un vrai langage de programmation : l’avantage d’un vrai langage, c’est que vous pouvez exécuter le programme pour vérifier qu’il fonctionne (et en particulier vérifier sa syntaxe, etc. automatiquement), alors qu’on est rarement motivé pour relire avec soin un pseudo-code. L’ordinateur apporte un retour d’expérience (feedback) direct, impartial et incontestable.

D’autre part, les assistants de preuve sont essentiellement des langages de programmation. Ils se placent en effet à cheval entre la théorie des types (côté informatique) et la théorie de la démonstration (côté mathématiques) : c’est essentiellement le même domaine, vu de points de vue différents. On parle souvent de l’isomorphisme de Curry-Howard, mais ce théorème précis concerne des systèmes bien précis, et l’étendue de la correspondance est en fait beaucoup plus large ; fondamentalement, depuis le début du 20e siècle, on pense les preuves formelles comme des programmes (fonctionnels). Ces outils font donc de bons mélanges entre des preuves très formelles et des langages très fortement typés, et ça a tout pour me plaire. On peut d’ailleurs aussi utiliser ces systèmes pour prouver des choses sur les langages de programmation (tous paradigmes confondus, cette fois-ci).

Il existe plusieurs systèmes d’assistants à la démonstration. Les plus utilisés actuellement sont Coq et Isabelle (et éventuellement Twelf et HOL-Light, avec aussi des anciens couverts de lauriers (Mizar), et de jeunes qui explorent l’espace de recherche (Agda)). Ces systèmes ont des qualités et des défauts variés. J’utilise Coq. D’une part parce que c’est le système conçu en France, et donc je suis entouré de gens qui l’utilisent, l’enseignent, voire le développent.α D’autre part, j’utilise pour apprendre les bases le livre en ligne Certified Programming with Dependent Types, qui conseille d’utiliser Coq et qui a certainement ses raisons, moi, je ne connais pas assez le domaine pour faire un choix informé.

Ces outils sont majoritairement codés dans un des langages dont le nom contient « ML » et ne commence pas par « X ». C’est une corrélation historique, puisque le langage ML (notre grand-père à tous) a été créé conjointement à l’assistant de preuve LCF.

α : J’ai passé un été à faire de l’OCaml dans le même bureau qu’un des développeurs actuels de Coq. On se sent bien encadré.

β : Encore par patriotisme : son auteur, Adam Chlipala, est un citoyen émérite du canal irc #ocaml.

Au passage, si vous envisagez vous-même de vous intéresser un jour au sujet, je suis assez content de ce bouquin. En particulier, il prend surtout ses exemples dans le monde des langages de programmation : dès le premier chapitre, on compile des expressions arithmétiques simples vers une machine à pile simple, en rajoutant du typage à gogo, et sera donc plus accessible qu’un livre orienté vers la logique formelle si vous vous sentez plutôt informaticien. Une connaissance (au moins des bases) de la programmation fonctionnelle est tout de même indispensable.

Il est temps de signaler les deux articles qu’asmanur a écrit comme une petite introduction à Coq (un passe-temps qu’il a certainement abandonné assez vite, comme tous les autres) : Voyage au bout de la logique, et sa deuxième partie. Si vous vous intéressez à la discussion sur le choix de l’assistant à la démonstration, vous pouvez aussi lire l’introduction du livre de Chiplala. Au passage, le mode Emacs qu’il recommande, Proof General, est vraiment très bien.

Programmation par contraintes

Cette fois-ci, cela tient plus de l’anecdote, mais je trouve ça assez amusant pour avoir envie de vous en parler. En plus de la logique formelle fonctionnelle typée et des hautes sphères éthérées de la théorie des catégories, je fais cette année des choses bassement matérielles qui demandent de la programmation. Évidemment, je fais tout en OCaml.

Depuis cet été, je suis assez séduit par les types fantômes. C’est un outil vraiment puissant et agréable, à tel point que je suis tenté d’en mettre un peu partout dans mes divers projets. Par exemple, je dois coder un projet de bataille navale en réseau (et d’ailleurs c’est assez ennuyeux ; je prends sur mon temps de développement pour vous écrire cet article). Ça a l’air assez trivial, mais j’ai perdu tellement de temps à refactoriser dans tous les sens pour avoir le typage le plus expressif possible que j’en suis venu à m’auto-imposer la règle suivante : « Interdiction d’utiliser les types fantômes ! »

Et vous, avez-vous des règles personnelles que vous appliquez avec cruauté, pour éviter de perdre votre temps dans des considérations, certes intéressantes, mais déraisonnables pour le projet précis sur lequel vous travaillez ?

J’en profite pour vous signaler que j’étais passé à côté jusqu’à présent, mais j’ai découvert récemment que la bibliothèque standard OCaml utilise elle aussi, des types fantômes, ma bonne dame : ça se passe dans le module le plus haut niveau de la bibliothèque, Bigarray.

Traitement d’images

Je suis, par un curieux hasard de circonstances, un cours de traitement d’images. C’est un sujet très éloigné de mes centres d’intérêt habituels, mais je ne le regrette pas. Je vois défiler beaucoup de jolies photos, et de méthodes pour les améliorer encore, parfois franchement inventives. J’ai un professeur formidable, qui est un mordu de la photographie, mais aussi un amateur de l’analyse de Fourier. Sans entrer dans les détails pénibles, il nous fait des démonstrations « avec les mains » qui sont parfois assez sympathiques, et donnent un petit coup de jeune à toutes ces choses qui sombrent lentement dans l’oubli quand on ne les utilise pas.

Au cas où ça vous intéresse, voici un exemple d’algorithme que nous avons étudié : Colorization Using Optimization. Il s’agit de donner des couleurs à des images en noir et blanc, à partir de quelques indications de l’utilisateur (des traits de couleur connue). L’idée de base, c’est de supposer que deux pixels de luminosité proche doivent être de couleur proche. On peut alors exprimer la teinte de chaque pixel en fonction de celle de ses voisins et, combiné aux conditions initiales de l’utilisateur, ça donne un gros système linéaire (une grosse matrice) qu’on fournit à une fonction de résolution et pouf, une image en couleur. Ça marche étonnamment bien.

Pour ce cours, j’ai participé à l’écriture de bindings OCaml vers des bibliothèques C (ou interfacées par l’intermédiaire du C), par exemple OpenCV ou SuiteSparse. C’est très drôle, on retrouve la joie du segfault (et c’est encore mieux à plusieurs). Je ne m’étais jamais vraiment plongé dans l’interface C-OCaml, et je peux vous dire que ce n’est pas inoubliable mais ça reste une expérience amusante. Cela m’a d’ailleurs permis de mettre mon petit grain de sel dans un tutoriel de Cacophrène à ce sujet sur le SdZ. Ça fait toujours une introduction en français à ce sujet, qui peut servir de mise en bouche, mais si un jour vous voulez vraiment coder quelque chose je vous conseille ensuite de lire conjointement le manuel officiel pour les idées, et le tutorial en anglais de François Monnier pour les exemples de code. Attention au DA-Ocaml, sur ce point il est un peu vieux.

Une petite réaction pour finir ?

Que serait mon billet mensuel sans une référence, ou du moins une allusion, à un petit papier à se mettre sous la dent ? J’en ai survolé une bonne poignée depuis la rentrée, mais les derniers que j’ai croisés sont des articles de Gilad Bracha. C’est un type qui a commencé ses exploits en codant un système Smalltalk avec annotations optionnelles de typage très performantγ, pour aller ensuite réutiliser ses compétences en travaillant sur Java, sa spécification et sa VM chez Sun pendant quelques temps, avant de repartir construire un nouveau langage dynamique, Newspeak.

γ : Il a travaillé avec les gens de Self, qui ont inventé le concept de JIT et toutes les optimisations méchantes de langage dynamique

J’ai jeté un coup d’oeil à la bête. Comme vous imaginez, les langages très dynamiques ne sont pas forcément ceux qui me tentent le plus, mais ça fait du bien de voir ce qui se fait ailleurs de temps en temps. J’ai aussi lu deux de ses papiers au hasard : Executable Grammars in Newspeak, et Pluggable Type Systems.

Le premier papier présente une façon possible de créer une bibliothèque de parsing combinators dans un langage dynamique. Comme l’annonce l’auteur d’entrée de jeu, c’est plutôt le point fort des langages fonctionnels (l’étalon dans le domaine étant le très bon Parsec de Haskell). Il promet que les fonctionnalités nouvelles de son langage, ainsi que son fort dynamisme, permettront d’en faire une traduction « objet dynamique » adaptée. Je n’ai pas été émerveillé par le résultat (en particulier, comme toutes les approches « DSL internes au langage », la syntaxe n’est au final pas si géniale que ça), mais ça reste une lecture intéressante. Il discute aussi de la facilité ou non à typer ces opérations dans un langage statique, mais le référentiel semblant être celui de Java, on en apprend plus sur l’état des systèmes de types mainstream que sur le fond du problème.

Le deuxième article n’est pas vraiment un article scientifique, mais plutôt une prise de position de l’auteur. Il voudrait qu’on envisage les systèmes de type non pas comme une donnée fondamentale du langage de programmation, à laquelle tout programme dans ce langage doit se soumettre, mais comme une étape supplémentaire, optionnelle. C’est une idée très séduisante, mais assez délicate à mettre en place en pratique (la plupart des avantages théoriques des systèmes de types actuels venant justement de leur capacité à (presque) tout contrôler).

Il voudrait aussi que l’inférence soit optionnelle/désactivable, en un mot « pluggable », ce qui est un point de vue intéressant (et trouve des échos chez les théoriciensδ). Il voudrait même pouvoir faire cohabiter plusieurs systèmes d’inférence pour « choisir le meilleur selon les situations », et là je ne suis franchement pas convaincu : ça mène à une situation où le type repose sur beaucoup d’heuristiques diverses que le programmeur ne maîtrise pas en général, et cela pourrait à mon avis poser plus de problèmes que ça n’en résoud. Ça reste une lecture intéressante, c’est agréable de voir que les universitaires grisonnants ne sont pas les seuls à se poser la question.

Pour amadouer mes lecteurs potentiels les plus revêches, je dois signaler que cet article cite explicitement les essais d’ajout (partiel) d’un typage statique au langage Erlang.

δ : Pour les systèmes de type « partiels » ou désactivables, on pourra regarder par exemple du côté du Blame Calculus de Wadler.

P.S. : Mon pari de temps n’a pas été tenu, j’ai plutôt mis une heure et demie, mais c’est toujours agréable d’écrire. Ma bataille navale avance assez bien : les clients peuvent se connecter, lancer des parties et placer leurs bateaux, et je vais bientôt être obligé de coder la partie où ils tirent les uns sur les autres. Heureusement, j’ai glissé des variantes polymorphes pour que le type des fonctions reflète le protocole de communication.

[ tag:blog.huoc.org,2009:bluestorm/7 ]
Voir les commentaires · Commenter

/\ \/

Détection automatique de bugs

#. Par Cygal dans Réactions. Mis à jour le 01.03 2010 à 23:29. 7 commentaires.
<. Histoire de sexe et d'informatique
>. Cohérence des effets de bord
analyse_statique bugs inférence réaction

L’Ours & Hippy crew vous présente son second billet feat. Cygal. Sous le regard hippie de blueblue aux commandes des platines d’édition — que l’on reconnaît grâce au manque d’originalité de son titre frigido-sérieux — l’artiste nous a concocté en direct de derrière le studio un nouvel article…
Qui nous parle du chant des insectes déviants,
Leurs pesticides heuristiques,
Et ses recettes hygiéniques,
À inculquer absolument aux ptits enfants !

—minh

Cette réaction traite d’un point qui va finir par ressembler à une obsession de ma part : la correction de bugs. Plus précisément, ce papier traite de la détection automatique de bugs. Le but est d’obtenir du programme assez d’informations pour pouvoir remarquer des comportements anormaux.

☃ : Alors que pas du tout, je suis encore à la recherche d’un fétiche.

C’est d’une certaine manière ce qu’apporte le typage à la compilation : les types apportent suffisament d’informations pour pouvoir détecter un certain nombre d’erreurs avant qu’elles n’apparaissent à l’exécution. L’analyse qui est faite ici est aussi statique, et se base sur un principe général qui peut s’appliquer à un nombre étonnament varié d’erreurs possibles. Les auteurs du papier ont ainsi détecté des centaines de bugs dans les noyaux Linux et BSD, principalement dans des drivers pas toujours maintenus.

Des langages comme OCaml sont capables d’inférer les types utilisés dans un programme, c’est-à-dire les deviner à partir du code. Le principe est ici le même, sauf qu’on essaie d’inférer du sens pour nous aider à trouver des erreurs.

Présentation du papier

Les exemples donnés ici sont directement tirés du papier : Bugs as deviant behavior (PDF, 16 pages).

Le principe est donc d’inférer le maximum d’informations possible dans chaque fonction, et d’essayer de détecter les contradictions présentes. L’existence d’une contradiction entre deux lignes montre qu’il y a forcément une erreur, même si le système ne peut pas forcément savoir de quelle ligne il s’agit.

Le montant d’informations qu’il est possible d’obtenir à partir d’un code source automatiquement est non seulement limité, mais on doit parfois se baser sur des informations peut-être fausses. C’est pourquoi j’ai séparé la présentation en deux courtes parties :

  • une sur ce qui est appelé les « MUST beliefs » dans le papier (on est certain qu’il y a une erreur) ;
  • et une autre sur les « MAY beliefs » (il y a très probablement une erreur).

MUST beliefs

Ainsi, prenons un exemple simple, tiré d’une version du noyau Linux aussi vieille que le papier.

/* 2.4.7 drivers/char/mxser.c */
int mxser_write(struct tty_struct *tty, ...) {
  struct mxser_struct *info = tty->driver_data;
  unsigned long flags;

  if (!tty || !info->xmit_buf)
    return(0);
  ...

Le bug ici vient du déréférencement du pointeur tty alors qu’il pourrait valoir NULL. C’est quelque chose qui peut arriver assez vite si l’on ni porte pas attention, par exemple pendant l’évolution d’un code source. Regardons comment notre programme fonctionne dans ce cas précis.

*info = tty->driver_data
Le pointeur tty est déréférencé, ce qui veut dire que le programmeur estime qu’il ne peut pas être nul.
if (!tty || !info->xmit_buf)
La nullité du pointeur tty est testée, ce qui veut dire que le programmeur estime qu’il peut être nul.

On a donc une contradiction : l’une des deux lignes pose problème, ce qui être un bug. Et en effet si le pointeur peut être nul, il est dangereux de le déréférencer.

En pratique, il y a beaucoup trop de faux positifs. Par exemple, les macros vérifient souvent que leur arguments ne sont pas nul, et une fois que leur expansion est faite, on ne fait pas la différence avec le code normal. Les auteurs ont donc modifié leur programme (basé sur GCC apparement) pour annoter les expansions de macros, et ne pas les traiter pendant l’algorithme. D’autres problèmes ont été traités pour pouvoir montrer de réelles erreurs, je vous laisse lire le papier si ça vous intéresse.

MAY beliefs

Dans l’exemple sur les pointeurs donné ci-dessus et quelques autres, le programme est capable de dire avec certitude qu’il y a un problème. Cependant, ce n’est souvent pas possible de l’affirmer avec certitude : les informations inférées lors de la lecture du code ne sont pas forcément exactes. Ce n’est pas une raison de les ignorer, mais il faut les traiter avec prudence. L’idée est alors de rassembler un maximum de cas d’utilisations similaires, et d’en déduire les cas érronés. Par exemple, si une fonction est utilisée 999 fois d’une certaine manière, et autrement ailleurs, on peut supposer que le cas seul est érroné.

Donnons encore une fois un exemple pour expliquer cette pratique.

lock lk;            // Lock
int a, b;           // Variables potentially
                    // protected by l
void foo() {
       lock(lk);    // Enter critical section
       a = a + b;   // MAY: a, b protected by l
       unlock(lk);  // Exit critical section
       b = b + 1;   // MUST: b not protected by l
}
void bar() {
       lock(lk);
       a = a + 1;   // MAY: a protected by l
       unlock(lk);
}
void baz() {
       a = a + 1;   // MAY: a protected by l
       unlock(lk);
       b = b - 1;   // MUST: b not protected by l
       a = a / 5;   // MUST: a not protected by l
}

Supposons que lock() soit un appel système qui existe, et qui bloque l’accès aux données du noyau. On suppose donc que toute variable modifiée entre deux locks peut être concernée par ce lock. Une fois que l’on sait à peu près quelles sont les chances estimées que la variable soit concernée par le lock lk, on peut dire à l’utilisateur à quel point une utilisation en dehors du lock peut être dangereuse.

‽ : Pour information, le Big Kernel Lock a été créé, puis est enlevé petit à petit.

Ici, la fonction foo nous permet de dire que a est sûrement concerné par le lock lk, et que b peut l’être (mais moins de chance, vu que l’accès ne se fait qu’en lecture, non ?). La fonction bar montre que a est la seule variable utilisée dans le lock, donc il commence à y avoir de grandes chances que ce soit effectivement a qui est protégé. Dans baz, a est utilisé en dehors d’un lock, donc c’est certainement un bug qu’il faut corriger !

Cette manière d’essayer de deviner des informations plausibles pour ensuite essayer de détecter des erreurs a elle aussi de nombreuses applications décrites dans le papier. Certaines ne sont que des idées placées en plein milieu sans aucun rapport : d’ailleurs le style du papier laisse à désirer, le tout n’est pas très structuré, soyez avertis si vous comptez le lire (sachez aussi que je ne prétends pas faire mieux :). Une de ces idées consiste en la vérification de la cohérence d’une API proposée d’une version stable à une autre. Si on compare les croyances obtenues par notre système entre deux versions stables, elle doivent être exactement les mêmes. Sinon cela implique une possibilité de casser les applications utilisant l’API en question. Cela peut être aussi trouvé via l’utilisation de tests (potentiellement unitaires), mais c’est plus compliqué à mettre en place : un effort de la part du programmeur est impératif, et un test doit être exécuté, contrairement à l’analyse qui est ici statique.

N’hésitez pas à lire le papier si ces quelques lignes ne vous ont pas endormies : un certain nombre d’autres techniques intéressantes pour par exemple :

  • inférer le maximum d’information d’un code (notamment avec le nom des fonctions) ;
  • vérifier que la gestion d’erreurs d’un programme est efficace ;
  • que les pointeurs sont utilisés correctement dans le kernel (différence entre kernel pointer et user pointer) ;
  • détecter des fuites mémoires ;
  • etc.

Enseignements à tirer ?

Pour m’être amusé pendant tout un été à corriger des bugs, je commence à avoir une idée des pratiques à éviter ou au contraire à encourager pour éviter l’apparition de bugs. Le principe de base de ces pratiques est d’apporter le maximum d’informations à celui qui va lire le code (sans parler des commentaires). Ainsi, si un programme est capable de penser que telle façon de faire quelque chose est une erreur, c’est que certainement un humain qui va passer par là sans savoir comment fonctionne le code va aussi penser à une erreur, ou du moins ne va pas comprendre au premier abord ce qui se passe.

C’est l’origine de plusieurs pratiques couramment encouragées en programmation. Citons ici KISS et la minimisation des effets de bords. La première permet à tout le monde de mieux comprendre un code, de mieux comprendre ce qu’il fait exactement pour pouvoir l’utiliser et le modifier la conscience tranquille.

La seconde permet de pouvoir utiliser une fonction sans avoir peur qu’elle casse autre chose ailleurs. Ça permet aussi d’utiliser de la composition de fonction sans risque. Il y a beaucoup de choses à dire sur cette pratique, mais retenez que l’intérêt principal est de ne pas cacher les canaux par lesquels transite l’information pour rendre le code le plus compréhensible et facile à modifier possible. Moins le programmeur qui lit le code a de chances d’être surpris, moins il y a de chances d’avoir des problèmes par la suite.

Ainsi, certains 'design patterns' ont beau résoudre des problèmes, ils ajoutent souvent une couche d’abstraction qu’il faut pouvoir traverser pour comprendre exactement ce qui se passe. Cette couche empêche l’échange d’informations entre deux parties du code qui intéragissent pourtant directement, ce qui provoquera forcément des bugs, qui auraient pu être détectés autrement. De la même manière, trop de généricité empêche une vision claire du code qu’on a sous les yeux. Cela a causé certains problèmes qui sont devenus tellement fréquents qu’il a fallu inventer d’autres design patterns pour les éviter. L’important est d’en être conscient et d’ajouter d’autres mesures de protection quand le bon sens où le compilateur ne peut plus aider à filtrer les erreurs.

De la même manière, utiliser son langage de manière idiomatique et rester consistant dans son formattage, sa façon de penser et les techniques utilisées est important. Cela permet au lecteur de retirer beaucoup d’informations rapidement, et donc de pouvoir se concentrer sur les points importants.

Si le programme a cru détecter une erreur qui n’était en fait qu’un faux positif, il reste néanmoins important d’essayer de corriger le code : tous les humains qui le liront risqueront de faire la même erreur et de ne pas comprendre ce qui se passe. Dans ce sens là, les exemples présents dans le papier sont instructifs : se sont des erreurs qui peuvent sembler anodines mais qui restent importantes.

[ tag:blog.huoc.org,2009:bluestorm/3 ]
Voir les commentaires · Commenter

/\ \/

Histoire de sexe et d'informatique

#. Par Cygal dans Réactions. Publié le 27.08 2009 à 12:06. 5 commentaires.
<. L'innocence est un mythe : les programmes fonctionnels nécessairement impurs
>. Détection automatique de bugs
femmes histoire informatique réaction

Comme me l’a encore récemment prouvé le Google Summer of Code, et surtout sa hum, superbe, liste de diffusion, où, noyés parmi cinquante messages dont je n’avais aucune utilité (et un petit e-mail vital), on pouvait trouver un vieux troll sur le sexisme, voici un thème qui est plus que jamais sensible dans le monde de l’informatique, et errr, je parie un sucre que quelqu’un va se sentir offensé par mon titre tout meugnon. :]

Bien sûr, cette réaction n’est pas de moi. Et surprise, elle n’est pas de blueblue non plus ! Cygal est dans la place ! Faut que l’on arrête le rap français, ici.

—minh

Cette réaction traite d’un point récurrent dans l’informatique en général et dans le monde du libre en particulier : la minorité parfois écrasante de la gent féminine. Nous ne discuterons pas ici de toutes les raisons qui me font penser que c’est une mauvaise chose qu’il faut enrayer, bien que ça puisse être intéressant, mais nous contenterons d’une analyse de cet article par rapport à la littérature existante sur le sujet.

La présence des femmes dans l’informatique est un sujet qui a déjà été maintes fois traité. Ce papier là est original dans le sens où il n’essaye pas de traiter le problème, et ne le présente pas seulement via des statistiques. Il essaie d’expliquer de manière historique pourquoi les femmes sont sous-représentées dans l’informatique en général. C’est une analyse courte et peut-être simpliste mais les explications données sont crédibles.

Petit résumé du papier

Contrairement aux autres disciplines scientifiques, l’informatique n’existait pas avant la Seconde Guerre Mondiale. C’était donc la porte ouverte aux femmes qui ont été embauchées dans le domaine, et se sont avérées très douées, selon l’auteur. Après la guerre, les hommes ont « récupéré leurs places » dans toutes les disciplines qu’ils occupaient avant la guerre. L’informatique n’existant pas avant, les femmes n’avaient pas de places à laisser.

Cependant, la pression sociale a fait qu’une majeure partie est retournée à des occupations considérées comme acceptables à l’époque. Celles qui sont restées dans de grandes compagnies étaient discriminées et devaient repousser les avances de leur collègues. Ce fut la première vague de déféminisation. De manière générale, le fait que le rôle traditionnel des femmes soit d’élever ses enfants n’a pas aidé, dans aucune de ces vagues.

La deuxième vague s’est effectuée quand l’apprentissage de l’informatique a été couplé à celui de l’ingénierie électronique, domaine presque exclusivement masculin, ce qui n’a pas encouragé les gens à s’intéresser à l’informatique.

La troisième vague s’est déroulée lors de la démocratisation de l’informatique. Les jeux ont toujours été faits pour et par des hommes, ce qui a fini par ancrer dans les esprits que « l’informatique c’est pas pour les filles ».

Un peu plus loin ?

Il n’est pas rare dans les domaines scientifiques de voir les filles traitées différemment, voire rejetées, parfois inconsciemment.

En informatique, c’est particulièrement vrai puisque les femmes et les hommes estiment que l’informatique n’est pas faite pour les femmes. Elles ne se disent pas intéressées et préfèrent choisir d’autres voies. Le fait qu’elles soient sous-représentées à ce point ne leur donne pas envie de venir. Si vous êtes un homme, imaginez ce que ça fait de rentrer dans un salon de manucure rempli de femmes. Vous ne vous sentirez pas à l’aise.

Cette analyse me semble intéressante parce qu’elle est courte mais semble expliquer un certain nombre de choses. Il y a sûrement d’autres points qui expliquent la situation, c’est pourquoi je vous propose un certain nombre de lectures complémentaires en annexe. Elles ne sont pas toutes centrées sur l’informatique en général, mais semblent intéressantes. L’article Wikipédia vous permettra de pousser plus loin.

Liens

[ tag:blog.huoc.org,2009:posts/27 ]
Voir les commentaires · Commenter

/\ \/

L'innocence est un mythe : les programmes fonctionnels nécessairement impurs

#. Par bluestorm dans Réactions. Publié le 21.08 2009 à 16:49. 8 commentaires.
<. bluestorm, Emily, et les chameaux
>. Histoire de sexe et d'informatique
caml expressivité fonctionnel langages programmation pureté réaction

Tut tut tut, deuxième réaction de bluestorm dans les bacs. Après ses aventures avec la petite Emily, notre héros nous revient en terrain familier et, par ce discours nouveau, introduit la jeunesse à une des dures réalités de la vie d’un guerrier fonctionnel, dans sa quête éternelle de la meilleure conception, chassant les belles factorisations… tak tak.

Mais je vais éviter de m’imposer davantage sur son billet de peur de le vexer, puisque j’ai déjà pris la liberté d’amender son titre originel tout meugnon.

—minh

Cette réaction n’est pas la deuxième sur ma liste : c’est la quatrième, et je l’ai choisie parce que c’est un sujet qui me tentait plus. C’est un papier sur les programmes et langages de programmation, qui repose sur un arrière-plan théorique complètement inaccessible (pour moi en tout cas), mais qui cache toutes ces difficultés et qui est orienté vers une certaine forme de pratique.

Le papier explore un fait qu’il considère comme « surprenant » : il existe des fonctions qui respectent la transparence référentielle, mais qu’on ne peut pas écrire dans un langage de programmation pur ; autrement dit, il parle de la programmation fonctionnelle qu’on ne peut faire que dans un langage (un peu) impératif. Il donne des exemples de ces fonctions, se demande à quoi elles peuvent bien servir, donne des cas d’utilisation et discute de leurs avantages et inconvénients par rapport aux fonctions écrites dans un langage pur, d’une part, et aux fonctions générales d’autre part.

Il aborde aussi de façon relativement tangente deux points que je trouve intéressants :

  1. parfois, pour qu’une fonction ait de bonne propriétés, il faut lui faire perdre une partie des informations qu’elle donne, en fournissant pour cela un travail supplémentaire ;

  2. l’écriture dans un style fonctionnel pur peut poser des problèmes de modularité.

When is a functional program not a functional program
Citeseer (PDF, PS), 7 pages
John Longley

Ce n’est pas un papier révolutionnaire dans le sens où les techniques présentées ne changeront pas fondamentalement votre vision de la programmation, mais ça reste une lecture agréable et à placer dans la catégorie "exercices mentaux de programmeurs ML".

Le reste de cette réaction est une explication plus détaillée et plus verbeuse de tous les points que j’ai évoqués jusqu’ici. C’est donc une paraphrase de l’article, avec des choses en moins, mais je pense que c’est plus accessible : ce qui dans l’article constitue une petite allusion d’une ligne (« the familiar recursive type », « in the obvious way », « in the usual sense »…) sera un peu plus détaillé et donc normalement plus accessible au lecteur qui ne connaît pas le sujet, même si je ne ferai sans doute pas de miracles. En pensant aux gens qui n’ont pas envie d’entrer trop dans les détails, j’ai retiré une partie de mes digressions et les ai mis en annexe ; ça casse un peu le flot naturel du discours, mais vous pouvez très bien ne pas les lire, ou les lire seulement à la fin si vous voulez des détails. C’est même ce que je vous conseille : lisez-les quand vous aurez terminé le corps de l’article et si vous en avez encore envie, c’est le plus simple.

Par contre, je donne des exercices dans ce billet. Si cela peut vous motiver, au milieu de mes explications à la noix vous trouverez des énoncés d’exercices pas tous évidents, qui pourront vous faire un petit challenge matinal en mangeant vos chocapics (ou pas).

Des fonctions fonctionnelles non pures

L’article se place dans le cadre de langages dont les fonctions peuvent "échouer", c’est à dire ne pas renvoyer de valeur : boucler à l’infini, renvoyer une erreur/exception, etc. C’est le cas de tous les langages Turing-complets,α donc tous les langages que l’on manipule en général. On dit dans ce cas que la fonction diverge, ou n’est pas définie. Le bon outil mathématique pour étudier les fonctions dans ces langages est le concept de fonction partielle : une fonction partielle de A vers B (où A et B sont des ensembles au sens mathématiques du terme) est une fonction qui renvoie, pour tout élément de A, soit un élément de B, soit ⊥, qui représente l’échec, la divergence. Si on évalue une fonction (informatique) en un argument où elle n’est pas définie, l’ensemble du calcul diverge : si par exemple l’évaluation de la fonction boucle à l’infini (ne termine pas), le reste du programme n’est jamais exécuté et l’ensemble boucle à l’infini.

α : la notion de Turing-complétude permet d’étudier l’expressivité d’un langage pour écrire des fonctions de type int -> int ; comme expliqué dans l’article, on peut voir les fonctions présentées ici comme une extension de la notion Turing-like de calcul aux autres types fonctionnels.

Ce point de vue explique aussi la prépondérance du type int dans l’article. Il utilise par exemple souvent des fonctions de type int -> int alors qu’une fonction int -> 'a aurait été plus générale et plus appropriée ; j’ai choisi de respecter ce choix, ne vous étonnez pas de voir des types inutilement restreints.

En deux mots, on considère les fonctions du langage qui sont fonctionnelles, c’est à dire qu’elles se comportent vraiment comme une fonction partielle mathématique : pour chaque valeur possible en argument, elles renvoient toujours la même valeur, ou divergent toujours. On les appelle aussi séquentiellement représentables (SR) parce qu’elles englobent toutes les fonctionnelles que l’on peut définir dans un langage séquentiel, où il n’y a qu’un seul fil d’exécution. Concrètement, les fonctionnelles et les SR désignent ici les mêmes fonctions, mais quand je parle de fonctionnelle j’insiste sur le comportement mathématique (une sortie par entrée au plus), et quand je parle de SR j’insiste sur la possibilité de les implémenter dans un langage SR. De plus, on pourrait imaginer d’autres classes d’expressivité où des fonctions restent fonctionnelles mais ne sont plus SR (cas traité en annxe). Ou l’inverse (cas de la plupart des langages impératifs).

Formellement, on peut voir les fonctions partielles de A dans B comme des applications de A dans (B + {⊥}), où (+) représente la somme disjointe de deux ensembles. On peut alors définir une traduction des valeurs du langage de programmation vers des objets mathématiques : les fonctions du langage deviennent des fonctions partielles, et les types deviennent des ensembles. On note [u] l’objet mathématique représenté par le terme u, et ['a] l’ensemble qui représente le type 'a.

  • Les types et valeurs de base du langage sont traduits par les ensembles et éléments correspondants : [nat] = N, [bool] = {true, false}, [1] = 1, etc.

  • On définit ['a -> 'b] comme l’ensemble des fonctions partielles de ['a] dans ['b] qui sont implémentables dans notre langage de programmation

Plutôt qu’étudier directement les fonctions du langage, on étudiera donc les fonctions mathématiques correspondantes. Il peut y avoir de nombreuses implémentations différentes d’une fonctionnelle, mais on les considèrera toutes comme équivalentes si elles donnent les mêmes résultats pour les mêmes arguments, donc dénotent la même fonction mathématique.

Avec la définition que j’ai donnée, qui est celle du papier, les fonctions prennent en paramètre non pas un terme quelconque du langage, mais une valeur.β Cela veut dire que les arguments doivent être évalués avant d’être passés aux fonctions, et on est donc dans un cadre d’évaluation stricte. On ne perd en fait aucune généralité, et si cela vous intéresse je parle un peu de l’évaluation paresseuse en annexe. En un mot, on peut représenter une valeur paresseuse de type t comme une valeur stricte de type unit -> t, donc cela reste dans le cadre de cette discussion.

β : par valeur on entend "terme qui s’évalue en lui-même" : valeurs immédiates (entiers, booléens), mais aussi structures et fonctions

L’intérêt de cette classe de fonction est apparent : si on sait qu’elles renvoient toujours le même résultat, on peut effectuer un grand nombre de transformations du code sans modifier sa signification, beaucoup plus que pour des fonctions qui n’ont pas cette propriété. Par exemple si un compilateur trouve le code f x + f x, où x est une valeur et f vérifie cette propriété, il peut le transformer en let y = f x in y + y. Cette optimisation correspond à une optimisation déjà effectuée par la plupart des compilateurs optimisants, la CSE (Common Subexpression Elimination) ; mais elle ne peut en générale être pratiquée que pour des expressions très simple (calculs numériques principalement) et pas pour les appels de fonctions, si leur résultat peut changer entre deux appels (certains compilateurs de langages impératifs, dont GCC, permettent cependant au programmeur d’annoter des fonctions pour préciser qu’elles sont fonctionnelles et peuvent être prises en compte par la CSE).

L’article repose en fait sur un deuxième article beaucoup plus long du même auteur, qui développe toute la partie théorique. J’ai essayé d’y jeter un coup d’oeil avant d’écrire cette réaction, et il est complètement indigeste. Exemples de mots clés : « full and faithful » (théorie des catégories), « presheaves » (algèbre), « hypercoherence » (?).

Exemple : fonction use

On s’intéresse à un type, que l’on notera (), qui a une seule valeur que l’on note u (type u = unit). On dénote () par un objet mathématique quelconque noté * : [u] = {*}.

Quels sont les éléments de [u -> u] ? Ces fonctions sont définies par leur valeur en le seul argument possible, *. On a donc deux possibilités :

  1. (* -> ⊥), la fonction qui diverge, que je noterai 0

  2. (* -> *), la fonction qui renvoie *, que je noterai 1

Cas (u -> u) -> u

Quels sont maintenant les éléments de [(u -> u) -> u] ? Quand on a en argument une fonction de type u -> u, on a trois possibilités :

  1. diverger dans tous les cas : c’est la fonction [bot] : f -> ⊥,
    par exemple let rec bot f : unit = bot f ;

  2. renvoyer () dans tous les cas : c’est la fonction [top] : f -> *,
    par exemple let top _ = () ;

  3. évaluer l’argument en () puis renvoyer () : c’est la fonction [mid] : f -> f(*), par exemple let mid f = f ()

    argument retour
    0
    1 *

    En effet, si on évalue 0(*), le calcul diverge (puisque 0 diverge), donc mid(0) est ⊥.

Il y a une quatrième fonction dans l’ensemble des fonctions partielles [u -> u] -> [u], c’est neg, qui termine pour 0 et diverge pour 1. Cette fonction n’est pas représentable,γ car elle correspond en fait au théorème de l’arrêt : elle permettrait de "savoir" sans diverger si son argument diverge, ce qui est impossible dans tout langage Turing-complet. J’ai développé ce point en annexe.

γ : donc n’appartient pas à [(u -> u) -> u]

Cas ((u -> u) -> u) -> bool

Nous arrivons maintenant au coeur du sujet. Quelles sont les fonctions représentables de ((u -> u) -> u) -> bool ? Ce sont les fonctions qui prennent bot, mid et top en paramètre et qui renvoient true ou false, ou divergent.

Quand on a un paramètre de (u -> u) -> u, on peut :

  • diverger dans tous les cas ;

  • évaluer en 0, puis renvoyer un booléen b :

    paramètre retour
    bot
    mid
    top b
  • évaluer en 1, puis renvoyer un booléen b :

    bot
    mid b
    top b
  • renvoyer un booléen b dans tous les cas

Pour toutes ces fonctions, le choix du booléen b n’a pas d’importance : que ce soit true ou false, ça ne diverge pas et c’est tout ce qu’on en tire.

On peut cependant imaginer une autre fonction que l’on appellera use :

bot
mid true
top false

(on peut inverser true et false ici, ça ne change pas grand chose)

Cette fonction permettrait de différentier mid et top, bien qu’elles renvoient les même résultats sur la valeur où elles sont toutes les deux définies. On ne peut pas la coder dans un langage fonctionnel pur. On peut cependant l’écrire en Caml, en se basant sur l’observation suivante : il faut renvoyer true si et seulement si la fonction "utilise" la fonction u -> u qui lui est passée en paramètre.

let use f =
   let used = ref false in
   f (fun () -> used := true);
   !used

Le code utilise un effet de bord, mais la fonction est bien fonctionnelle et appartient donc à la classe SR.

Exercice (plutôt facile)
Coder use en utilisant des exceptions à la place des références.

Cet exercice, ainsi que les suivants, demande d’écrire une fonction. Vous pouvez le faire dans le langage que vous voulez. L’auteur de l’article le ferait sans doute en SML (mais tous mes exercices ne trouvent pas leur solution dans l’article), je l’ai fait en OCaml, mais vous pouvez le faire dans le langage que votre choix. Tout me va, du moment que vous savez différencier les fonctionnalités "pures" du langage des autres (qu’il ne faut pas utiliser sauf quand c’est explicitement demandé), et que vous restez dans un cadre séquentiel. Pour les exercices qui parlent d’exceptions, il est sous-entendu qu’elles ont la sémantique des exceptions ML : si votre langage n’en a pas, faites comme si, un pseudocode suffit. Si vous ne les connaissez pas, j’en ai fait un résumé rapide en annexe.

Fonction modulo

La fonction modulo est un exemple qu’on sent un peu plus utile de fonction SR qui n’est pas représentable dans un langage fonctionnel pur. En deux mots, modulo surveille l’utilisation d’une des fonctions qui lui est passée en paramètre, c’est à dire les arguments qu’elle reçoit au court de ses appels, et les résultats qu’elle renvoie.

Plus précisément, modulo prend une fonction q : (int -> int) -> int (comme "questions") et une fonction r : int -> int (comme "réponses"). Imaginez que l’on donne la fonction r en paramètre à la fonction q, qui doit renvoyer un entier. Les deux fonctions (comme toutes les fonctions de l’article) sont supposées fonctionnelles, donc les moyens d’action de q pour fournir son résultat sont limités : il doit poser des questions à r et, selon les réponses, décider du résultat. Il peut poser une questions, plusieurs, aucunes (c’est alors une fonction constante), poser un nombre différent de questions selon les réponses qu’il a déjà reçues, etc. On définit modulo ainsi : il renvoie l’ensemble des questions posées par q, ainsi que les réponses que r a données.

Il est facile de coder modulo en utilisant des références :

let modulo q r =
  let li = ref [] in
  let traced_r x =
    let y = r x in
    li := (x, y) :: !li;
    y in
  ignore (q traced_r);
  !li

Il y a cependant un problème : cette fonction n’est pas SR ! En effet, elle produit des résultats différents pour fun r -> r 1 + r 3 et fun r -> r 3 + r 1, qui ont pourtant le même représentant dans [int -> int] : le résultat varie selon l’ordre dans lequel les questions ont été posées (d’abord 3, après 1, ou l’inverse). Pour la même raison, on ne s’intéresse pas au nombre d’appels de chaque argument, mais juste à la présence d’au moins un appel avec cet argument.

Il est cependant facile de résoudre ce problème : au lieu d’une liste de résultats on renvoie un ensemble de résultats, en utilisant un type dédié, ou en supprimant les doublons et triant la liste :

let modulo q r =
  let li = ref [] in
  let traced_r x =
    let y = r x in
    if not (List.mem_assoc x !li) then
      li := (x, y) :: !li;
    y in
  ignore (q traced_r);
  List.sort (fun (a, _) (b, _) -> a - b) !li

Cette fonction est maintenant SR. Pourquoi ? On pourrait imaginer que deux implémentations d’une même fonction partielle, qui ne posent pas les mêmes questions. En réalité, ça n’est pas possible : si la première fonction demande un entier n que la deuxième ne demande pas, il suffit de donner une fonction (SR) qui est définie partout sauf en n pour que les deux fonctions produisent des résultats différents (car la première diverge), donc elles ne correspondent pas à la même fonction mathématique.

(moins facile) Écrire modulo en utilisant des exceptions (et sans références).

On arrive maintenant au premier point tangent. Pour rendre notre fonction SR on a du lui retirer des informations, en fournissant du travail supplémentaire. L’auteur de l’article le remarque bien et utilise une formulation poétique : « one sometimes has to do stupid extra work in order to behave functionally ». Cette idée (fournir du travail supplémentaire pour garantir de bonnes propriétés) n’est pas du tout spécifique à ce papier, et je l’ai rencontré dans des situations variées. Par exemple, dans l’excellent CTM, Peter Van Roy différencie la programmation concurrente déterministe de la concurrence non-déterministe. Il doit parfois brider certaines de ses opérations pour rester dans le cadre déterministe : ça limite un peu (voire beaucoup dans certains cas) l’expressivité, mais apporte des avantages considérables en terme de raisonnement sur les programmes, recherche des erreurs, optimisations potentielles, etc.

(moins facile) D’après l’auteur, « indeed it is easy to see how use can be defined from modulo in pure functional ML ». Définir use à partir de modulo.

(difficile) En fait, l’expressivité de modulo est équivalente à celle de use : définir modulo à partir de use. Comme précédemment, il ne faut utiliser aucune autre fonctionnalité impure. Indice : l’implémentation (ou en tout cas la mienne) est nettement moins efficace que les précédentes.

L’auteur présente rapidement quelques fonctions SR qui ne sont pas définissables à partir de use ou modulo, mais sans expliciter leur définitionε ni justifier cette non-définissabilité. Il évoque même une SR universelle, qui permettrait d’exprimer toutes les autres, bien que très peu efficace.

ε : si quelqu’un comprend la signification du deuxième paramètre de la fonction E de l’article, je suis preneur ; je croyais l’avoir trouvée en lisant le paper la première fois, mais je m’étais peut-être trompé, et en tout cas ne l’ai pas retrouvée au moment de la présente rédaction.

Cas d’utilisation

L’auteur présente ensuite deux applications pratiques des fonctions SR. Elles restent de l’ordre du jouet, parce qu’il explique ensuite qu’on peut toujours faire mieux, soit en abandonnant la contrainte de comportement fonctionnel, soit en enrichissant l’interface des fonctions considérées, mais ça reste assez amusant à voir.

Prédicat de recherche

On définit une fonction search :

int -> ((int -> int) -> bool) -> (int -> int) list`

search n pred doit fournir la liste des permutations de [1 .. n] (représentées comme des fonctions int -> int) qui vérifient le prédicat pred.

L’implémentation naïve parcourt l’espace de recherche, qui est de taille !n. Il est possible de couper de grandes parties de l’espace de recherche en utilisant la fonction modulo sur les permutations qui ne vérifient pas le prédicat : si le prédicat ne demande que les trois premières valeurs (en 1, 2 et 3) d’une permutation avant de renvoyer une réponse négative, il est inutile de tester toutes les permutations ayant les mêmes images en 1, 2 et 3 : la réponse sera forcément négative aussi. On a donc éliminé (n - 3)! candidats potentiels.

La définition de search est assez générale pour être appliquée à des problèmes de programmation par contrainte classique, comme le problèmes des huits reines, et l’optimisation utilisant modulo la rend, d’après l’auteur, raisonnablement efficace.

Il est bien sûr possible de faire le même genre de choses en restant dans le cadre des fonctions pures : il suffit de modifier l’interface de search pour que le prédicat renvoie de lui-même les informations sur les appels qu’il a effectué.ζ Plus généralement, on peut visiblement imiter l’effet des fonctions SR impures dans un langage pur, en ajoutant des informations aux valeurs de retour des fonctions appelées. Ce n’est cependant pas toujours possible, par exemple si le prédicat est fourni par une bibliothèque externe sur laquelle le programmeur n’a pas de contrôle.

ζ : (int -> int) -> bool * (int * int) list

Plus généralement, et c’est le second point tangent, l’idée d’enrichir l’interface va en fait à l’encontre des principes habituels de génie logiciel : il faudrait modifier la fonction pour révéler une partie plus importante de son implémentation interne, dans le but d’un usage particulier, ce qui nuit d’une part à la modularité (il faut modifier l’interface d’une fonction fondamentale dans la perspective d’une utilisation spécifique qui peut être très loin dans la chaîne de dépendances), d’autre part à l’encapsulation (on demande à une fonction d’exposer plus d’information sur son implémentation interne). Cette pratique n’est tout simplement pas praticable à grande échelle : si l’on essayait d’exposer toutes les informations potentiellement utilisables par ce genre de pratiques rendues explicites, cela reviendrait essentiellement à rendre publique l’implémentation complète de la fonction.

C’est une question que j’ai trouvé intéressante parce que d’autres outils de la programmation fonctionnelle pure, comme les monades, peuvent soulèver des problèmes similaires. Les fonctions SR sont ici des outils précieux parce qu’elles permettent d’accéder à des informations sur une fonction sans alourdir son interface. Comme dit l’auteur, elles servent en quelque sorte à "rendre la boîte noire moins opaque".

Intégration d’une fonction

Le deuxième exemple est aussi assez joli. Supposez que l’on représente un nombre réel (entre 0 et 1 par exemple) comme un flux paresseux de nombres entiers (ses décimales) : type real = int * (unit -> real).

On cherche à écrire une fonction qui intègre une fonction real -> real entre 0 et 1, à une précision donnée (le nombre de décimales exactes du résultat, passé en paramètre). On commence par demander la valeur de la fonction en 0 à une précision suffisante (au moins autant de décimales que ce qui est demandé pour le résultat final), mais on utilise une variante de modulo pour connaître le nombre k de décimales demandées par la fonction avant de renvoyer un résultat. On sait que, si on se contente de cette précision, tous les nombres réels qui ont les mêmes k premières décimales (ici tous les nombres inférieurs à 10-k) renverront le même résultat. On peut donc approximer l’intégrale de la fonction sur tout cette intervalle avec la précision désirée, et recommencer le processus en commençant à 10-k au lieu de 0. L’algorithme termine en un nombre fini d’étapes (l’auteur en fait la preuve), et c’est une manière astucieuse de faire de très bonnes approximations.

Conclusion

Je donnerai la solution des exercices dans un prochain billet. En attendant, n’hésitez pas à poser vos questions, proposer vos solutions, ou simplement donner votre avis en commentaires ; ils sont là pour ça !

Les commentaires risquent donc de contenir des (fragments de) solutions. Prenez garde à ne pas vous gâcher le plaisir de chercher accidentellement.

Par ailleurs. je suis tout aussi intéressé par des commentaires généraux sur la réaction : avez-vous trouvé ça intéressant ? Trop détaillé ? Pas assez détaillé ? Qu’est-ce qu’il faut changer ? Je n’ai pas encore complètement décidé de la forme que ces réactions devraient avoir, et si vous avez des conseils/demandes, c’est avec plaisir.


Appendices

Il y a quelque chose de curieux dans la définition de la relation [ ] (qui est celle de l’auteur) : on ne prend en compte pour définir les éléments de ['a -> 'b] que leur action sur les éléments définissables (tels qu’on peut écrire un bout de code qui fait exactement ça) de type 'a. Ça me dérange un peu, sachant qu’on pourrait éventuellement passer à une fonction une valeur qui n’est pas définissable à l’intérieur du langage, mais qui fait appel à une bibliothèque logicielle codée dans un autre langage. Dans ce cas il faudrait considérer l’action sur toutes les fonctionnelles, et pas seulement les fonctionnelles définissables.

Exceptions en ML

Je décris ici les expressions en OCaml, mais l’important c’est la sémantique (ce que ça fait), pas la syntaxe (comment ça s’écrit). Le but c’est que vous sachiez un peu précisément ce que vous pouvez utiliser pour les exercices qui demandent d’utiliser des exceptions : un pseudo-code suffit, mais il faut comprendre ce que vous avez le droit de faire ou pas.

L’idée de base est qu’une expression, quand elle est lancée (raise) interromp complètement le calcul qui l’entoure, sauf si elle est rattrappée par un bloc try .. with. Par exemple :

let exists predicate array =
  try
    for i = 0 to Array.length array - 1 do
      if predicate array.(i)
      then raise Exit
    done;
    false
  with Exit -> true

Le bloc try <expr> with <cases> évalue l’expression <expr>, renvoie sa valeur si aucune exception n’est levée pendant l’évaluation, et sinon réagit selon les cases <cases> (c’est un filtrage de motif, mais c’est un détail) : si l’un des cas correspond à l’expression lancée, elle est arrêtée et la branche correspondante est suivie. Ici, on teste toutes les valeurs d’un tableau, en lançant une exception si l’une d’entre elle vérifie une condition, et on renvoie "faux" sinon. En dehors de la boucle, on rattrappe l’exception en renvoyant "vrai". Cela teste l’existence d’un élément vérifiant le prédicat, en arrêtant immédiatement le parcours du tableau dans ce cas (un peu comme les instructions de contrôle break ou return des langages impératifs).

On peut aussi utiliser des exceptions qui transmettent des valeurs. Pour cela il faut déclarer une nouvelle exception (Exit est prédéfinie) :

 exception Mon_exception of mon_type

mon_type est le type des valeurs transmises par l’exception. Par exemple :

exception Position_invalide of (int * int)

... raise Position_invalide (1, 2) ...

match .. with
  Position_invalide (x, y) ->
    printf "erreur : position invalide (%d, %d)" x y

Et les langages paresseux ?

Dans un langage paresseux, les fonctions ne prennent pas des valeurs, mais des termes quelconques que la fonction évaluera éventuellement elle-même si elle en a besoin. Toute la différence pour ce qui nous concerne repose sur le fait qu’on ne sait pas encore si les paramètres vont diverger ou pas (alors qu’en évaluation stricte, on est sûr qu’ils ne divergent pas). Au lieu de fonctions de ['a] dans ['b] + {⊥}, on doit donc modéliser avec des fonctions de ['a] + {⊥} dans ['b] + {⊥}.

Un terme paresseux t est donc un terme "par encore évalué". Il suffit de le représenter dans un langage strict par le terme fun () -> t, qui n’est pas encore évalué non plus.

Plus formellement : au lieu du type 'a, on considère que le paramètre est de type u -> 'a. En effet, [u -> 'a] = {*} -> 'a + {⊥} est isomorphe à 'a + {⊥}. Donc dans un langage strict [(u -> 'a) -> 'b] = ['a]+{⊥} -> ['b]+{⊥}.

On ne gagne donc aucune expressivité en passant à un langage paresseux. Cette étude les concerne tout autant que les langages stricts, modulo une traduction des types. Les fonctions u -> u étudiées sont juste des termes de type u dans un langage paresseux, (u -> u) -> u donne u -> u, et le dernier type est (u -> u) -> bool.

Dans un cadre paresseux, mid est la fonction qui force l’évaluation de son argument. En haskell on écrirait par exemple :

mid f = seq f ()

Impossibilité de neg

L’auteur ne donne pas plus de précisions, mais il n’est pas du tout clair que la fonction neg : 0 -> * | 1 -> ⊥ conduise au problème de l’arrêt. Dans cette section je donne une preuve faite de mes blanches mains, mais elle est un peu compliquée (je me plonge dans une classe de fonctions plus riche), donc si vous en avez une plus simple je suis intéressé.

Le problème de l’arrêt est la question suivante : « peut-on décider sans l’exécuter de si un programme va terminer ou boucler à l’infini ? ». Plus précisément, « existe-t-il un programme qui prend un programme P en paramètre, et renvoie vrai‘ si P bouclerait à linfini s’il était exécuté, et faux sinon ? ». On sait depuis le début du siècle précédent (donc avant l’apparition du premier ordinateur) que la réponse est « Non ».

La preuve de ce « Non » est assez simple (c’est en fait un remake d’une preuve très connue en mathématiques, l’argument diagonal) : imaginons qu’il existe un tel programme H (comme Halting problem). On définit le programme B (comme "Boum") suivant :

B(P) =
  si H(P(P)), boucler à l'infini
  sinon, terminer

B prend un programme, l’applique sur lui-même (le truc pas tordu déjà) et inverse sa divergence : si le programe diverge, B s’arrête, si le programme s’arrête B diverge. On peut maintenant poser la question qui tue : quel est le comportement de B(B) ?

  • si B(B) s’arrête, alors par définition B(B) diverge

  • si B(B) diverge, alors par définition B(B) s’arrête

Boum. On est arrivé à une contradiction, donc on est parti de quelque chose de faux. La seule supposition qu’on a faite pour en arriver là est l’existence de P, donc c’est ce qui est faux : P n’existe pas.

On peut réutiliser cette preuve pour déduire de nombreuses choses sur ce qu’on sait des programmes. Il suffit de "réduire" le problème au problème d’arrêt, en montrant en gros que « monsieur, si on savait répondre à votre question, on pourrait résoudre le problème d’arrêt, you fool ».

Cas présent

Dans le cas présent, on s’intéresse à la fonction neg qui s’arrête si on lui donne 0 (la fonction divergente), et diverge si on lui donne 1. On sent bien qu’il y a un lien avec le problème de l’arrêt, le fait d’inverser les divergences, mais ça ne va pas forcément plus loin : cette fonction ne permet pas de savoir directement si l’argument diverge puisque, une fois sur deux, elle ne répond rien du tout (⊥). Par exempe la fonction voisine 0 -> ⊥ | 1 -> * existe évidemment (c’est mid) et ça n’a rien à voir avec le théorème d’arrêt.

Concrètement, dans notre cadre la fonction d’arrêt est la fonction suivante :

halt 0 = true
halt 1 = false

Peut-on écrire halt à partir de neg ? Dans SR ça n’a pas l’air commode : si on donne 0 à neg, le calcul termine et on peut renvoyer true, mais si on donne 1 on est coincé dans une boucle infinie et on ne peut pas renvoyer false.

Je m’en tire en sortant de SR, en rajoutant une primitive de parallélisme : on ajoute la primitive par : (α -> β) -> (α -> β) -> (α -> β), qui exécute en parallèle les deux fonctions passées en paramètre : quand on lui donne une valeur, elle demande à la première fonction et à la deuxième simultanément, et elle renvoie le résultat de la première des deux à terminer ; si les deux divergent, elle diverge.

Cette primitive existe : il suffit de se placer dans un langage avec multi-threading et un ordonnancement équitable, et d’exécuter les deux fonctions dans des threads séparés, ou plus simplement encore de formaliser l’exécution d’un programme comme une machine procédant étape par étape, et d’alterner une étape d’exécution de chaque fonction à tour de rôle. Cela permet d’accéder à une autre classe d’expressivité des fonctions ; si l’on définit par convenablement, on peut rester dans un cadre déterministe (il suffit de s’assurer que si on lui donne deux fonctions qui terminent, elle ne choisira pas parfois l’une, parfois l’autre mais toujours la même), par contre on sort du cadre des "fonctionnelles", car deux implémentations dénotant une même fonction mathématique pourront donner des résultats différents (si l’une est beaucoup plus lente que l’autre, elle sera moins choisie par par).

(plutôt facile, si vous avez fait le reste) Écrire halt en utilisant par, neg et sans fonctionnalités impures.

Donc, dans le cadre bien réel des "fonctions SR auxquelles ont a ajouté du parallélisme", on ne peut pas écrire neg puisqu’elle permettrait d’écrire halt. Si on ne peut pas écrire neg après avoir ajouté par, on ne pourra sûrement pas l’écrire sans ajouter par (puisqu’on peut écrire moins de chose), donc [neg] n’appartient pas à [(u -> u) -> u].

[ tag:blog.huoc.org,2009:posts/24 ]
Voir les commentaires · Commenter

/\

bluestorm, Emily, et les chameaux

#. Par bluestorm dans Réactions. Mis à jour le 27.02 2010 à 12:08. 3 commentaires.
>. L'innocence est un mythe : les programmes fonctionnels nécessairement impurs
caml capabilities emily langages pola réaction sécurité

Il était une fois un gars sans blog, mais qui voulait publier des choses. Un autre, qui avait un blog (quoique primitif, sans commentaires, sans JavaScript et sans Flash, OMG), vint, et lui proposa astucieusement d’héberger son contenu. Shazam !

Voici donc le premier billet de bluestorm, qui relate ses aventures dans le monde merveilleux des bêbêtes sous haute sécurité (et des concepts liés, bien évidemment), avec l’exemple d’Emily, une belle chamelle s’il en est (et rien à voir avec le modèle de mes dessins). —rz0

C’est ma première réaction. Wouhou ! Je suis débutant, merci d’être indulgent. Comment choisir le paper auquel réagir le premier ? J’ai commencé par rédiger une liste des papers que j’avais envie de traiter, en me promenant au hasard dans mon répertoire ~/Papers, et maintenant je prends arbitrairement le premier de la liste pour me forcer à commencer.

Le paper est How Emily Tamed the Caml (PDF, 16 pages). Il concerne la sécurité informatique, domaine que je connais très mal, et plus précisément une approche qui me fascine : le principe du moindre privilège (POLA, Principle Of Least Authorithy), et la capability-based security.

En deux mots (je vais développer un peu plus ensuite), la question que pose l’article est la suivante : comment faut-il modifier un langage de programmation généraliste pour lui faire respecter les principes du POLA ? On prend un langage de programmation généraliste (ici OCaml), et on le domestique (tame) en lui retirant toutes les fonctionnalités qui cassent le POLA ; un programme écrit dans le langage domestiqué donne donc des garanties de sûreté nettement plus fortes.

Je connais très mal le domaine de la sécurité informatique, donc il faut prendre tout ce que je dis avec prudence. Il y a certainement des imprécisions ou des erreurs dans ce que je vais dire. Si cela vous a intéressé, n’hésitez surtout pas à laisser un commentaire (constructif si possible), mail (note d’édition : bluestorm@, chez GMail, gmail.com, pour ceux qui ne sauraient vraiment pas) ou autres.

POLA, capability-based security

Si vous connaissez déjà tout ça, pas la peine de lire mon introduction, sautez directement aux deux derniers paragraphes de cette réaction, qui résument le contenu du papier.

Privilèges, POLA

On appelle privilège la possibilité de faire quelque chose sur l’ordinateur. Un utilisateur dispose par exemple du droit de renommer ses fichiers, ou d’en ajouter de nouveaux dans son répertoire personnel. Il n’a pas forcément tous les droits, par exemple s’il n’est pas administrateur de la machine il n’a pas forcément le droit d’installer des logiciels ou changer la configuration du système.

Dans la plupart des systèmes actuels, un programme est affilié à un utilisateur (la personne qui l’a lancé) et hérite exactement des droits de l’utilisateur : il a le droit de modifier les fichiers du répertoire personnel, mais pas de modifier la configuration du système. S’il avait plus de droits que l’utilisateur, ce serait une faille de sécurité : il pourrait par exemple modifier la configuration du système pour se lancer au démarrage et se répliquer sur le réseau (c’est comme ça que fonctionnent les virus).

Quand on y pense, il n’est pas forcément normal que chaque programme lancé par l’utilisateur ait accès à tous les privilèges de l’utilisateur. Par exemple si je lance un programme pour me donner l’heure qu’il est, il n’a pas besoin du droit de supprimer des fichiers dans mon répertoire, et s’il essaie de le faire c’est sans doute une erreur de programmation, ou carrément un programme malicieux fournit par un méchant programmeur qui essaie de me faire croire qu’il affiche l’heure pour effacer mes données personnelles. Dans les systèmes d’exploitations modernes, il est donc possible de restreindre les droits de certains programmes.

Le POLA est un principe qui résume cette approche : chacun doit avoir à sa disposition exactement l’ensemble de privilèges dont il a absolument besoin pour faire son travail, et rien de plus.

Lien

Granularité

C’est un principe enfantin qui n’est pas appliqué complètement par les systèmes d’exploitations actuels. En particulier les systèmes dérivés de UNIX utilisent des dérivés de l’approche ugo, où l’on attribue les droits selon un découpage utilisateur/groupe (certains utilisateurs ou groupes d’utilisateurs ont des droits spécifiques sur certains fichiers/programmes/outils), qui est une distinction beaucoup moins fine. Il y a eu des améliorations depuis les premiers temps d’UNIX, mais à ma connaissance aucun système grand public ne respecte vraiment les principes du POLA à un niveau suffisamment fin.

Parce que l’on peut appliquer le POLA à un grand nombre de niveau. Au niveau des utilisateurs, c’est grosso-modo ce que font les administrateurs systèmes consciencieux aujourd’hui. Au niveau des programmes, c’est déjà nettement plus délicat. On pourrait même appliquer le POLA à l’intérieur des programmes en assignant des droits différents aux différents modules et composants logiciels du programme (la couche GUI aurait des droits d’affichage, et pas le reste, par exemple). C’est ce que permettent les langages capability-based.

Il y a donc une idée de granularité : dans l’absolu tout le monde est d’accord sur le fait que le POLA est la meilleure approche de la sécurité, mais jusqu’à quel niveau de détail on peut l’appliquer ?

Capabilities

Les capabilitiesα sont une autre idée qui est fortement reliée au POLA. Elles ne sont pas nécessaires pour construire un système qui respecte le POLA, mais leur élégance fait que les gens qui travaillent dans ce domaine les utilisent très souvent.

Dans les systèmes d’exploitation et les langages actuels, on fait une distinction très nette entre une action, et l’autorisation de faire cette action. Par exemple, n’importe quel programme peut exécuter la fonction qui lit le contenu d’un fichier, il suffit de donner l’adresse du fichier sur le disque. Cependant, cette fonction va parfois échouer si le programme n’a pas les droits nécessaires pour lire ce fichier, et renvoyer une erreur. Le « moyen d’accès » au fichier est son adresse, et il y a une phase de sécurité séparée.

L’idée des capabilities est que le moyen d’accès et le droit d’accès peuvent être réunis en une seule et même entité. Une capability est un objetβ qui contient à la fois la méthode pour faire une action et les droits pour l’effectuer. Si le système donne à un programme la capability « lire le fichier machin », le programme peut l’utiliser pour lire le fichier, ou bien la donner à un autre programme qui aura à son tour le pouvoir de lire le fichier. S’il n’a pas reçu cette capability, le programme n’a pas le droit de le lire, mais il n’a même pas le moyen concret d’essayer de le lire.

α : Je n’ai pas trouvé de traduction en français qui me plaise, je ne sais pas si "capacité" convient.

β : Je parle ici d’objet au sens large et ça n’a aucun rapport avec la Programmation Orientée Objet.

Lien

Rôle des langages de programmation

Support par le système

On a déjà écrit des systèmes d’exploitation miniatures basés sur le concept de capabilities ; par exemple dans le projet EROS/CaprOS. C’est un noyau de système d’exploitation.

Le problème c’est que l’approche par capabilities est radicalement différente des méthodes des systèmes actuels, et il est très difficile d’assurer la compatibilité entre un système d’exploitation capability-based et un logiciel développé pour un système grand public : lui s’attend à ce qu’on lui donne des droits sur tout et n’importe quoi, et si on le fait on perd les bénéfices en matière de sécurité ; il faudrait donc ré-écrire en profondeur le logiciel pour qu’il respecte le POLA. Concrètement, ces projets de recherche ne sont donc utilisés par essentiellement personne parce qu’ils ne sont compatible avec aucun des logiciels existants, et l’effort pour les rendre utilisables serait énorme, bien supérieur au budget des équipes de recherche concernées.

Support par le langage de programmation

Il est cependant possible de mettre en place le POLA localement, au sein d’un programme. Il suffit essentiellement de restreindre l’interface entre le programme et le système d’exploitation (qui est laxiste et donne trop de droits) à une petite couche "administratrice" (qui détient tous les droits du programme), qui se charge de répartir ces droits aux autres composants du programme sans leur en donner trop.

Cela limite un peu le cadre de la chose puisque c’est le programmeur qui s’occupe de sécuriser son propre programme. On ne peut pas faire confiance par exemple à un méchant pour écrire ses programmes de cette manière, il ne va pas se tirer une balle dans le pied ! Cela peut toujours être utile dans le cas où on propose au méchant de coder quelque chose pour notre propre programme : si son code est inclus dans une partie sécurisée, et que la sécurité est vérifiée par le compilateur, il ne peut pas faire de bêtises. C’est aussi utile pour les erreurs qui ne sont pas volontaires mais accidentelles : si on a un bug dans une partie du programme, ça peut mal se passer, mais il ne fera rien qui lui est interdit. S’il y a par exemple une vulnérabilité dans une partie du code qui permet à des attaquants de faire des actions non prévues, ils seront limités par le peu de droits dont dispose cette partie du code, donc c’est très intéressant.

Pour avoir un tel support au sein du programme, il faut des langages de programmation pensés pour. Le langage le plus connu dans ce domaine est E.

Il faut d’une part que le compilateur du langage puisse vérifier que la sécurité est bien respectée (qu’une partie du code qui n’est pas censée lire le contenu de fichiers ne puissse pas quand même demander au système d’exploitation de le faire), et d’autre part que les fonctionnalités du langage ne permettent pas d’écrire du code qui casse cette protection. Par exemple, si une partie du programme peut écrire en mémoire la clé d’accès au contenu du fichier, une partie qui n’est pas censée lire de fichier mais peu lire dans la mémoire peut utiliser cette clé et outrepasser ses droits, sans que le compilateur puisse s’en rendre compte et signaler l’excès de privilège.

Lien

Cas d’Emily

Ce que présente le papier cité c’est la transformation d’un langage généraliste, qui n’a pas été pensé à la base pour respecter le POLA, en un langage sécurisé. Il s’agit essentiellement de retirer toutes les fonctions « dangereuses » de la bibliothèque standard, en les remplaçant par des équivalents « sécurisés » qui rendent explicite les capabilities nécessaires. Par exemple la fonction open_out de Caml, qui permet d’ouvrir un canal en écriture vers n’importe quel fichier, est retirée et n’est plus accessible à l’ensemble du programme. Seule la petite couche d’interface avec le système peut créer des canaux à volonté, et les autres parties du programme doivent lui demander pour recevoir un canal, ou plutôt la capability correspondante, qui encapsule dans un record le canal et les fonctions d’entrée/sortie qui y sont liées.

Le papier décrit donc d’une part les modifications apportées à la bibliothèque standard OCaml, d’autre part la découpe entre la partie « toute puissante » du programme qui a tous les droits donnés par le système, et les parties sécurisées : la partie « toute puissante » doit être la plus petite et la plus simple possible pour ne pas comporter d’erreurs/failles. Il présente des exemples de programmes sécurisés, programmes en ligne de commande ou programmes avec GUI. Il discute aussi les impacts de cette transformation sur le style de programmation, ou les performances des programmes écrits de cette manière.

Pour résumer, je trouve que cet article est une bonne introduction aux problématiques de sécurité d’un langage de programmation. Il est très accessible, n’approfondit pas énormément et ennuirait sans doute un connaisseur du sujet, mais pour un inculte comme moi ça a constitué une lecture agréable.

Lien

[ tag:blog.huoc.org,2009:posts/19 ]
Voir les commentaires · Commenter

>> Page : 0 1