Home About us Products Services Contact us Bookmark
:: wikimiki.org ::
Langage De Programmation

Langage de programmation

ko:프로그래밍 ja:プログラミング La programmation dans le domaine informatique est l'ensemble des activités qui permettent l'écriture des programmes informatiques. C'est une étape importante de la conception de logiciel (voire de matériel, cf. VHDL).

Pratiques


- Algorithmique
- Codage
- Contrôle de version
- Optimisation du code
- Programmation système
- Refactoring
- Test unitaisre

Techniques de programmation


- Programmation impérative
  - Programmation orientée objet
  - Programmation par contrat
- Programmation déclarative
  - Programmation fonctionnelle
  - Programmation logique
  - Programmation par contraintes
- Programmation orientée composant
- Programmation orientée aspect
- Programmation concurrente

Langages de programmation

Les langages de programmation permettent de définir les ensembles d'instructions effectuées par l'ordinateur lors de l'exécution d'un programme. Il existe des milliers de langages de programmation, la plupart d'entre eux étant réservés à des domaines spécialisés. Ils font l'objet de recherches constantes dans les universités et dans l'industrie. Les langages de programmation peuvent être classifiés de nombreuses manières : généraliste/spécialisé, haut niveau/bas niveau, interprété/compilé, avec ou sans gestion de mémoire automatisée, système de gestion d'exceptions, typage fort/typage faible, typage statique/typage dynamique, syntaxe fixe/extensible ; non objet/orienté objet/purement objet, impératif/fonctionnel/déclaratif, fonctionnel pur/impur, etc. Nous incluons ci-dessous une classification sommaire des langages de programmation les plus connus. Il faut garder à l'esprit que de nombreux langages appartiennent simultanément à plusieurs catégories - ils sont dits « multi-paradigmes ». Par exemple, C++ permet la programmation impérative, orientée objet et la programmation générique (à base de classes et de fonctions paramétrées nommées templates). Common Lisp est à la fois impératif, fonctionnel, orienté objet -- et de par son caractère « programmable » (un langage de programmation programmable...), il peut intégrer d'autres « paradigmes » de programmation en son sein (par exemple la programmation logique, ou par contraintes). Ci-dessous, nous listons les langages les plus connus (nous mettons entre parenthèses certains langages dérivés ou les extensions requises).

Langages déclaratifs


- Oz
- Mercury
- Prolog pour PROgrammation LOGique
- Clips Ci-dessous, nous listons les langages spécialisés, c'est-à-dire dont l'utilisation est réservée à des domaines bien spécifiques ; les plus connus sont :

Langages de définition de données


- ASN.1
- DTD SGML
- DTD XML
- XML Schéma
- Relax NG

Langages spécialisés pour la communication avec une base de données


- 4GL

Langages de manipulation de chaînes de caractères


- SNOBOL StriNg Oriented symBOlic Language (Langage Symbolique Orienté Chaînes de Caractères)
- awk
- Perl
- sed

Langages spécialisés Web


- Exécution par le serveur HTTP (côté serveur) :
  - ASP
  - JSP (issu de Java, basé sur des Servlets)
  - PHP
  - XSP (issu de XML, soutenu par Apache)
  - D'une manière générale, les langages non spécialisés (notamment Perl et C) peuvent également être utilisés via Common Gateway Interface
- Exécution par le navigateur Web (côté client) :
  - JavaScript ou ECMAScript
  - VBScript
  - applets écrites en Java
  - ActionScript de Macromedia Flash

Langages de description de page

voir Langage de balisage

Langages de programmation théorique


- Lambda-calcul
- Pi-calcul
- Join-Calcul
- Récursion Primitive
- Système T de Kurt Gödel
- BNF

Langages de programmation de Commande Numérique (C.N.)

Une machine-outil automatisée, ou Commande Numérique (C.N.), a besoin d'un langage de programmation pour réaliser les opérations de tournage, ou de fraisage
- Programmation de Commande Numérique

Pour rendre la programmation plus difficile


- Brainfuck (ou encore F
- ckF
- ck
, Ook ou spoon)
- Intercal
- Malbolge
- Unlambda

Non classés


- Nosica
- SAS
- Langage K
- GOTO++

Langages spécialisés


- ABEL : langage pour la programmation électronique des PLD
- R : langage pour l'outil de statistiques du même nom
- VHDL : langage de description matérielle, permettant de synthétiser de l'électronique numérique (descriptions de portes logiques)
- VRML : description de scènes en trois dimensions

Bibliothèques graphiques


- Allegro - multi-plateforme, Multimédia, Jeux
- DirectX - 3D, Multimédia
- GTK+ - multi-plateforme, Environnement graphique
- JFC - Environnement graphique, 2D
- OpenGL - 3D
- Qt - multi-plateforme, Interface utilisateur
- Quartz - Environnement graphique
- SDL - Video
- SWT - multi-plateforme, Interface utilisateur
- Tk - multi-plateforme - Interface graphique associée à Tcl
- wxWidgets - multi-plateforme - Environnement graphique
- Xlib - 2D

Voir aussi

Liens internes


- Chronologie des langages de programmation
- [http://fr.wikibooks.org/wiki/Programmation Wikilivre sur la programmation]
- ABAP
- RIP

Liens externes


- [http://www.codes-sources.com/ CodeS-SourceS ] : site de passionnés qui partagent leurs connaissances
- [http://www.developpez.com/ Developpez.com, le club des développeurs] (de nombreux forums, cours et tutoriels de programmation)
- [http://www.levenez.com/lang/ Computer Languages History]
- [http://www.techbooksforfree.com/perlpython.shtml Free Python Books]
- [http://www.a525g.com/programmation/index-fr.htm A525G - Programmation]
- [http://www.99-bottles-of-beer.net/ 99 Bouteilles de Bière - Un même programme en plus de 780 langages]
- [http://coding.romainl.com Programmation Network Security]
- [http://rmdiscala.developpez.com/cours/ Package pédagogique multimédia V4.1] Catégorie:Programmation informatique

Programme informatique

Un programme informatique indique à un ordinateur ce qu'il devrait faire faire. Il s'agit d'un ensemble d'instructions qui doivent être exécutées dans un certain ordre par un processus. Un ordinateur sans programme ne fait absolument rien. En fait, la capacité à suivre un programme enregistré sert même souvent, d'un point de vue historique, à distinguer un ordinateur d'une simple machine à calculer. Avec cette définition, le premier ordinateur est le Manchester Mark I, premier calculateur à programme enregistré. À l'origine d'un programme, il y a un code source écrit par un programmeur dans un langage de programmation compréhensible par l'homme. Selon le langage utilisé, ce code est ensuite traduit avec un jeu d'instructions spécifique à un microprocesseur par un compilateur, le programme obtenu peut alors être exécuté directement par l'ordinateur, ou bien est pris en charge par un interpréteur qui décode les instructions et les exécute. Parfois le langage de programmation se réduit à un ensemble de symboles correspondant aux instructions en code machine. C'est du langage assembleur et dans ce cas, un programme appelé assembleur est utilisé pour faire la traduction en langage machine. Le terme programme informatique est souvent utilisé comme synonyme de logiciel ; bien que la majeure partie de tout logiciel soit composée de programmes, les logiciels incluent souvent les fichiers de ressources qui contiennent des données de toutes sortes ; elles ne font pas partie du programme en elles-mêmes. Un programme abstrait est souvent appelé algorithme. Les programmes d'ordinateur sont aujourd'hui souvent les sujets des mathématiques : voir les méthodes formelles, la sémantique des langages de programmation, etc.

Voir aussi

[ langage de programmation | machine de Turing | lisibilité | génie logiciel ] Catégorie:Programmation informatique ja:プログラム ko:프로그램 simple:Computer program zh-cn:程序

Very High Speed Integrated Circuit Hardware Description Language

Catégorie:Sigle Catégorie:Langage informatique VHDL (abréviation de VHSIC HDL) est l'acronyme de Very High Speed Integrated Circuit Hardware Description Language. Il s'agit d'un langage de description du matériel, destiné à décrire le comportement et/ou l'architecture d’un « module » de logique matérielle, c'est-à-dire une fonction combinatoire et/ou séquentielle. Il est utilisé en conception assisté par ordinateur (CAO) de circuits intégrés, dans le cadre de développement d'ASIC et de FPGA.

Détails techniques

La syntaxe du VHDL est tirée du langage Ada, dont les mots clefs ont été adaptés à la conception matérielle. L'une des particularités du VHDL provient du fait qu'il est possible d'exprimer facilement le parallélisme présent à l'intérieur d'un circuit. Le but d'un tel langage de description du matériel est de faciliter le développement d'un circuit numérique en fournissant une méthode rigoureuse de description du fonctionnement et de l'architecture du circuit désirée. L'idée est de ne pas avoir à réaliser (fondre) un composant réel, en utilisant à la place des outils de développement permettant de vérifier le fonctionnement attendu. Ce langage permet en effet d'utiliser des simulateurs, dont le rôle est de tester le fonctionnement décrit par le concepteur. L'étape suivante consiste à synthétiser cette description matérielle pour obtenir un composant réalisant les fonctions désirées, à l'aide d'éléments logiques concrets (portes logiques, bascules ou registres). Ceux-ci seront implémentés, selon la technologie utilisée, soit en transistors (dans le cas d'un ASIC), ou plus couramment en se basant sur les éléments programmables des FPGA. Pour ce dernier cas, il faut alors passer encore par des phases de placement et de routage qui consistent à placer les éléments synthétisés en fonction des ressources particulières disponibles dans les différentes FPGA.

Historique

Originellement commandé par le ministère de la défense américain, celui-ci lui a finalement préféré le langage Verilog HDL, très similaire. Il existe de fait une quasi équivalence entre les deux langages, d'où l'existence de nombreux scripts de traduction de l'un vers l'autre. Le langage VHDL est maintenant principalement utilisé par les entreprises européennes. La version initiale de VHDL, standard IEEE 1076-1987, incluait un large éventail de types de données, numériques (entiers, réels), logiques (bits, booléens), caractères, temps, plus les tableaux de bits et chaînes de caractères. L'un des principaux problèmes concernait le type bit. Celui-ci ne pouvant prendre que 2 valeurs (0, 1), il était impossible de représenter les signaux de valeur inconnue ou encore les signaux en haute impédance, ainsi que la "force" d'un signal (faible, forte ou nulle). La norme IEEE 1164 définit le type std_logic avec 9 états possibles. Ceci a été adopté dans le VHDL-93 (seconde version de la norme IEEE 1076). Afin de répondre aux différents problèmes de l'éléctronique, la norme VHDL a du évolué. L'IEEE Design Automation Standards Committee (DASC) a créé la norme IEEE 1076.1, ou VHDL-AMS (VHDL-Analog & Mixed Systems). Cette nouvelle norme est une extension de la norme IEEE 1076-1987 déjà existante. Elle supporte à présent la description et la simulation de circuits analogiques, numériques, et mixtes (analogique et numérique).

Introduction au VHDL

En VHDL, on décrit l’architecture d’un « module matériel » en deux temps :
- Une ENTITY : il s’agit de déclarer ce que le module expose au monde extérieur, principalement la liste des E/S d’un module (exemple pour une porte logique classique : deux entrées a et b, et une sortie s).
- Une ARCHITECTURE : il s’agit de décrire le fonctionnement d’un module dont les E/S ont été définies dans l'ENTITY. Plusieurs modules aux fonctionnements très différents et donc décrits par plusieurs ARCHITECTUREs, peuvent être basés sur une même ENTITY. (exemple de plusieurs portes logiques à deux entrées et une sortie : ET, OU, XOR…) C’est donc l'ARCHITECTURE qui contient la description de la fonction matérielle désirée :
- soit sous forme de comportement attendu (behaviour), c'est-à-dire orienté fonctionnel,
- soit sous forme de description précise de l’architecture matérielle (les portes logiques à utiliser). Dans cette ARCHITECTURE, la logique utilisée peut être :
- soit combinatoire (on dit « concurrente », c'est-à-dire traitements en parallèle),
- soit dépendante du temps (on parle de logique « séquentielle par opposition à la logique concurrente », c'est-à-dire traitements en série).

Exemples de code VHDL

Un multiplexeur 3 vers 1 (trois architectures concurrentes différentes)

En VHDL, il faut distinguer le contenant du contenu, nommés respectivement entité et architecture.

Le fichier VHDL

Un fichier VHDL doit toujours porter le nom de l'entité qu'il contient. Son extension standard est ".vhd" Avant toute chose, il faut commencer par déclarer l'utilisation des librairies à utiliser dans le projet :
 -- En VHDL : une ligne de commentaires commence avec deux "-"
 -- le fichier doit porter le même nom que l'entité qu'il décrit, ici ce sera "logique_3_vers_1.vhd"

 -- Il faut toujours commencer par importer les bibliothèques VHDL standards normalisées par l'IEEE
  library IEEE;
 use IEEE.std_logic_1164.all;
 use IEEE.std_logic_arith.all;
 use IEEE.std_logic_unsigned.all;

L'entité

Nous décrivons en premier lieu l'interface (l'entité) d'un composant multiplexeur, entité qui nous serviras pour les trois exemples d'architectures concurrentes permettant de réaliser un multiplexeur.
 -- Voici un exemple d’entité, décrivant les E/S utilisées
 -- par les trois exemples d’architectures purement concurrentes :
 -- 
 -- ATTENTION, l'entité doit avoir le même nom que le fichier (logique_3_vers_1.vhd)
 ENTITY logique_3_vers_1 IS

   PORT
   (
     a   : IN STD_LOGIC;
     b   : IN STD_LOGIC;
     c   : IN STD_LOGIC;
     adr : IN STD_LOGIC_VECTOR (1 downto 0);
     s   : OUT STD_LOGIC
   );

 END logique_3_vers_1;

Première Architecture

La première architecture permettant de décrire ce multiplexeur est en fait plus particulièrement adaptée aux équations simples, ou aux fonctions en somme de produits impliqants un nombre d'entrées variable (par exemples=a.b+a.b.c) car cette méthode est très souple et permet de tout faire. En contrepartie elle est peu lisible pour les équations complexes.
 -- Première architecture concurrente décrivant un mux :
 ARCHITECTURE mux_3_vers_1 OF logique_3_vers_1 IS

 BEGIN

   s <= ( a AND NOT adr(1) AND NOT adr(0) )
     OR ( b AND NOT adr(1) AND     adr(0) )
     OR ( c AND adr(1)     AND NOT adr(0) );
  -- OR ('0'AND adr(1)     AND     adr(0) );  -- Cette dernière ligne est implicite dans l'équation du dessus

 END mux_3_vers_1;

Deuxième Architecture

La deuxième architecture permettant de décrire ce multiplexeur est limitée aux fonctions dont le nombre d'entrées est fixée (n) pour lesquelles elle est tout particulièrement adaptée. Cependant, avec cette méthode il est obligatoire de lister l'ensemble des états possible (2^n).
-- Deuxième architecture concurrente décrivant un mux :
ARCHITECTURE mux_3_vers_1 OF porte_3_vers_1 IS

BEGIN

  WITH adr SELECT
    s <=  a  WHEN "00",
          b  WHEN "01",
          c  WHEN "10",
         ‘0’ WHEN "11";

END mux_3_vers_1;

Troisième Architecture

La troisième architecture permettant de décrire ce multiplexeur est elle aussi limitée aux fonctions dont le nombre d'entrées est fixée (n) pour lesquelles elle est tout particulièrement adaptée. Cependant, avec cette méthode il n'est pas obligatoire de lister l'ensemble des états possible, puisque la dernière ligne permet d'appliquer un traitement par défaut.
-- Troisième architecture concurrente décrivant un mux:
ARCHITECTURE mux_3_vers_1 OF porte_3_vers_1 IS

BEGIN

  s <= a  WHEN adr = "00" ELSE
       b  WHEN adr = "01" ELSE
       c  WHEN adr = "10" ELSE
      ‘0’;

END mux_3_vers_1;

Architecture séquentielle - une bascule D

La description d'une architecture séquentielle, c'est à dire avec d'une fonction dépendante du temps (ie. de l'horloge) passe par l'utilisation de process.
-- Architecture séquentielle pour une bascule D :
ARCHITECTURE bascule_d OF xxx IS

BEGIN
	
  bascule : PROCESS (clk, reset)
    IF reset = ‘1’ THEN
      q <= 0;
    ELSE
      IF clk’event AND clk = ‘1’ THEN
        q <= d;
      END IF;
    END IF;
  END bascule;

END bascule_d;

Hello World

 --  En VHDL, une ligne de commentaires commence par deux "-" 
 --  Programme d'exemple VHDL: hello.vhd

 -- La directive "use" permet d'inclure des packages externes
use std.textio.all;

 -- le concept d'entity (entite) est ce qui sera exposé
 -- au monde extérieur. Ici ne sont décrites que les entrées/sorties
ENTITY hello IS
  -- aucune description ici
END ENTITY hello;


 -- on peut implémenter une entity de multiples manières, en utilisant
 -- les architectures. C'est pourquoi il convient de nommer chaque
 -- implémentation.
ARCHITECTURE Wiki OF hello IS
  CONSTANT message : string := "hello world";
    -- bien que le VHDL le permette, une telle chaîne de caractères n'a aucun sens 
    -- en implémentation matérielle.
BEGIN

  -- un process peut être considéré comme une description séquentielle d'un
  -- comportement.
  PROCESS
    variable L: line; 
  BEGIN
  
    write(L, message);
    writeline(output, L);
    wait;
  
  END PROCESS;

END ARCHITECTURE Wiki;
ja:VHDL

Codage

Cet article concerne le codage de données en général. Pour ce qui est de l'acte de programmer, se référer à codage (programmation). De façon générale un codage permet de passer d'une représentation des données vers une autre. Parmi les différents codages utilisés, on trouve :
- Le codage de Huffman, qui permet de faire de la compression de données (essentiellement sur du texte).
- Le codage de caractères pour représenter les textes dans diverses langues.
- La transformation d'une source vidéo ou sonore en un format informatique déterminé. Coder en MP3, en AVI, etc. Dans ce cas, le codage n'est plus une opération mathématique bijective (réversible) et l'expression encodage numérique est préférée. Le codage fait appel à des codec (CODeur/DECodeur) s'appuyant sur des algorithmes afin de compresser une source pleine en une forme plus légère, mais toujours exploitable lors du décodage, par ces mêmes codecs.

Optimisation du code

En programmation informatique, l'optimisation est la pratique qui consiste généralement à réduire le temps d'exécution d'une fonction, l'espace occupé par les données et le programme, ou la consommation d'énergie. La règle numéro un de l'optimisation est quelle ne doit intervenir qu'une fois que le programme fonctionne et répond aux spécifications fonctionnelles. L'expérience montre qu'optimiser du code avant que ces deux conditions ne soient réalisées revient le plus souvent à une perte de temps et s'avère néfaste à la clarté du code et au bon fonctionnement du programme : :« L'optimisation prématurée est la source de tous les maux. », Donald Knuth citant Dijkstra La plupart des compilateurs récents pratiquent de façon automatique un certain nombre d'optimisations qu'il serait fastidieux d'effectuer manuellement et qui rendraient le code source moins lisible. L'optimisation manuelle peut s'avérer nécessaire dans des cas très spécifiques, mais les mesures montrent que sur des machines RISC qui possèdent un nombre élevé de registres et où l'efficacité demande le regroupement des instructions identiques pour bénéficier de l'effet pipeline, l'optimiseur d'un compilateur C fournit souvent un code plus efficace que celui qui serait écrit en assembleur par un programmeur expérimenté (ce qui n'était jamais le cas sur les machines CISC). Et de surcroit ce code est bien plus facile à maintenir, car les instructions en C restent dans un ordre lié à la seule intelligibilité du code et non aux spécificités de la machine : dans les optimiseurs actuels, en effet, les ordres machines associés à une instruction ne se trouvent plus nécessairement en position contiguë, pour des raisons d'efficacité d'exécution. Cela rend le code assembleur généré particulièrement indéchiffrable.

Pratique de l'optimisation

Première approche

Avant de commencer l'optimisation, il faut savoir mesurer la vitesse du code. Pour cela il faut choisir un paramètre, de préférence simple, mesurable. Ceci peut-être par exemple le temps de traitement sur un jeu de donnée précis, ou le nombre d'images affichées par seconde, ou encore le nombre de requêtes traitées par minute. Une fois le paramètre de mesure déterminé, il faut mesurer le temps passé dans chacune des parties du programme. Il n'est pas rare que 80% à 90% du temps soit consacré à l'exécution de 10% du code (les
boucles critiques). Les chiffres varient en fonction de la taille et de la complexité des projets. Il faut localiser ces 10% de code pour être le plus rentable dans ses optimisations. Cette étape de localisation peut être réalisée à l'aide d'outils spécialisés d'instrumentation du code nommés profilers. Ils sont chargés de compter le nombre d'exécutions de chaque fonction et de cycles du microprocesseur correspondants au cours de l'exécution. Ensuite on itère sur la section la plus consommatrice de ressource autant de fois que nécessaire cette boucle :
- optimisation d'une partie du code
- mesure du gain de performances

Seconde approche

On peut optimiser à plusieurs niveaux un programme:
- au niveau algorithmique, en choisissant un algorithme de complexité inférieure (au sens mathématique) et des structures de données adaptées,
- au niveau du langage de développement, en ordonnant au mieux les instructions et en utilisant les bibliothèques disponibles,
- en utilisant localement un langage de bas niveau, qui peut être le langage C ou, pour les besoins les plus critiques, le langage assembleur. On ne passe au niveau supérieur d'optimisation qu'une fois qu'on a épuisé les possibilités d'un niveau. L'utilisation d'un langage de bas niveau sur l'ensemble d'un projet pour des raisons de rapidité est l'une des erreurs les plus communes et les plus coûteuses que puisse faire un projet industriel. L'optimisation de code est considéré par beaucoup de développeurs amateurs comme un art un peu magique et, pour cette raison, comme l'une des parties les plus excitantes de la programmation. Ceci les conduit à croire qu'un bon programmeur est une personne qui optimise d'emblée le programme. Cependant l'expérience montre qu'elle ne peut pallier une mauvaise conception initiale.
C'est dans la conception que l'expérience du développeur joue le plus. Par ailleurs, dans un nombre majoritaire et grandissant de cas, le « bon programmeur » est moins celui qui écrit du code astucieux (l'optimiseur s'en chargera le plus souvent mieux que lui) que celui qui écrit du code lisible et aisé à maintenir. Une bonne connaissance des techniques de structures de données ainsi que des algorithmes (même sans aller jusqu'aux considérations théoriques poussées de la complexité algorithmique) se montre bien plus féconde que celle d'un langage d'assemblage. Lorsqu'on a déterminé l'algorithme le plus adéquat, les optimisations les plus efficaces peuvent être obtenues en utilisant le chemin suivant :
- écriture du code critique dans un langage de haut niveau (comme Scheme ou Common Lisp),
- application de transformations mathématiques successives qui préservent la spécification du programme tout en réduisant la consommation des ressources,
- traduction du code transformé dans un langage de bas niveau (langage C). Dans la pratique, les performances des machines actuelles font que des applications comportant beaucoup d'entrées-sorties peuvent faire l'économie de ces trois étapes et se rédiger directement dans un langage comme Haskell. L'application bien connue
nget, qui moissonne systématiquement les images publiées dans les forums Usenet, avait dans sa première implémentation été écrite en Haskell. La version en C n'en a été qu'une traduction qui ne se révèle pas plus performante pour ce type d'application.

Optimisation automatique

Les compilateurs sont souvent capable de faire des optimisations locales, auxquelles aucun développeur ne penserait en première approche. Pour le langage C, cela peut considérer :
- les variables locales et les registres
- les fonctions non implémentées en assembleur en tant que fonction
- les switch, qui sont optimum.

Exemples

Une spécificité du binaire : le décalage

Une des toutes premières optimisations a été celle de la division et de la multiplication par une puissance de 2. En effet, l'informatique actuelle repose sur le binaire, puisqu'elle utilise comme élément de base le transistor (et historiquement, auparavant le relais) qui n'autorise que deux valeurs différentes. On a donc logiquement implémenté en langage machine les opérations de décalage à gauche et décalage à droite. En effet, en binaire, le décalage d'un nombre d'un cran vers la gauche le multiplie par 2. :Ainsi, 2 (102) décalé de 1 bit donne 4 (1002). :5 (1012) décalé de 2 bits donne 20 (101002) : 5
- 2^2=20. Ceci marche aussi pour la division, en décalant les bits vers la droite. :100 (11001002) décalé de 3 bits vers la droite donne 100/2^3=12.5 donc 12 (11002) car nous travaillons sur des nombres entiers. La division (en dehors de ce cas et des cas pathologiques) est une instruction coûteuse en temps machine, et n'est d'ailleurs toujours pas disponible sur la grande majorité des processeurs de type RISC.

Le mot clef inline du C

Le code C suivant: inline int f(int a, int b) int g (int a) Une compilation avec gcc -O4 -S donne: .file "opt.c" .text .p2align 4,,15 .globl g .type g, @function g: pushl %ebp movl %esp, %ebp movl 8(%ebp), %edx cmpl $12, %edx jg .L14 leal -2(%edx), %eax cmpl $11, %edx jge .L15 movl $100, %eax cmpl $10, %edx .L17: je .L2 movl %edx, %eax .L15: imull %edx, %eax .L2: popl %ebp ret .p2align 4,,7 .L14: movl $1437600, %eax cmpl $1200, %edx jmp .L17 .size g, .-g .section .note.GNU-stack,"",@progbits .ident "GCC: (GNU) 3.3.2 (Mandrake Linux 10.0 3.3.2-6mdk)" Ce qui pourrait se traduire, pour une compréhension plus aisée, par le code C suivant: int g(int a) On peut remarquer par exemple que la fonction 'f' n'a pas été générée, mais que son code a directement été incorporé dans la fonction 'g' (le mot clef 'inline' permet de forcer ce type d'optimisation en C ) Catégorie:Programmation informatique

Programmation système

La programmation système est un type de programmation qui vise au développement de programmes qui font partie du système d'exploitation d'un ordinateur ou qui en réalisent les fonctions. Elle se distingue de la programmation des applications en ce qu'elle s'intéresse non pas au traitement des données, mais aux interfaces, aux protocoles et à la gestion des ressources, telles que le temps et l'espace. Elle inclut, en outre, l'accès aux fichiers, la programmation du clavier, de l'écran, des modems, la programmation réseau, et, en général, la programmation de tous les périphériques qui font entrer ou sortir de l'information d'un ordinateur, de la mémoire vive et des processeurs. Catégorie:Système d'exploitation

Refactoring

Catégorie:Programmation informatique Catégorie:Génie logiciel La refactorisation (anglicisme venant de refactoring) est une opération de maintenance du code informatique. Elle consiste à retravailler le code source non pas pour ajouter une fonctionnalité supplémentaire au logiciel mais pour améliorer sa lisibilité et simplifier sa maintenance (on parle aussi de remaniement). C'est donc une technique qui s'approche de l'optimisation du code, même si les objectifs sont radicalement différents.

Pourquoi refactoriser ?

Au fur et à mesure de la vie d'un logiciel on est amené à implémenter de nouvelles fonctions ou à corriger des bugs. Or ces modifications ne se plient pas toujours avec élégance dans l'architecture du logiciel. Le code source d'un programme tend donc à devenir de plus en plus complexe au fur et à mesure de son existence. Cela est notamment vrai avec les techniques modernes de développement itératif incrémental où le logiciel entre en phase de modification pratiquement dès le début de son existence. Il est donc important de mettre en œuvre des techniques qui permettront de toujours conserver un code aussi simple que possible. Cela consiste à :
- S'assurer que toute l'information nécessaire est disponible
- Supprimer toute information redondante ou duplication de code
- Simplifier l'algorithmique des méthodes
- Limiter la complexité des classes
- Limiter le nombre de classes

Les niveaux de refactorisation

On peut distinguer plusieurs niveaux de refactorisation, selon l'impact des modification sur le déroulement du programme et les risques rencontrés. En pratique, durant une même session de refactorisation on jonglera souvent entre ces divers niveaux.

Modification de la présentation

A ce niveau on cherche à simplement améliorer la présentation du code source sans modifier le code exécuté. Ce type d'amélioration concerne donc essentiellement les commentaires (suppression des commentaires superflus, ou ajout de commentaires sur des sections complexes) et la mise en page (indentation du code, passages à la ligne).

Modification de l'algorithmique

Ce type de modification est destiné à conserver des méthodes aussi simples que possible. Cela est le plus souvent réalisé en scindant une méthode ou un algorithme en plusieurs parties ou en confiant à un objet annexe une partie du traitement. Dans ce type de modification, il est tout à fait possible (et fréquent) d'introduire des bugs, il est donc fortement conseillé de construire une batterie de tests unitaires et de l'exécuter après chaque modification.

Refonte du design

C'est le type de modification le plus radical puisqu'il consiste à modifier la hiérarchie de classes composant l'application. Il est là aussi préférable de procéder par petites modifications et d'exécuter une suite de tests à chaque fois. Il est par ailleurs très difficile de réaliser une refonte en profondeur sans outil spécialisé comme un butineur de classes.

Des activités de refactorisation

Suppression du code mort

Le code mort est du code qui ne sert à rien car il n'est jamais appelé par une autre partie du programme. Il ne sert donc qu'à rendre le source plus complexe et à provoquer des risques de confusion. Le plus difficile est bien entendu de détecter le code mort, on peut pour cela utiliser plusieurs techniques:
- recherche statique par l'outil grep sur le code source pour vérifier qu'une méthode est bien appelée quelque part
- analyseur de références croisées (par exemple l'outil objxref livré avec le compilateur turbo C de Borland)
- outil de mesure de couverture de code. C'est sans doute la méthode la plus pratique puisqu'elle permet également de vérifier des portions de méthodes. Elle est également la plus risquée puisque du code peut être marqué comme non couvert simplement parce que votre suite de test n'est pas complète (ce qui est en pratique toujours le cas). Il existe une autre forme de code mort: le code commenté. Il arrive souvent que suite à des modifications, on laisse des pans entiers de l'ancien code pour pouvoir éventuellement revenir à la version antérieure facilement. Ce type de code devrait également être supprimé à la fin de la session de développement. Dans tous les cas, il n'est jamais recommandé de conserver du code qui pourrait servir un jour. Il est toujours préférable de le supprimer de la version de travail et d'utiliser un outil de contrôle de version pour archiver ce type de code.

Ajout d'assertions

Les assertions sont une technique de programmation qui consiste à vérifier qu'un certain nombre de conditions sont vérifiées. Elles sont très intéressantes d'une part car elles permettent de simplifier le déboggage en détectant les erreurs au plus tôt, mais également parce qu'elle sont placées à l'intérieur du code qu'elles contrôlent et peuvent donc aider à la compréhension de l'état du système.

Renommage

Au fur et à mesure du développement d'un logiciel, le rôle des classes et des méthodes devient plus clair. Il est donc souvent utile de modifier les noms de classes ou de méthodes pour bien indiquer ce rôle.

Commentaires

Les commentaires sont un sujet assez controversé de documentation du logiciel. Il est de toute façon important de toujours garder les commentaires synchronisés avec le code.

Références


- Jean-Philippe Retaillé, Refactoring des applications Java/J2EE, Eyrolles, 2005, 390 p., ISBN 2212115776
- Martin Fowler, Kent Beck, Refactoring: Improving the Design of Existing Code, Addison-Wesley Professional, 1999, 464 p., ISBN 0201485672
- Joshua Kerievsky, Refactoring to Patterns, Addison-Wesley Professional, 2004, 400 p., ISBN 0321213351 ja:リファクタリング (プログラミング)

Programmation impérative

En informatique, la programmation impérative est un style de programmation qui décrit les opérations en termes d'états du programme et de séquences d'instructions exécutées par l'ordinateur pour modifier l'état du programme. L'implémentation de la quasi totalité des microprocesseurs qui équipent les ordinateurs est de nature impérative : ils sont faits pour exécuter du code écrit sous forme d'opcodes (pour operation codes), qui sont des instructions élémentaires exécutables par le microprocesseur. L'ensemble des opcodes forme le langage machine spécifique au microprocesseur et à son architecture. L'état du programme à un instant donné est défini par le contenu de la mémoire centrale à cet instant, et le programme lui-même est écrit en style impératif en langage machine, ou le plus souvent dans une traduction lisible par les humains du langage machine, dénommée assembleur. Les langages de plus haut niveau utilisent des variables et des opérations plus complexes, mais suivent le même paradigme. Les recettes de cuisine et les vérifications de process industriel sont deux exemples de concepts familiers qui s'apparentent à de la programmation impérative; de ce point de vue, chaque étape est une instruction, et le monde physique constitue l'état modifiable. Puisque les idées basiques de la programmation impérative sont à la fois conceptuellement familières et directement intégrées dans l'architecture des microprocesseurs, la grande majorité des langages de programmation sont impératifs. La plupart des langages de haut niveau comportent quatre types d'instructions principaux : l'assignation, le bouclage, le branchement conditionnel, et le branchement sans condition.
- Les instructions d'assignation, en général, effectuent une opération sur l'information en mémoire et y enregistrent le résultat pour un usage ultérieur. Les langages de haut niveau permettent de plus l'évaluation d'expressions complexes, qui peuvent consister en une combinaison d'opérations arithmétiques et d'évaluations de fonctions, et l'assignation du résultat en mémoire.
- Les branchements conditionnels permettent à un bloc d'instructions de n'être exécuté que si une condition prédéterminée est réalisée. Dans le cas contraire, les instructions sont ignorées et la séquence d'exécution continue à partir de l'instruction qui suit immédiatement la fin du bloc.
- Les branchements sans conditions permettent à la séquence d'exécution d'être transférée à un autre endroit du programme. Cela inclut le saut, appelé « goto » dans de nombreux langages, et les sous-programmes, ou appels de procédures.
- Les instructions de bouclage servent à répéter une suite d'instruction un nombre prédéfini de fois, ou jusqu'à ce qu'une certaine condition soit réalisée. Les instructions de bouclage peuvent être vues comme des sauts conditionnels, c'est-à-dire la combinaison d'un branchement conditionnel et d'un saut.

Un bref historique

Les langages impératifs les plus anciens sont les langages machine des premiers ordinateurs. Dans ces langages, le jeu d'instructions était minimal, ce qui rendait l'implémentation hardware plus simple, mais empêchait la création de programmes complexes. Le premier compilateur - un programme destiné à vérifier un programme au préalable et à le traduire en langage machine - dénommé A0, fut écrit en 1951 par Grace Murray Hopper. FORTRAN, développé par John Backus chez IBM à partir de 1954, fut le premier langage de programmation capable de réduire les obstacles présentées par le langage machine dans la création de programmes complexes. FORTRAN était un langage compilé, qui autorisait entre autre l'utilisation de variables nommées, d'expressions complexes, et de sous-programmes. Après plusieurs révisions du langage, en 1978 et en 1990, FORTRAN est toujours utilisé dans le milieu scientifique pour la qualité de ses bibliothèques numériques et sa grande rapidité, ce qui en fait le langage informatique ayant eu la plus grande longévité. Les deux décennies suivantes virent l'apparition de plusieurs autres langages de haut niveau importants. ALGOL, développé en 1958 par un consortium américano-européen pour concurrencer FORTRAN, qui était un langage propriétaire, fut l'ancêtre de nombreux langages de programmation d'aujourd'hui. COBOL (1960) et BASIC (1964) étaient deux tentatives pour rendre la syntaxe plus proches de l'anglais, et donc plus accessible. COBOL était spécifiquement destiné aux applications de gestion, tandis que BASIC avait essentiellement un but éducatif. Sa simplicité et le fait qu'il soit interprété facilitait grandement la mise au point des programmes, ce qui lui conféra rapidement une grande popularité, malgré la pauvreté de ses constructions. Malheureusement, cette pauvreté même devait mener à une quantité de programmes non structurés et donc difficilement maintenables. Après un article de Dijkstra dénonçant les ravages de BASIC, la réputation de BASIC comme langage pour l'enseignement de la programmation déclina. Dans les années 1970, le Pascal fut développé par Niklaus Wirth, dans le but d'enseigner la programmation structurée et modulaire. Pascal combinait les meilleures constructions des langages COBOL, FORTRAN et ALGOL dans un ensemble élégant, mais simpliste, qui lui assura un succès durable comme langage d'initiation (en remplacement de BASIC). À la même époque, Dennis Ritchie créa C aux laboratoires Bell, pour le développement du système Unix. La puissance du C, permettant grâce aux pointeurs de travailler à un niveau proche de la machine, ainsi qu'un accès complet aux primitives du système lui assura un succès qui ne s'est jamais démenti depuis. Par la suite, Niklaus Wirth fut à l'origine de Modula-2, Modula-3, et d'Oberon, les successeurs de Pascal. En 1974, le Department of Defense des États-Unis cherchait un langage dont le cahier des charges mettait l'accent sur la sûreté d'exécution, pour tous ses besoins futurs. Le choix se porta sur Ada, langage créé par Jean Ichbiah à Honeywell, dont la spécification ne fut complétée qu'en 1983. Dans les années 1980, devant les problèmes que posait la complexité grandissante des programmes, il y eut un rapide gain d'intérêt pour la programmation orientée objet. Smalltalk-80, conçu à l'origine par Alan Kay en 1969, fut présenté en 1980 par le Palo Alto Research Center de la compagnie Xerox (États-Unis). À partir des concepts objet, Bjarne Stroustrup, chercheur aux Bell Labs, conçut en 1985 une extension orientée objet de C nommée C++, qui gardait la vitesse de C. Parallèlement, une extension à C moins ambitieuse, mais inspirée de Smalltalk avait vu le jour, Objective C. Le succès d'Objective C, notamment utilisé pour le développement sur les stations NeXT et Mac OS X, est resté faible par rapport à C++. Le langage Common Lisp fut le premier langage à objets (CLOS) standardisé par l'ANSI, en 1995. Dans les décennies 1980 et 1990, de nouveaux langages impératifs interprétés ou semi-interprétés doivent leur succès au développement de scripts pour des pages web dynamiques et les applications client-serveur. On peut citer dans ces catégories Perl (Larry Wall, 1987), Python (Guido van Rossum, 1990), PHP et Java (Sun Microsystems, 1996). Les langages de programmation impératifs doivent être distingués d'autres types de langages, les langages fonctionnels et les langages de programmation logique. Les langages fonctionnels, tels que Haskell ou ML, ne sont pas des suites d'instructions et ne s'appuient pas sur l'idée d'état global, mais au contraire tendent à s'extraire de ce modèle pour se placer à un niveau plus conceptuel (qui a ses fondations dans le lambda-calcul). Les langages de programmation logiques, tels que Prolog, se concentrent sur ce qui doit être calculé, et non comment le calcul doit être effectué.

Liens externes

Un [http://www.levenez.com/lang/ synoptique] de l'histoire des langages de programmation. Un autre se trouve [http://merd.net/pixel/language-study/diagram.html ici]. Catégorie:Programmation informatique

Programmation par contrat

Catégorie:Programmation informatique La programmation par contrat est un paradigme de programmation dans lequel le déroulement des traitements est garanti par des vérifications sur les données, ce qui permet d'être sûr que les traitements ne vont pas déclencher d'erreur. Il y a trois catégories de vérification :
- Précondition : L'ensemble des conditions qui doivent être vérifiées avant le lancement d'un traitement donné. Ces conditions permettent de s'assurer que le déroulement du traitement est possible sans déclencher d'erreur.
- Postcondition : L'ensemble des conditions qui doivent être vérifiées après le déroulement d'un traitement. Ces conditions permettent de s'assurer que le déroulement du traitement n'a pas déclenché d'erreur.
- Invariant : L'ensemble des conditions qui doivent être vérifiées à tout moment, y compris au sein d'un traitement. Plusieurs langages de programmation implémentent ce paradigme comme Eiffel, D, Lisaac, Spark, VDM. Des modules existent pour d'autres langages, comme JContractor pour Java. Ce paradigme a été imaginé pour vérifier le traitement sans pour cela surcharger les opérations par trop de tests. Ils représentent le minimum des tests a réaliser garantissant le traitement mais à la condition qu'ils soient corrects et complet. Il est alors nécessaire de s'assurer de leurs bonnes réactions aux erreurs et aux cas correctes, l'utilisation de test unitaire permet de coder se genre de vérification de manière élégante.

Exemple

Une fonction calculant une racine carrée peut être vérifiée de la manière suivante en pseudo code.
- Fonction calculant la racine carrée de la valeur x
  - Précondition : x ≥ 0
  - Postcondition : résultat² = x La précondition assure que le développeur utilise la fonction correctement, alors que la postcondition permet au développeur de faire confiance à la fonction.

Liens externes


- [http://www.eiffel.com/ Le site officiel Eiffel]
- [http://jcontractor.sourceforge.net/ Le site de JContractor]

Programmation déclarative

Catégorie:Programmation informatique Dans la programmation déclarative, on décrit d'une part la situation d'un problème, ce qui correspond aux données et les contraintes sur ces données, ce qui correspond à la partie programme. Ensuite on pose une question, le but. Le programme cherche à répondre à la question à partir de la situation en respectant les contraintes. Prolog (pour programmation logique) est le langage de programmation déclaratif le plus connu.

Programmation logique

La Programmation logique est une forme de programmation dont l'essence est de définir des règles de logique mathématique au lieu de fournir une succession d'instruction que l'ordinateur exécuterait. Le premier langage de programmation logique est Prolog. th:การเขียนโปรแกรมเชิงตรรกะ

Programmation par contraintes

Catégorie:Algorithmique Catégorie : Recherche opérationnelle __NOTOC__

Présentation

La programmation par contraintes (PPC, ou Constraint Programming (CP) en anglais) consiste à programmer avec des relations appelées 'contraintes'. Un problème comporte un certain nombre de variables, chacune ayant un domaine, et un certain nombre de contraintes. Une contrainte implique une ou plusieurs variables, et restreint les valeurs que peuvent prendre simultanément ces variables. Trouver une solution à un problème de PPC consiste à décrire l'ensemble des affectations autorisées de chaque variable, de telle sorte que la totalité des contraintes soient satisfaites.

- Définition
- Historique
- Tout ce que vous devez savoir sur la PPC en moins de 5 mn

Programmation par Contraintes sur les domaines finis


- Description du problème de satisfaction de contraintes
- Algorithmes de recherche : en profondeur, en largeur, heuristiques, LDS
- Consistances locales : node, arc, hyperarc, chemin, k-consistance, autres consistances
- Algorithmes de consistance locale : AC1, AC2, AC3, AC4, AC5, AC6, AC7, AC8, AC2001
- Exemples : n-dames, send+more=money, reconnaissance de scènes 3D, raisonnement temporel qualitatif
- Applications

Autres domaines de contraintes


- termes
- booléens
- réels
- intervalles
- ensembles
- chaînes

Solveurs de contraintes


- Etude théorique
- Architecture d'un solveur
- Langages d'expression des propagateurs : indexicaux, CHRs
- liens vers solveurs libres et commerciaux

Extensions


- contraintes valuées
- étude et détection des symétries
- techniques de décomposition et classes de problèmes "traitables"
- métaheuristiques
- transition de phase
=Liens externes=
- [http://www.afpc-asso.org AFPC] : Association Française de Programmation par Contraintes

Programmation orientée composant

La programmation orientée composants (POC) consiste à utiliser une approche modulaire au niveau de l'architecture d'un projet informatique, ce qui permet d'assurer au logiciel une meilleure lisibilité et une meilleure maintenance. Les développeurs, au lieu de créer un exécutable monolithique, se servent de briques réutilisables. La POC n'est pas sans similitudes avec la POO, puisqu'elle revient à utiliser une approche objet, non pas au sein du code, mais au niveau de l'architecture générale du logiciel. La POC est particulièrement pratique pour le travail en équipe et permet d'industrialiser la création de logiciels. =Le composant, base de la POC=

Qu'est-ce qu'un composant ?

Lorsque l'on parle de composants, il s'agit de simples fichiers, contenant généralement du code compilé. Sous les systèmes de type Unix, par exemple, les composants se présentent sous la forme de fichiers portants l'extension .so (shared object). Sous les systèmes de type Microsoft Windows, il s'agit des fameuses dll (dynamic library link). On parle également de modules, de bibliothèques, ou de librairies, par abus de traduction (library étant un faux ami et signifiant bibliothèque) Il est possible de créer des composants avec la grande majorité des langages. Toutefois, dans certains cas, notamment pour les langages interprétés ou semi-compilés il n'est pas possible de créer des composants "classiques". Par exemple, en python, les composants sont des simples fichier contenant du code (.py), tandis qu'en java, il est possible de créer des bibliothèques de classes (.jar). Ainsi, seul un programme écrit en java pourra utiliser comme composant un fichier .jar. Un composant regroupe un certain nombre de fonctionnalités qui peuvent être appelés depuis un programme externe, ou client. Comme le composant ne contient que du code compilé, il n'est a priori pas possible de savoir de quelle manière elles sont implémentées, à moins de disposer du code source. De plus, pour pouvoir être utilisé, le composant doit fournir une interface, c'est-à-dire un ensemble de fonctions lui permettant de communiquer avec le programme client. Dans le cas ou le code source n'est pas disponibles, les spécifications détaillées de ces fonctions doivent être fournies avec la documentation. Un composant est censé fournir un service bien précis. Les fonctionnalités qu'il encapsule doivent être en rapport et cohérentes entre elles. On verrait mal l'intérêt d'un composant regroupant les tâches de gestion d'impression et de compression de fichiers, par exemple. Enfin, un composant doit être réutilisable, c'est à dire qu'il ne doit pas simplement servir dans le cadre du projet durant lequel il a été développé. Cet aspect n'est possible qu'à la condition qu'il possède un comportement suffisamment général. Trouver un compromis entre la spécialisation du composant pour optimiser son utilisation dans le cadre du projet actuel, et sa généralisation en vue de sa réutilisation est souvent un casse-tête lors de la phase de conception du projet.

Structure d'un composant

Comme le composant n'est utilisable qu'une fois compilé, la méthode d'implémentation importe peu. Il est possible d'utiliser une approche objet ou procédurale. Il convient d'éviter une confusion courante : la POC et la POO sont des choses différentes. Utiliser la POO pour développer un composant est facultatif. En fait, l'implémentation du composant n'a aucune influence sur les programmes clients, du moment que les signatures de l'interface ne changent pas. Le code d'un composant peut en effet être séparé en deux parties. Tout d'abord, les méthodes et données internes. Un composant peut implémenter des fonctions qu'il utilise "pour son compte personnel", et qui ne sont pas accessibles de l'exterieur. On parle de méthodes "privées". Ensuite, le composant, pour pouvoir être utilisé, doit fournir un moyen de communication avec les programmes clients. Certaines fonctions sont donc accessibles de l'exterieur, et dévolues à être appelées par ces programmes. On parle de méthodes "publiques", ou d'"interface".

Comment fonctionnent les composants ?

Parler des différentes phases de compilation, lien à l'éxécution, etc.

Exemples de programmation de composants dans différents langages

montrer des exemples de codes et d'instructions de compilation qui permettent d'utiliser les composants =La POC dans la gestion de projet informatique=

Les avantages à utiliser la POC

Les avantages à utiliser une approche POC pour conduire un projet sont multiples :

- centralisation des compétences: L'équipe de développement peut-être divisée en sous-groupes, chacun se spécialisant dans le développement d'un composant
- sous traitance: Le développement d'un composant peut-être externalisé, à condition d'en avoir bien réalisé les spécifications au préalable
- facilité de mise à jour: La modification d'un composant ne nécessite pas la recompilation du projet complet
- facilité de livraison/déploiement: Dans le cas d'une mise à jour, d'un correctif de sécurité, ... alors que le logiciel à déjà été livré au client, la livraison en est facilité, puisqu'il n'y a pas besoin de re-livrer l'intégralité du projet, mais seulement le composant modifié
- choix des langages de développement: Il est possible, dans la plupart des cas, de développer les différents composants du logiciel dans des langages de programmation différents. Ainsi, un composant nécéssitant une fonctionnalité particulière pourra profiter de la puissance d'un langage dans un domaine particulier, sans que cela n'influe le développement de l'ensemble du projet
- productivité: La réutilisabilité d'un composant permet un gain de productivité non négligeable car elle diminue le temps de développement, d'autant plus que le composant est réutilisé souvent

Et les inconvénients

Bien que l'utilisation de la POC soit réellement appréciable dans la conduite d'un projet de développement, elle n'est pas sans imposer quelques désagréments et arrachages de cheveux au chef de projet. Tout d'abord, la POC est une méthode dont le bénéfice se voit surtout sur le long terme. En effet, lorsque l'on parle de réutilisation, de facilité de déploiement, c'est que le développement est sinon achevé, du moins bien entamé. Mais factoriser un logiciel en composants nécéssite un important travail d'analyse. La rédaction des signatures des méthodes devra être particulièrement soignée, car modifier une signature nécessitera de retravailler toutes les portions de codes du projet qui font appel au composant, et l'on perdrai alors les bénéfices de l'indépendance des briques logicielles. En un mot, si la POC industrialise le développement, la phase de conception du logiciel prendra un rôle encore plus important. Le fait de ne pas connaître l'implémentation d'un composant (à moins d'avoir accès au source), peut également gêner certains chefs de projets qui veulent garder un contrôle total sur leur logiciel. Catégorie:Programmation informatique

Programmation orientée aspect

La programmation orientée aspect (POA, en anglais aspect-oriented programming - AOP) est un paradigme de programmation qui permet de réduire fortement les couplages entre les différents aspects techniques d'un logiciel. La programmation orientée aspect est basé sur le principe de l'inversion de contrôle (en anglais, IOC, Inversion Of Control). La programmation orientée aspect est une technologie transverse et n'est pas liée à un langage particulier mais peut être mise en œuvre aussi bien avec un langage orienté objet comme Java qu'avec un langage impératif comme le C, le seul prérequis étant l'existence d'un tisseur d'aspect pour le langage cible (cf. § Tisseurs d'aspects) Les concepts de la programmation orientée aspect ont été formulé par Chris Maeda et Gregor Kiczales, qui travaillaient alors pour le Xerox PARC.

Limites technologiques

Les techniques actuelles de conception logicielles et de programmation amènent à découper un logiciel en modules techniques a priori indépendants les uns des autres car gérant des aspects différents du système conçu. Par exemple, on aura différents modules ou couches logicielles pour gérer les aspects techniques suivants :
- gestion des utilisateurs(authentification),
- archivage des données (persistance),
- programmation concurentielle (multi-threading),
- information pendant l'exécution du logiciel (trace),
- logique métier (par exemple informatique de gestion, système d'information géographique, commerce électronique, ...)
- etc. Dans la pratique, on s'aperçoit que ces modules sont en fait intimement liés : c'est l'intrication ou entrecroisement des aspects techniques. Ainsi, une couche logicielle initiallement dédiée à gérer la logique métier applicative (par exemple un système bancaire), va se retrouver dépendante de modules gérant les aspects transactionnels, journalisation, etc., conduisant à une complexification du code, de son développement et de sa maintenance. L'inversion de contrôle mise en œuvre par la programmation par aspect va permettre d'extraire les dépendances entre modules concernant des aspects techniques entrecroisés et de les gérer depuis l'extérieur de ces modules en les spécifiant dans des composants du système à développer nommés aspects.

Principe

Ainsi, au lieu d'avoir un appel direct à un module technique depuis un module métier, ou entre deux modules techniques différents, en programmation par aspect, le code du module en cours de développement est concentré sur le but poursuivi (la logique bancaire, pour reprendre notre exemple) , tandis qu'un aspect est spécifié de façon autonome, prenant en charge de faire appel au module technique requis (l'authentification de l'utilisateur) à un certain point d'exécution du système. Bien sûr, si chaque aspect créé devait lui-même effectuer un appel au modules technique à un point d'exécution explicite, c'est à dire par exemple avec une dépendance directe vers le module métier où devra s'intercaller le code technique, on n'aurait alors fait que décaler le problème. Aussi, l'astuce particulière de la programmation par aspect consiste à utiliser un système d'expressions régulières pour préciser à quel points d'exécution (en anglais, joinpoint) du système l'aspect spécifié devra être activé. Un aspect permet donc de spécifier :
- les points d'action(poincut), qui définissent les points de jonction satisfaisants aux conditions d'activation de l'aspect, donc le ou les moments où l'interaction va avoir lieu,
- les greffons c'est à dire les programmes (advice) qui seront activés avant, autour de ou après les points d'action définis.

Avantages

Le couplage entre les modules gérant des aspect techniques peut être réduit de façon très importante, en utilisant ce principe, ce qui présente de nombreux avantages :
- Maintenabilité accrue : chaque module peut être maintenu indépendamment des autres,
- Meilleure réutilisabilité : tout module peut être réutilisé sans se préoccuper de son environnement. Chaque module implémentant une fonctionnalité technique précise, on n'a pas besoin de se préoccuper des évolutions futures : des nouvelles fonctionnalités pourront être implémentées dans de nouveaux modules qui intéragiront avec le système au travers des aspects.
- Gain de productivité : le programmeur ne se préoccupe que de l'aspect de l'application qui le concerne, ce qui simplifie son travail, et permet d'augmenter la parallélisation du développement.
- Amélioration de la qualité du code : La simplification du code qu'entraine la programmation par aspect permet de le rendre plus lisible et donc de meilleure qualité.

Désavantages

Le tissage d'aspect qui n'est finalement que de la génération automatique de code inséré à certains points d'exécution du système développé, produit un code qui peut être difficile à analyser (parce que généré automatiquement) lors des phases de mise au point des logiciels (déboggage, test). Ceci dit, une implémentation comme AJDT, basée sur AspectJ, offre des outils sophistiqués qui permettent de passer de façon transparente, en mode déboggage, du code d'une classe à celui d'un aspect.

Lexique

La programmation orientée aspect, parce qu'elle propose un paradigme de programmation et de nouveaux concepts, à développé un jargon bien spécifique qui ne facilite pas sa compréhension de ses concepts, qui sont, en définitive simples - mais puissants.
- aspect : un module définissant des greffons et leurs points d'activation,
- greffon (en anglais, advice) : un programme qui sera activé à un certain point d'exécution du système, précisé par un point de jonction,
- tissage ou tramage (en anglais, weaving) : insertion statique ou dynamique dans le système logiciel de l'appel aux greffons,
- point d'action, de coupure, de greffe (en anglais, pointcut) : endroit du logiciel ou est inséré un greffon par le tisseur d'aspect,
- point de jonction, d'exécution (en anglais, jointpoint) : endroit spécifique dans le flot d'exécution du système, où il est valide d'insérer un greffon. Pour clarifier le propos, il n'est pas possible, par exemple, d'insérer un greffon au milieu du code d'une fonction. Par contre on pourra le faire avant, autour de, à la place ou après l'appel de la fonction.
- considérations entrecroisées (en anglais, cross-cutting concerns) : mélange, au sein d'un même programme de sous-programmes distincts couvrant des aspects techniques séparés.

Implémentation

Stratégies

Deux grandes stratégies de tissage d'aspects existent :
- le tissage statique par instrumentation du code source ou du pseudo-code machine intermédiare (bytecode java, IL)
- le tissage dynamique lors de l'exécution du logiciel (implémentée par exemple par JAC)

Tisseurs d'aspects


- En Java :
  - [http://eclipse.org/aspectj/ AspectJ] : Extension du langage Java nécessitant donc une étape de précompilation. Le résultat est toutefois du bytecode Java standard.
  - [http://www.aopsys.com/jac.html JAC] (Java Aspect Components) : Framework 100% Java.
- En C++ :
  - [http://www.aspectc.org/ AspectC++]
- En C#, VB.NET :
  - [http://sourceforge.net/projects/aspectdng/ AspectDNG]
- En PHP :
  - [http://phpaspect.org PHPaspect]
- En C :
  - [http://www.cs.ubc.ca/labs/spl/projects/aspectc.html Aspect-C]
- En Caml :
  - [http://www.cs.iastate.edu/~leavens/FOAL/papers-2005/tatsuzawa-masuhara-yonezawa.pdf Aspectual Caml]

Références


- Renaud Pawlak, Jean-Philippe Retaillé, Lionel Seinturier, Programmation orientée aspect pour Java / J2EE, Eyrolles, 2004, 446 p., ISBN 2212114087
- Ramnivas Laddad, AspectJ in Action: Practical Aspect-Oriented Programming, Manning Publications, 2003, 512 p., ISBN 1930110936
- Russell Miles, AspectJ Cookbook, O'Reilly, 2004, 360 p., ISBN 0596006543
- Thomas Gil, Conception Orientée Aspects, DotNetGuru Editions, 2004, 260 p., [http://www.dotnetguru.biz/ebooks/opencontent/DNG-Book-ConceptionOrienteeAspects-Gil-2004.pdf Gratuit En Ligne]

Liens externes


- [http://www.dotnetguru.org/articles/dossiers/aop/quid/AOP15.htm AOP, Intérêts et Usages] Catégorie:Programmation informatique ja:アスペクト指向プログラミング

Ramasse-miettes

Un ramasse-miettes, ou récupérateur de mémoire, ou glaneur de cellules (en anglais garbage collector, abrégé en GC) est un sous-système informatique de gestion automatique de la mémoire. Il est responsable du recyclage de la mémoire préalablement allouée puis inutilisée. Lorsqu'un système dispose d'un ramasse-miette, ce dernier fait généralement partie de l'environnement d'exécution associé à un langage de programmation particulier. Le ramassage des miettes a été inventé par John McCarthy comme faisant partie du premier système Lisp.

Définition et fonctionnement

Le principe de base de la récupération automatique de la mémoire est simple :
- déterminer quels objets dans le programme ne peuvent pas être utilisés,
- récupérer le stockage utilisé par ces objets. Bien qu'en général il soit impossible de déterminer à l'avance à quel moment un objet ne sera plus utilisé, il est possible de le découvrir à l'exécution : un objet sur lequel le programme ne maintient plus de référence, donc devenu inaccessible, ne sera plus utilisé.

Principe d'accessibilité d'un objet

Les ramasse-miettes utilisent un critère d'accessibilité pour déterminer si un objet peut être potentiellement utilisé. Les principes sont :
- un ensemble distinct d'objets qui sont supposés atteignables, ce sont les racines. Dans un système typique ces objets sont les registres machine, la pile, le pointeur d'instruction, les variables globales. En d'autres termes tout ce qu'un programme peut atteindre directement.
- tout objet référencé depuis un objet atteignable est lui-même atteignable. Dit autrement : un objet atteignable peut être obtenu en suivant une chaîne de pointeurs ou de références. Bien évidemment, un tel algorithme est une approximation conservatrice de l'objectif idéal de destruction des valeurs ne servant plus : certaines valeurs peuvent fort bien être accessibles depuis les racines mais ne plus jamais être utilisées. Cet objectif idéal est cependant inaccessible algorithmiquement : déterminer quelles valeurs serviront dans le futur est équivalent au problème de l'arrêt. Cette approximation conservatrice est la raison de la possibilité de fuites de mémoire, c'est-à-dire de l'accumulation de blocs de mémoire qui ne seront jamais réutilisés, mais jamais libérés non plus. Par exemple, un programme peut conserver un pointeur sur une structure de donnée qui ne sera jamais réutilisée. Il est pour cette raison recommandé d'écraser les pointeurs vers des structures inutilisées, afin d'éviter de conserver des références inutiles.

Algorithme de base

L'algorithme du ramasse-miettes est dû à Schorr et Waite. Les ramasse-miettes effectuent des cycles de ramassage. Un cycle est démarré lorsque le récupérateur décide (ou est notifié) qu'il doit récupérer de l'espace de stockage. Un cycle est constitué des étapes suivantes :
- Créer des ensembles dit noir, gris et blanc. Initialement, l'ensemble noir est vide, l'ensemble gris contient les objets « racines » et éventuellement certains objets supplémentaires choisis en fonction de l'algorithme particulier employé, et l'ensemble blanc contient tout le reste. À tout moment dans l'exécution de l'algorithme, un objet ne peut être que dans un seul des trois ensembles. L'ensemble blanc peut être vu comme l'ensemble des objets dont nous essayons de récupérer l'espace mémoire ; au cours du cycle, l'algorithme ôtera des objets de l'ensemble blanc, y laissant les objets dont il peut réclamer l'espace mémoire.
- Choisir un objet de l'ensemble gris, déplacer cet objet vers l'ensemble noir, déplacer tous les objets blancs référencés directement par cet objet vers l'ensemble gris. Cette étape est répétée jusqu'à ce qu'il n'y ait plus d'objets dans l'ensemble gris.
- Quand il n'y a plus d'objets dans l'ensemble gris, alors tous les objets restant dans l'ensemble blanc ne sont pas atteignables, et l'espace mémoire qu'ils utilisent peut être réclamé. L'invariant des trois couleurs peut être traduit comme ceci : aucun objet noir ne pointe directement sur un objet blanc. Observons que l'algorithme ci-dessus préserve l'invariant des trois couleurs. La partition initiale n'a pas d'objet noir, de sorte que l'invariant est trivialement respecté. Par la suite, si un objet devient noir, tous ses fils directs (les objets qu'il référence) deviennent gris, ceci préservant l'invariant. Lorsque la dernière étape de l'algorithme est réalisée, parce que l'invariant est préservé, aucun des objets de l'ensemble noir ne pointe vers un objet de l'ensemble blanc (et il n'y a plus d'objet gris) ce qui signifie que les objets blancs résiduels sont inatteignables depuis les racines. Le système peut alors appeler leurs destructeurs et libérer leur espace mémoire. Certaines variantes de l'algorithme ne respectent pas l'invariant des trois couleurs, mais ils utilisent un principe différent par lequel toutes les propriétés importantes sont respectées.

Variantes

L'algorithme de base a plusieurs variantes.
- Les ramasse-miettes qui déplacent les objets en mémoire (qui changent leur adresse), dits stop and copy.
- Certains récupérateurs peuvent correctement identifier toutes les références à un objet : ils sont appelés des récupérateurs « exacts », par opposition avec des récupérateurs « conservateurs » ou « partiellement conservateurs ». Les ramasse-miettes « conservateurs » doivent présumer que n'importe quel suite de bits en mémoire est un pointeur si (lorsqu'ils sont interprétées comme un pointeur) il pointe sur un quelconque objet instancié. Ainsi, les récupérateurs conservateurs peuvent avoir des faux négatifs, où l'espace mémoire n'est pas réclamé à cause des faux pointeurs accidentels. En pratique ceci est rarement un gros inconvénient.
- Le ramasse-miettes peut s'exécuter en alternance ou en parallèle avec le reste du système ; les récupérateurs les plus simples suspendent l'exécution du système lorsqu'ils exécutent un cycle ; ils ne sont pas incrémentaux ; les récupérateurs incrémentaux entrelacent leur travail pour s'exécuter pendant les temps d'inactivité du reste du système. Certains récupérateurs incrémentaux peuvent s'exécuter complètement en parallèle dans un thread séparé ; ils peuvent en théorie s'exécuter sur un processeur différent, mais le coût de la mise en cohérence des caches rend cette approche moins pratique qu'il n'y paraît.

Classification des ramasse-miettes

Les récupérateurs peuvent être classés en considérant la façon dont ils implémentent les trois ensembles d'objets blancs, gris et noirs.

Marquage et nettoyage

Ou mark and sweep en anglais. Un ramasse-miettes de ce type maintient un bit (ou deux) associé à chaque objet pour indiquer s'il est blanc ou noir ; l'ensemble gris est maintenu soit comme une liste séparée ou en utilisant un autre bit. Un récupérateur copieur distingue les objets gris et noirs en les copiant vers d'autres zones mémoire (l'espace de copie) et souvent différencie les objets noirs des objets gris en bi-partitionnant l'espace de copie (dans le cas le plus simple en maintenant un unique pointeur qui indique la séparation entre les objets noirs et gris).

Récupérateur à générations

Ou generational GC en anglais. Toutes les données d'un programme n'ont pas la même durée de vie. Certaines sont éliminables très peu de temps après leur création (par exemple, une structure de donnée créée uniquement pour retourner une valeur d'une fonction, et démantelée dès que les données en ont été extraites). D'autres persistent pendant toute la durée d'exécution du programme (par exemple, des tables globales créées pendant l'initialisation). Un ramasse-miette traitant toutes ces données de la même façon n'est pas forcément des plus efficaces. Une solution serait de demander au programmeur d'étiqueter les données créées selon leur durée de vie probable. Cependant, cette solution serait lourde à utiliser ; par exemple, il est courant que les données soient créées dans des fonctions de bibliothèque (par exemple, une fonction créant une table de hachage), il faudrait leur fournir les durées de vie en paramètre. Une méthode moins invasive est le système des générations. Le ramasse-miette opère alors sur une hiérarchie de 2 ou plus générations, étagées de la plus « jeune » à la plus « âgée ». Les données nouvellement créées sont (en général) placées dans la génération la plus jeune. On ramasse assez fréquemment les miettes dans cette génération jeune ; les données encore présentes à l'issue de la destruction des données inaccessibles de cette génération sont placées dans la génération d'âge supérieur, et ainsi de suite. L'idée est que les données de plus courte durée de vie n'atteignent, pour la plupart, pas la génération supérieure (elle peuvent l'atteindre si elles viennent d'être allouées quand le ramassage de miettes les repère dans la génération jeune, mais c'est un cas rare). On utilise généralement 2 ou 3 générations, de tailles croissantes. Généralement, on n'utilise pas le même algorithme de ramasse-miette pour les diverses générations. Il est ainsi courant d'utiliser un algorithme non incrémental pour la génération la plus jeune : en raison de sa faible taille, le temps de ramasse-miette est faible et l'interruption momentanée de l'exécution de l'application n'est pas gênante, même pour une application interactive. Les générations plus anciennes sont plutôt ramassées avec des algorithmes incrémentaux. Le réglage des paramètres d'un ramasse-miettes à génération peut être délicat. Ainsi, la taille de la génération la plus jeune peut influencer de façon importante le temps de calcul (un surcoût de 25%, par exemple, pour une valeur mal choisie) : temps de ramasse-miette, impact sur la localité du cache... Par ailleurs, le meilleur choix dépend de l'application, du type de processeur et d'architecture mémoire.

Comptage de références

Une solution qui vient vite à l'esprit pour la libération automatique de zones de mémoire est d'associer à chacune un compteur donnant le nombre de références qui pointent sur elle. Ces compteurs doivent être mis à jour à chaque fois qu'une référence est créée, alterée ou détruite. Lorsque le compteur associé à une zone mémoire atteint zéro, la zone peut être libérée. Cette technique souffre d'un inconvénient certain lors de l'usage de structures cycliques : si une structure A pointe sur une structure B qui pointe sur A (ou, plus généralement, s'il existe un cycle dans le graphe des références), mais qu'aucun pointeur extérieur ne pointe ni sur A ni sur B, les structures A et B ne sont jamais libérées : leurs compteurs de références sont strictement supérieurs à zéro (et comme il est impossible que le programme accèdent à A ou B, ces compteurs ne peuvent jamais repasser à zéro). En raison de ces limites, certains considèrent que le comptage de références n'est pas une technique de récupération de mémoire à proprement parler ; ils restreignent le terme de récupération de mémoire à des techniques basées sur l'accessibilité. Le comptage de références souffre de certains problèmes sérieux, comme son coût élevé en temps de calcul et aussi en espace mémoire et, comme on l'a vu, l'impossibilité de gérer les références circulaires. D'un autre côté, il récupère les « miettes » plutôt vite, ce qui présente des avantages s'il y a des destructeurs à exécuter pour libérer les ressources rares autres (sockets...) que le tas (mémoire). Des systèmes hybrides utilisant le comptage de références pour obtenir la libération quasi immédiate des ressources, et appelant à l'occasion un récupérateur de type Mark and Sweep pour libérer les objets contenant des cycles de références, ont été proposés et parfois implémentés. Cela donne le meilleur des deux mondes, mais toujours au prix d'un coût élevé en termes de performances.

Langages dotés de ramasse-miettes


- Common Lisp, Scheme, Smalltalk, Caml, Haskell, Prolog, Oz
- interpréteurs de commandes et langages de scripts comme Bash, Csh, Korn shell, etc.
- Java, C#, Eiffel
- Perl, Javascript, Ruby
- Le langage de programmation Python utilise un mécanisme de comptage de références doublé depuis la version 2.2 d'un garbage collector pour la destruction des cycles.

Avantages et inconvénients des ramasse-miettes

Les langages utilisant un ramasse-miettes permettent d'écrire des programmes plus simples et plus sûrs. La mémoire étant gérée automatiquement par l'environnement d'exécution, le programmeur est libéré de cette tâche, source de nombreuses erreurs difficiles à débusquer. La gestion manuelle de la mémoire est l'une des sources les plus courantes d'erreur. Trois types principaux d'erreurs peuvent se produire :
- l'accès à une zone non allouée, ou qui a été libérée,
- la libération d'une zone déjà libérée,
- la non-libération de la mémoire inutilisée (fuites mémoire). L'utilisation d'outils et de méthodologie appropriés permet d'en réduire l'impact, tandis que l'utilisation d'un ramasse-miettes permet de les éliminer presque complétement – les fuites de mémoire restent possibles, bien que plus rares. Cette simplification du travail de programmation peut présenter quelques inconvénients, principalement au niveau des performances des programmes les utilisant. Des mesures montrent que dans certain cas l'implémentation d'un ramasse-miettes augmente les performances d'un programme, dans d'autre cas le contraire se produit. Le choix des paramètres du ramasse-miette peut aussi altérer ou améliorer significativement les performances d'un programme. Lorsque le ramasse-miette effectue de nombreuses opérations de copies en tâche de fond (cas de l'algorithme stop-and-copy), il tend à défragmenter la mémoire. Le ramasse-miettes peut ainsi se révéler plus rapide qu'un codage ad-hoc de l'allocation/désallocation. Les meilleures implémentations peuvent aussi optimiser l'utilisation des caches mémoires, accélérant ainsi l'accès aux données. À contrario, l'opération de collection est souvent coûteuse. Il est difficile de borner le temps d'exécution de la phase de collection des objets non atteignables. L'utilisation d'un ramasse-miettes standard peut donc rendre difficile l'écriture de programmes temps réel ; un ramasse-miettes spécialisé (temps-réel) doit être utilisé pour cela. Sans intervention du programmeur, un programme utilisant un ramasse-miettes a tendance à utiliser plus de mémoire qu'un programme où la gestion est manuelle (en admettant que, dans ce cas, il n'y a pas de fuites, d'erreur d'accès ou de libération). Toutefois, rien n'interdit d'employer des stratégies de pré-allocation des objets utilisés, dans des pools, lorsqu'on veut minimiser le taux d'allocation/désallocation. Dans ce cas, le ramasse-miettes fournit toujours le bénéfice d'une programmation sans erreur grave de gestion de la mémoire (une assurance). Bien que ce ne soit pas le but d'un ramasse-miettes son implémentation peut aussi faciliter l'implémentation de la persistance d'objet (certains algorithmes sont partagés).

Citations

« Il est dit que les programmeurs Lisp savent que la gestion de la mémoire est si importante qu'elle ne peut être laissée aux programmeurs, et que les programmeurs C savent que la gestion de la mémoire est si importante qu'elle ne peut être laissée au système » -- Bjarne Stroustrup peut-être tiré d'une source antérieure.

Voir aussi

Liens externes


- [http://www.dotnetguru.org/articles/GC/GC.html Les Ramasses-Miettes .NET et Java ]
- http://www.memorymanagement.org/ (textes en anglais)
- [http://www.cs.utexas.edu/users/oops/papers.html Publications du groupe OOPS de l'Université du Texas] (textes en anglais)
- [http://www.cs.kent.ac.uk/people/staff/rej/gc.html Resources sur les ramasses-miettes] (textes en anglais)

Références

H. Schorr, W.M. Waite, An Efficient Machine-Independent Procedure for Garbage Collection in Various List Structures. CACM Août 1967 Catégorie:Algorithmique ja:ガベージコレクション

Système de gestion d'exceptions

Sujets connexes :
- interruptions
- notion de continuation =Principes= Nous étudions les SGE dans le contexte des langages de programmation fonctionnels et impératifs.

Justifications

Tout programme en exécution peut être sujet à des erreurs, pour lesquels des stratégies de détection et de réparation sont possibles. Ces erreurs ne sont donc pas des bogues des programmes, mais des conditions particulières, on parle aussi de conditions exceptionnelles ou exceptions dans le déroulement normal d'une partie d'un programme. Par exemple, l'absence d'un fichier utile n'est pas un bogue du programme ; par contre, ne pas dire quoi faire en son absence en serait un. Le traitement des situations exceptionnelles fait apparaître deux besoins :
- une syntaxe spéciale, pour distinguer l'exécution normale du traitement des erreurs,
- un flot de contrôle "non local", pour traiter et réparer les situations exceptionnelles. Dans les langages de programmation sans SGE, on n'a pas d'outil pour séparer l'exécution normale et l'exécution exceptionnelle du programme. Un algorithme, dont l'exécution normale s'exprime de façon simple et élégante, peut devenir illisible (et donc difficile à maintenir) une fois « enrobé » par une logique de traitement des situations exceptionnelles ; disposer au niveau du langage de programmation d'une syntaxe pour différencier l'exécution normale de l'exécution dans un contexte exceptionnel peut être utile. Le traitement d'une situation exceptionnelle peut nécessiter de revenir "dans le passé" de l'exécution du programme, c'est-à-dire remonter brutalement la chaîne d'appels pour annuler une opération fautive, ou encore modifier les valeurs de certaines variables, puis reprendre l'exécution du programme un peu avant le site de l'erreur. D'où le besoin d'associer, à la syntaxe spéciale, des opérateurs spéciaux pour effectuer des sauts et des modifications de variables à des points arbitraires de la chaîne d'appels.

Définitions

Dans un langage de programmation, un système de gestion d'exceptions (SGE) est conçu pour traiter les erreurs pouvant survenir à l'exécution d'un programme : c'est un mécanisme, associé à un ensemble de constructions syntaxiques. Le programmeur utilisant un SGE doit identifier les parties du programme devant être protégées de certaines erreurs à l'exécution ; l'occurrence de ces erreurs est bien sûr connue à l'avance, ainsi que la stratégie à adopter pour leur traitement.

Types d'erreurs

Les erreurs à l'exécution suivantes peuvent survenir dans tout programme :
- arithmétique (débordement, division par zéro, ...)
- collections (débordement d'indices)
- allocation mémoire
- signaux système D'autres erreurs peuvent être interceptées par le compilateur, par exemple les erreurs de type, dans les langages à typage statique. Dans les langages à objets, le type d'une exception est déterminé par sa classe. Ces langages fournissent une hiérachie prédéfinie et extensible de types d'exceptions, correspondant au type des erreurs qu'elles représentent.

Gestionnaire d'exceptions

Le premier outil est donc le gestionnaire d'exceptions. Un gestionnaire d'exception établit un ensemble de routines de traitement d'erreurs, définies par le programmeur, sur un bloc (dans une fonction ou une méthode du programme) ; ces routines sont activées pendant toute la durée d'exécution du bloc protégé. La notion d'exécution du bloc protégé inclut toute la chaîne d'appels (de fonctions, procédures ou méthodes) à partir de ce bloc : on dit que les gestionnaires d'exception sont actifs dans la portée dynamique du bloc. Le signalement d'une exception peut être automatique, s'il correspond à une exception définie dans le langage de programmation ou une bibliothèque fournie, ou bien déclenché par le programmeur par l'utilisation d'une primitive de signalement. Il est généralement possible de construire de nouveaux types d'exceptions et de programmer leur signalement. Le gestionnaire d'exception peut être complété par un ensemble de restarts, qui sont des routines permettant la modification des environnements lexicaux entre le site de signalement et le point d'établissement des gestionnaires d'exception. Un restart permet à un gestionnaire d'exception de choisir de réparer et redémarrer un calcul plutôt que de l'abandonner intégralement. Un restart est également actif dans la portée dynamique du bloc sur lequel il est défini.

Signalement d'une erreur

Lorsqu'une condition d'erreur est détectée (par une primitive de signalement, une trappe processeur, le système d'exploitation ou encore l'environnement d'exécution du programme), on dit qu'elle est signalée : un bloc de traitement d'erreur (un handler) est recherché dans la liste des gestionnaires actifs. L'exécution du programme est déférée au bloc de traitement, qui effectue des actions correctrices et décide si le calcul où l'erreur a été signalée est terminé ou bien repris (si c'est possible, c'est-à-dire en présence de restarts). Il se peut qu'aucun gestionnaire n'ait été prévu par le programmeur, auquel cas un gestionnaire par défaut, dont le comportement est pré-défini, est sélectionné.

Réparation d'une erreur, Reprise

Dans un bloc de traitement, le programmeur a deux options :
- arrêt brutal du calcul fautif et choix d'un autre calcul : la pile d'appel du bloc protégé est alors détruite,
- réparations en différents points de la chaîne d'appel ayant conduit à l'erreur, et reprise du calcul à un lieu préalablement défini. Il faut noter que les langages de programmation les plus populaires à l'heure actuelle (par exemple, C++, Java, Python), lorsqu'ils offrent un SGE, ne permettent que la terminaison pure et simple du bloc fautif. On ne peut réparer ni reprendre les opérations. =Opérateurs= Signalement d'une condition d'erreur (ne détruit pas la pile) :
- signal, error, cerror, ... L'installation d'un bloc de traitement d'erreur est réalisé avec des primitives du type :
- try/catch/finally, handler-bind, ... L'installation d'un restart (un bloc de réparation de contexte lexical) est permise par :
- restart-case, restart-bind ... La destruction de la pile d'appel entre l'expression signalant une exception et le bloc de traitement, ou un point de reprise est effectuée par :
- throw, raise En Java ou Python, par exemple, le signalement implique la destruction de la pile d'appels jusqu'au premier bloc de traitement disponible. Dans ce cas, throw (ou raise) est la seule primitive de signalement, et la réparation et la reprise sont impossibles. =Dans les langages de programmation= Avec Java, SmallTalk et CommonLisp, nous essayons de montrer les spécificités du SGE de différents langages.

Java

Java offre un SGE terminal, donc sans réparation ni reprise.

Syntaxe

try catch TypeException1 e1 catch TypeException2 e2 finally

Exemple

Un exemple simpliste de traitement d'une division par zéro : public class FooBar

Particularités

Dans CLU [Lyskov 92] et Java, une distinction est faite entre :
- les exceptions déclarées dans la signature d'une méthode (CheckedException en Java) ; par exemple void foo () throws ThisExceptionType ,
- les exceptions à l'exécution (RunTimeException en Java), qui correspondent à des évènements impossibles à localiser lexicalement à la compilation (les exceptions asynchrones), ou pouvant survenir à tout moment dans l'exécution du programme, comme les problèmes d'allocation mémoire. Les "checked exceptions" essayent de résoudre un problème de contrat. L'interface d'un module (d'une bibliothèque de classes) représente un contrat entre l'auteur du module et son utilisateur : l'argument est qu'un tel contrat ne devrait pas passer sous silence les exceptions susceptibles d'être propagées hors des frontières du module. En spécifiant les exceptions dans les signatures des méthodes, on introduit toutefois un problème. En effet, les méthodes clientes doivent choisir dans l'alternative :
- installer un GE pour les exceptions du module, ou bien
- déclarer à leur tour ces exceptions. Les méthodes utilisant des checked exceptions contaminent leurs clients avec l'obligation de décorer leur signature, s'ils n'installent pas de GE pour ces exceptions. Cette contamination trahit en partie le contrat d'indépendance entre le lieu du signalement d'une exception et le lieu de son traitement, en exposant toutes ces déclarations d'interfaces dans le chemin d'appel ; en somme elles nous ramènent aux inconvénients des langages de programmation sans SGE (transmission de l'exception par une valeur de retour spéciale, prévue en tout point de la chaîne d'appels). Les "checked exceptions" violent finalement la philosophie des exceptions (la non-localité entre le lieu de la condition et le lieu de son traitement). Du coup, la bibliothèque standard de Java utilise en pratique des runtime exceptions pour les opérateurs les plus courants (arithmétique, collections, allocation mémoire) afin éviter la pollution lexicale des checked exception.

Smalltalk

Les SGE à terminaison portent une vue radicale et rigide sur les exceptions : toute exception signalée entraîne la mort du signaleur. Le programmeur ne peut souvent utiliser le SGE que comme un « pare-feu » contre les situations exceptionnelles, pour contenir une situation urgente et irréparable, sauver ce qui peut l'être avant d'abandonner le navire ... En pratique, les exceptions signalées peuvent n'être que relativement bénignes, ou transitoires ; dans ce cas, un idiome combinant le SGE, des variables, des tests, des boucles, doit être mis en œuvre pour recommencer un calcul qui aurait échoué pour des raisons bénignes. De tels idiomes sont la réponse ad-hoc du programmeur enfermé dans un SGE sans réparation et reprise du contexte signalant. En Smalltalk, ces difficultés sont mitigées par les possibilités suivantes :
- réessayer un bloc protégé, avec de nouveaux arguments,
- reprendre l'exécution du calcul signalant, éventuellement en fournissant une "valeur de retour".

Ré-essayer

Les mots clefs retry et retryUsing: permettent, respectivement d'exécuter à nouveau le bloc protégé par le handler sans utiliser de bouclage explicite, ou d'exécuter un nouveau bloc à la place du bloc qui a signalé l'exception. Voici un exemple : | fileContents | fileContents := ['myfile.txt' asFilename readStream contents] on: Error do: [:ex | | newName | newName := Dialog prompt: 'Problem reading file. Another name?'. ex retryUsing: [newName asFilename readStream contents]]

Reprendre

Certaines exceptions sont dites « continuables ». Cela signifie qu'un gestionnaire peut envoyer un message resume (ou resume: qui transmet son argument au retour de l'expression signalante) à l'exception, ce qui provoque le transfert du contrôle sur le retour de l'expression signalante. Voyons un exemple, sur un bout de programme effectuant la lecture des « options » d'un fichier de configuration (couples variable = valeur). Le premier fragment analyse la prochaine option située dans un stream représentant le fichier : MyApplication>>readOptionsFrom: aStream | option | [aStream atEnd] whileFalse: [option := self parseOptionString. "nil if invalid" option isNil ifTrue: [InvalidOption signal] ifFalse: [self addOption: option]] Le second fragment utilise le premier pour lire la configuration complète ; le gestionnaire de l'exception « InvalidOption » y est défini. MyApplication>>readConfiguration [self readOptionsFrom: 'options' asFilename readStream] on: InvalidOption do: [:ex | (Dialog confirm: 'Invalid option line. Continue loading?') ifTrue: [ex resume] ifFalse: [ex return]]

Conséquence sur le signalement

Puisqu'on a introduit la possibilité de reprendre un calcul sur l'instruction suivant le signalement, on doit se garder de détruire la pile d'appels au moment du signalement : cette destruction doit prendre place au moment où le programme sort du dernier gestionnaire impliqué dans le signalement.

Le Système de Conditions de Common Lisp

Les SGE des langages précédents ne considèrent pas la possibilité de réparer le contexte signalant l'exception et de redémarrer le calcul dans le contexte ainsi réparé. SmallTalk permet de fournir une valeur de retour de substitution pour l'expression signalant une exception, mais le gestionnaire n'a pas accès à l'environnement lexical fautif. Nous étudions une partie du Condition System de Common Lisp (SCCL).

Définitions

Une condition est une généralisation d'une erreur [Pitman 01] : toutes les conditions ne sont pas indésirables. À la hiérarchie des types d'exceptions des SGE à terminaison correspond une hiérarchie de types de conditions, incluant une branche pour les conditions non-fatales. Cette hiérarchie est décrite avec le Common Lisp Object System, c'est donc également une hiérarchie de classes d'exceptions. Dans le SCCL, un bloc de traitement d'un gestionnaire de condition est une fonction fermée sur l'environnement lexical du gestionnaire d'exception, et qui s'exécute dans l'environnement dynamique de la routine où la condition est signalée ; le contexte dynamique de la routine signalante n'est pas détruit. Cela signifie que le signalement n'implique pas de transférer le flot de contrôle de façon non locale : on ne détruit pas la pile d'appels au moment du signalement. Le signalement débute par un appel de fonction au GE adéquat ; il peut être écrit comme suit : (defun signal (condition) (funcall (find-first-active-handler-of-type (type-of condition)) condition)) Un restart est une fonction contenant les instructions nécessaires à la réparation d'une situation exceptionnelle, et fermée sur un environnement lexical proche du lieu du signalement. Il est donc situé, dans la chaîne d'appels, entre le GE adéquat et la condition signalée. Un restart est typiquement invoqué par le gestionnaire d'une condition pour modifier l'environnement lexical ou dynamique de la procédure signalant la condition (réparer la situation) et effectuer un saut non-local vers un point de cette procédure (reprise).

Opérateurs

Nous mentionnons les opérateurs les plus significatifs du système de conditions. (catch symbol) et (throw symbol) sont à la disposition du programmeur Lisp pour, respectivement, marquer avec un symbole le cadre courant, et détruire la pile d'appels en la remontant jusqu'à la première marque correspondant au symbole passé en argument. Ils sont utilisés implicitement par le système de condition. Il est important de noter que si handler-bind implique un catch, les primitives de signalement ne débutent jamais par un throw. Throw n'est invoqué que si le gestionnaire effectue un saut non-local vers son contexte lexical (qui est différent de son contexte dynamique au moment où il est appelé), ce qui signifie qu'on veut effectivement détruire la pile d'appels.

Utilisation et Fonctionnement

Utilisation terminale

Le SCCL peut être utilisé juste comme les SGE à terminaison : on établit un gestionnaire, on signale, on se demande quoi faire. 1 . (defun foo () 2 . (tagbody 3 . (print « foo ») 4 . (handler-bind ((some-condition 5 . (lambda (condition) 6 . (print (type-of condition)) 7 . (go after-the-fact)))) 8 . (bar)) 9 . after-the-fact 10. (print "après bar, dans foo")) 11. 12. (defun bar () 13. (print « bar ») 14. (error "C'est bien une erreur" 'some-condition) 15. (print "ce code est inatteignable")) Examinons cet exemple, du point de vue du flot de contrôle. La trace d'un appel à foo est : 3 . foo 13. bar 6 . SOME-CONDITION 10. après bar, dans foo La décision de détruire la pile d'appels est prise par le "(go after-the-fact)" du gestionnaire pour some-condition. Le système Lisp doit faire un throw juste avant d'exécuter le go.

Utilisation d'un restart

Le schéma suivant exprime les étapes mises en œuvre lorsqu'on utilise un restart. environnement dynamique Reprenons ces étapes (cas de la reprise) : 1. établissement du gestionnaire G (pour le type de condition C) dans l'environnement dynamique du programme (cela signifie que G est accessible en tout cadre de la chaîne d'appels sous son cadre d'établissement) ; G peut capturer des variables lexicales du cadre où il est déclaré (c'est une fermeture lexicale), 2. appel de la forme « protégée » par G, 3. appel à une nouvelle procédure, d'où sera signalée une condition de type C, 4. établissement d'un restart R protégeant une expression de cette procédure, dans l'environnement lexical de cette dernière, 5. l'expression protégée de la procédure signale une condition de type C : le gestionnaire G est trouvé dans l'environnement dynamique (c'est le gestionnaire actif le plus récent pour les conditions de type C), 6. G décide de reprendre le calcul, il invoque le restart R, 7. R effectue, dans le contexte lexical de la procédure signalante une réparation (si besoin) et tranfère le contrôle à la procédure (saut non-local), qui reprend son travail (selon toute raison, en #4). Bien sûr, si le gestionnaire décide d'abandonner le calcul (#6 bis), un saut non-local est effectué vers le cadre où G a établi ses liaisons lexicales (et en particulier l'étiquette utilisée pour le saut) ; la pile d'appels est détruite, R est détruit, et juste après le saut, G lui-même est détruit.

Exemple avec reprise

Nous développons l'idée d'utiliser le système de conditions pour exploiter les résultats d'un solveur de contraintes. Un solveur de contrainte, lorsqu'il trouve une solution, la signale à la routine qui a demandé le calcul. Si la routine est satisfaite de la solution, elle peut arrêter le solveur ; elle peut également reprendre le calcul pour obtenir la solution suivante. On commence par définir un nouveau type de condition, correspondant à une solution trouvée par le solveur, avec un slot pour la solution : (define-condition backtrack-solution (condition) ((sol :initarg solution :reader solution))) On établit un gestionnaire d'exceptions dans la routine qui a besoin des résultats du solveur ; ici, on choisit d'analyser le résultat du solveur au fur et à mesure de sa progression : 1 . (defun foobar (...) 2 . (let ((solutions-we-dont-like)) 3 . (handler-bind 4 . ((backtrack-solution ; type de la condition 5 . (lambda (condition) 6 . ;; on décide quoi faire 7 . (let ((sol (solution condition)) 8 . (when (good-enough sol) 9 . (return-from foobar sol)) 10. (push sol solutions-we-d