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
- 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 LanguageCaté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
Algorithmique ko:알고리즘 ja:アルゴリズム th:อัลกอริทึม
Catégorie:Algorithmique
On nomme algorithmique la science des algorithmes, visant à étudier les opérations nécessaires à la réalisation d'un calcul. On parle également de procédé ou de procédure. Une recette de cuisine constitue par exemple un algorithme parfaitement défini.
Historique
Antiquité
Les algorithmes dont on a retrouvé des descriptions exhaustives ont été utilisés dès l'époque des Babyloniens, pour des calculs concernant le commerce et les impôts.
L'algorithme le plus célèbre est celui attribué à Euclide permettant de trouver le plus grand commun diviseur de deux nombres.
Etude systématique
L'algorithmique a été systématisée par le mathématicien persan Al Kwarizmi (780-850), auteur d'un ouvrage décrivant des méthodes de calculs algébriques (ainsi que d'un autre introduisant le zéro des Indiens). Son nom donna au moyen-âge le nom "algorisme" qui devint algorithme avec lady Ada Lovelace, fille de lord Byron et assistante de Charles Babbage (1792-1871).
On peut voir une allusion à la méthode algorithmique chez René Descartes dans le Discours de la méthode : « diviser chacune des difficultés que j'examinerois, en autant de parcelles qu'il se pourroit, et qu'il seroit requis pour les mieux résoudre. » Néanmoins, cette approche ne parle ni de boucles, ni d'itérations.
Le substantif algorithmique désigne la méthode utilisant des algorithmes. Le terme est également employé comme adjectif.
Un algorithme énonce une résolution sous la forme d'une série d'opérations à effectuer. La mise en œuvre de l'algorithme consiste en l'écriture de ces opérations dans un langage de programmation et constitue alors la brique de base d'un programme informatique.
Les informaticiens utilisent fréquemment l'anglicisme implémentation pour désigner cette mise en œuvre. L'écriture en langage informatique est aussi fréquemment désignée par le terme « codage », qui n'a ici aucun rapport avec la cryptographie, mais qui se réfère au terme « code source » pour désigner le texte, en langage de programmation, constituant le programme. L'algorithme devra être plus ou moins détaillé selon le niveau d'abstraction du langage utilisé ; autrement dit, une recette de cuisine doit être plus ou moins détaillée en fonction de l'expérience du cuisinier.
Exemples d'algorithme
Il existe un certain nombres d'algorithmes classiques, utilisés pour résoudre des problèmes ou plus simplement pour illustrer des méthodes de programmation. On se réfèrera aux articles suivants pour de plus amples détails :
- Tours de Hanoï, problème célèbre illustrant la programmation récursive.
- Problème du tri, ou comment trier un ensemble de nombres le plus rapidement possible.
- Problème des huit dames, placer huit dames sur un échiquier sans qu'elles puissent se prendre entre elles.
La principale notion mathématique dans le calcul du coût d'un algorithme précis sont les notions de négligeabilité (notée o(f(n)), « petit o ») et de domination (notée O(f(n)), « grand o »), où f est une fonction mathématique de n, variable désignant la quantité d'informations (en bits, en nombre d'enregistrements…) manipulée dans l'algorithme. Les fonctions mathématiques relèvent de l'analyse. En algorithmique on trouve souvent des complexités du type :
- indépendant de la taille de la donnée
- , complexité logarithmique
- , complexité linéaire
- , complexité quasi-linéaire
- , complexité quadratique
- , complexité cubique
- , complexité polynômiale
- , complexité quasi-polynômiale
- , complexité exponentielle
- , complexité factorielle
Sans entrer dans les détails mathématiques, on peut dire que lorsque l'on calcule l'efficacité d'un algorithme (sa complexité algorithmique), on cherche davantage à connaître l'évolution du nombre d'instructions de base en fonction de la quantité de données à traiter (par exemple, dans un
algorithme de tri, le nombre de lignes à trier), que le coût exact en secondes et en quantité de mémoire. Baser le calcul de la complexité d'un algorithme sur le temps qu'un ordinateur particulier prend pour effectuer le-dit algorithme ne permet pas de prendre en compte la structure interne de l'algorithme ni la particularité de l'ordinateur : selon sa charge de travail, la vitesse de son processeur, la vitesse d'accès aux données ou même l'exécution de l'algorithme (qui peut faire intervenir le hasard) le temps d'exécution ne sera pas le même.
Quelques indications sur l'efficacité des algorithmes
Souvent, l'efficacité d'un algorithme n'est connue que de manière asymptotique, c'est-à-dire pour de grandes valeurs du paramètre n.
Lorsque ce paramètre est suffisamment petit, un algorithme de complexité supérieure peut en pratique être plus efficace.
Ainsi, pour trier un tableau de 30 lignes (c'est un paramètre de petite taille), il est inutile d'utiliser un algorithme évolué comme
Quicksort (l'un des algorithmes de tri les plus efficaces en moyenne) : l'algorithme de tri le plus trivial sera suffisamment efficace.
À noter aussi : entre deux algorithmes dont la complexité est identique, on cherchera à utiliser celui dont l'occupation mémoire est la plus faible. L'analyse de la complexité algorithmique peut également servir à évaluer l'occupation mémoire d'un algorithme. Enfin, le choix d'un algorithme plutôt qu'un autre doit se faire en fonction des données que l'on s'attend à lui fournir en entrée. Ainsi, le Quicksort (ou tri rapide), lorsque l'on choisit le premier élément comme pivot, se comporte de façon désastreuse si on l'applique à une liste de valeur ... déjà triée ! Il n'est donc pas judicieux de l'utiliser si on prévoit que le programme recevra en entrée des listes à peu près triées.
Un autre paramètre à prendre en compte est la localité de l'algorithme. Par exemple pour un système à mémoire virtuelle qui dispose de peu de mémoire (par rapport au nombre de données à traiter), le Tri rapide sera normalement plus efficace que le Tri par tas car le premier ne passe qu'une seule fois sur chaque élément de la mémoire tandis que le second accède à la mémoire de manière discontinue (ce qui augmente le risque de "swapping").
Les heuristiques
Pour certains problèmes, les algorithmes ont une complexité beaucoup trop grande pour obtenir un résultat en temps raisonnable, même si l'on pouvait utiliser une puissance de calcul phénoménale. On est donc amené à rechercher une solution la plus proche possible d'une solution optimale en procédant par essais successifs. Puisque toutes les combinaisons ne peuvent être essayées, certains choix stratégiques doivent être faits. Ces choix, généralement très dépendants du problème traité, constituent ce qu'on appelle une heuristique. Le but d'une heuristique est donc de ne pas essayer toutes les combinaisons possibles avant de trouver celle qui répond au problème, afin de trouver une solution approchée convenable (qui peut être exacte dans certains cas) dans un temps raisonnable.
C'est ainsi que les programmes de jeu d'échecs, de jeu de go (pour ne citer que ceux-là) font appel de manière très fréquente à des heuristiques qui modélisent l'expérience d'un joueur. Certains logiciels antivirus se basent également sur des heuristiques pour reconnaître des virus non répertoriés dans leur base, en s'appuyant sur des ressemblances avec des virus connus.
Applications
- Cryptologie et Compression de données
- Structure de données, Algorithmes de tri et Recherche dichotomique
- Allocation de mémoire et ramasse-miettes
- Informatique musicale
- Algorithme génétique en informatique décisionnelle
Voir aussi
- Al-Khuwarizmi
- Algorithme récursif
- Algorithme réparti
- Langage K
- Métaheuristique
- Structure de contrôle
Liens externes
- [http://www-ipst.u-strasbg.fr/pat/program/algo.htm Introduction à l'algorithmique, avec des exemples en langage C]
- [http://www.pise.info/algo/codage.htm Initiation à l'algorithmique]
- [http://www.myalgorithm.com Algorithmes de base dans plusieurs langages de programmation]
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.
Contrôle de versionLa gestion de version (en anglais revision control) est une activité qui consiste à maintenir l'ensemble des versions d'un logiciel. Essentiellement utilisée dans le domaine de la création de logiciels, elle est surtout concernée par le code source ; mais elle peut être utilisée pour tout type de document informatique.
Cette activité étant fastidieuse et relativement complexe, un appui logiciel est presque indispensable. À cet effet, il existe différents logiciels de gestion de version qui, bien qu'ayant des concepts essentiels communs, apportent chacun son propre vocabulaire et ses propres usages. À titre d'exemple, on trouve un mécanisme de gestion de version dans Wikipedia : pour chaque article, l'historique est disponible en cliquant sur le lien [http://fr.wikipedia.org/w/wiki.phtml?title=Contr%F4le_de_version&action=history Historique].
Les versions
Les logiciels évoluant, chaque étape d'avancement est appelée version. Les différentes versions sont nécessairement liées à travers des modifications.
Une modification peut correspondre à des ajouts, modifications, suppressions ou une combinaison des trois sur une version donnée. Schématiquement, on passera de la version N à la version N + 1 en appliquant une modification M. Un logiciel de gestion de versions nous aidera alors à soustraire la modification M à la version N + 1 pour retrouver la version N.
Il est à noter que les concepteurs du logiciel de gestion de versions CVS ont choisi de parler de « révisions » (revisions) afin de ne pas confondre la version du logiciel avec les « révisions » de ses fichiers sources.
Pour des raisons pratiques, on associe généralement un « numéro » à une version (voir Version d'un logiciel).
Modifications et ensemble de modifications
Une modification constitue donc l'évolution entre deux versions. On peut donc aussi bien parler de la différence entre deux versions que de modification ayant amené à une nouvelle version.
On utilise généralement la gestion de version à un ensemble de fichiers qui constitue un projet. De ce fait, il est courant de parler de modification pour un seul fichier et d'ensemble de modifications (change set) lorsqu'il s'agit du projet (et donc de plusieurs fichiers). En effet, les deux n'évoluent pas au même rythme.
Pour illustrer, prenons l'exemple d'un logiciel nommé « Toto ». Il est constitué des fichiers A, B et C. À la version 1.0 de « Toto » correspondent les versions 1.0 de chacun des fichiers. Admettons que l'ajout d'une fonctionnalité à « Toto » impose la modification de A et de C. Présentons la situation à l'aide d'un tableau
Du point de vue du projet, les modifications apportées à A et à C font partie du même ensemble.
Branches
Des modifications divergentes peuvent intervenir sur un ensemble de fichiers. On parle alors de branches.
Parfois il s'agit aussi de faire converger des branches. On parle alors de fusion de branches.
Conflit de modifications
Dans le cas d'une gestion de version en équipe, chacun travaille de façon indépendante, donc sur des branches de versions différentes. Les fusions régulières sont nécessaires à un avancement global du logiciel.
Il n'est pas rare que, suite à une mauvaise communication au sein de l'équipe, certaines modifications soient contradictoires (par exemple lorsque deux personnes ont apporté des modifications différentes à la même partie d'un fichier). On parle alors de conflit (de modifications) puisque le logiciel de gestion de version n'est pas en mesure de savoir laquelle des deux modifications appliquer.
Systèmes centralisés et décentralisés
CVS et Subversion sont des logiciels centralisés, ce qui veut dire qu'il n'existe qu'un seul dépôt des fichiers, dépôt qui fait référence. Cela peut simplifier le modèle mais cela est contraignant pour certains usages (travail sans connexion au réseau ou tout simplement travail sur des branches expérimentales ou bien contestées).
Il existe donc également des logiciels décentralisés comme Mercurial ou darcs. Avec ceux-ci, il existe plusieurs dépôts dont aucun n'a de statut privilégié.
Fonctionalités notoires des logiciels de gestion de version
Étiquetage ou marquage
Cela consiste à associer un nom à une version donnée. Pour certains outils de gestion de version (comme CVS) qui gèrent les versions à une faible granularité (beaucoup de modification non significatives), c'est un moyen de retrouver facilement une version significative.
Comparaison
Il est possible de comparer plusieurs versions pour en extraire les modifications.
Verrouillage et notifications
Pour le travail en équipe, certains logiciels de gestion de version apportent des outils pour communiquer.
Par exemple, le verrouillage permet d'interdire la modification d'un fichier, tandis que la notification émet un avertissement à tous les autres membres lorsqu'un fichier est modifié.
Exemples de logiciels de gestion de version
Les logiciels de contrôle de version sont nombreux. Sous UNIX il y a eu SCCS qui a suscité un logiciel libre alternatif : RCS (Revision Control System) qui est devenu un standard de fait. Comme RCS ne gérait que des fichiers individuels, nombre de ses utilisateurs ont créé des surcouches gérant les arborescences de fichiers. Certaines de ces surcouches furent distribuées librement. Il en fut ainsi de PRCS et de CVS. CVS est devenu extrêmement répandu dans le monde du logiciel libre sur Internet, mais aussi dans les entreprises. CVS est simple à mettre en œuvre et offre les fonctionnalités fondamentales qu'attendent ses utilisateurs.
Mais l'histoire des logiciels de contrôle de version ne s'arrête pas en 2002 et de nouveaux logiciels libres concurrencent CVS, comme par exemple Subversion, darcs et GNU Arch.
Dans le monde propriétaire, Visual Source Safe (de Microsoft) est largement utilisé, notamment du fait de son intégration avec l'outil de développement Visual Studio, malgré de nombreuses lacunes et des mises à jour peu fréquentes.
Voir aussi
- Logiciel de gestion de versions
- Gestion de configuration
- Version d'un logiciel
Catégorie:Programmation informatique
Catégorie:Gestion de projet
Catégorie:Gestionnaires de versions
ja:バージョン管理システム
Programmation systèmeLa 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
RefactoringCaté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 fonctionnelle
Un langage fonctionnel est un langage de programmation dont la syntaxe et les caractéristiques encouragent la programmation fonctionnelle. Le langage fonctionnel le plus ancien est le Lisp, créé en 1958 par Mac Carthy. Lisp a donné naissance à des variantes plus récentes telles que le Scheme (1975) et le Common Lisp (1984). D'autres langages fonctionnels récents incluent les langages ML (1973), Haskell (1987), Erlang, Clean et Oz. Dans la catégorie des langages non ML, il ne faut pas oublier XSLT.
Machine d'états et effets de bord
Programmation Impérative
En programmation impérative, on travaille sur le modèle de la machine de Turing, avec une mémoire centrale et des instructions qui modifient son état grâce à des assignations successives. On peut représenter un programme par une machine d'états qui représente les états successifs de la mémoire. Cela nécessite pour le programmeur de connaître à tout instant un modèle exact de l'état de la mémoire que le programme modifie.
Afin de réduire la difficulté que représente cette tâche, de nombreuses techniques destinées à réduire le nombre de variables à gérer sont utilisées. On peut citer parmi ces techniques les variables dites automatiques, dont la portée (portée lexicale) se limite à la procédure dans laquelle elles ont été définies et qui sont désallouées par le compilateur à la sortie de la procédure, l'encapsulation des données, à l'origine de la programmation structurée, et la programmation orientée objet. Cependant, en programmation impérative, il est fréquent qu'une variable ou une zone de mémoire ait une portée ou une durée de vie supérieure à celle de la procédure dans laquelle elle a été créée, de façon à pouvoir être lue ou modifiée par d'autres procédures ou unités d'exécution. Il arrive cependant que des procédures doivent mettre à jour certaines variables ou zones de mémoire dans un but qui n'est pas directement lié à leur fonction, mais uniquement afin que les données partagées restent dans un état prévu par le programmeur. On regroupe ces modifications « à distance » sous le terme générique d'effets de bord. Les effets de bord, en programmation impérative, qui sont plus la règle que l'exception, compliquent grandement la compréhension des programmes et sont la source de nombreuses difficultés et de bogues : en effet, si on oublie de mettre à jour certaines données partagées, si l'ordre chronologique des assignations par les différentes parties du logiciel est incorrect, ou si une zone de mémoire a été désallouée au mauvais moment, le programme se retrouve dans un état imprévu.
Programmation Fonctionnelle
La programmation fonctionnelle s'affranchit de façon radicale des effets de bord en interdisant toute opération d'assignation. Le paradigme fonctionnel n'utilise pas de machine d'états pour décrire un programme, mais un emboîtement de fonctions que l'on peut voir comme des « boîtes noires » que l'on peut imbriquer les unes dans les autres. Chaque boîte possédant plusieurs paramètres en entrée mais une seule sortie, elle ne peut sortir qu'une seule valeur possible pour chaque tuple de valeurs présentées en entrée. Ainsi, les fonctions n'introduisent pas d'effets de bord. Un programme est donc une application, au sens mathématique, qui ne donne qu'un seul résultat pour chaque ensemble de valeurs en entrée. Cette façon de penser, qui est très différente de la pensée habituelle en programmation impérative est l'une des causes principales de la difficulté qu'ont les programmeurs formés aux langages impératifs pour aborder la programmation fonctionnelle. Cependant, il faut noter qu'elle ne pose généralement pas de difficultés particulières aux débutants qui n'ont jamais été exposés à des langages impératifs.
Un avantage important des fonctions sans effet de bord est la facilité que l'on a à les tester unitairement. Par ailleurs, l'usage généralisé d'une gestion de mémoire automatique par l'intermédiaire d'un ramasse-miettes (garbage collector en anglais) simplifie la tâche du programmeur.
En pratique, pour des raisons d'efficacité, et du fait que certains algorithmes s'expriment aisément avec une machine d'états, certains langages fonctionnels autorisent la programmation impérative en permettant de spécifier que certaines variables sont assignables (ou mutables selon la dénomination habituelle), et donc la possibilité d'introduire localement des effets de bord. Ces langages sont regroupés sont le nom de langages fonctionnels impurs.
Les langages dit purement fonctionnels n'autorisent pas la programmation impérative. De fait, ils sont dénués d'effets de bord et protégés contre les problèmes que pose l'exécution concurrente.
L'implémentation des langages fonctionnels fait un usage sophistiqué de la pile, car afin de s'affranchir de la nécessité de stocker des données temporaires dans des tableaux, ils font largement appel à la récursivité, - le fait d'inclure l'appel d'une fonction dans sa propre définition -. La récursivité peut être rendue plus efficace à l'aide d'une technique dénommée récursion terminale (tail-recursion en anglais), qui consiste à accumuler les résultats intermédiaires dans une case mémoire de la pile et à la passer en paramètre dans l'appel récursif. Ceci permet d'éviter d'empiler les appels récursifs dans la pile en les remplaçant par une simple succession de sauts. Le code généré par le compilateur est alors similaire à celui généré par une boucle en impératif. Certains langages comme Scheme optimisent automatiquement les appels récursifs de cette manière.
Transparence référentielle
Les langages fonctionnels ont comme autre propriété la transparence référentielle. Ce terme recouvre le principe simple selon lequel le résultat du programme ne change pas si on remplace une expression par une expression de valeur égale. Ce principe est violé dans le cas de procédures à effets de bord puisqu'une telle procédure ne dépendant pas uniquement de ses arguments d'entrée, ne se comporte pas forcément de façon identique à deux instants donnés du programme. Pourtant, la transparence référentielle est, comme nous allons le voir, une propriété extrêmement utile.
Prenons un exemple. En C, si n désigne une variable globale contenant un entier à incrémenter (donc une case mémoire visible par tout le programme), et inc(k) une fonction qui augmente la valeur de n de la quantité k :
int n = 2;
int inc(int k) / - incrémentation par effet de bord - /
f(inc(1) + inc(1));
Dans cet exemple, la fonction inc(i) ne retourne pas la même valeur lors des deux appels : l'un des arguments de la fonction f vaudra 2 + 1 = 3 et l'autre 3 + 1 = 4. Il s'avère donc impossible de remplacer inc(i) + inc(i) par 2 - inc(i) car la valeur de inc(i) diffère à chaque appel. Ce comportement est fondamentalement différent de celui d'une fonction mathématique. À l'échelle d'un programme conséquent, cela signifie que le remplacement d'une fonction par une autre peut nécessiter des modifications à d'autres endroits du programme, et qu'il peut s'avérer nécessaire de retester l'intégralité du système, car on n'est pas assuré qu'un tel remplacement n'a pas modifié son comportement global. Au fur et à mesure que la complexité du système augmente, le coût d'un changement s'accroit aussi.
A l'inverse, la propriété de transparence référentielle permet d'assurer que le remplacement d'une fonction par une autre équivalente ne risque pas de modifier le comportement global du programme. Autrement dit, elles sont mathématiquement égales. Cette propriété facilite la maintenance logicielle. Elle permet aussi d'appliquer de façon automatique des preuves de fonctionnement. Elle a enfin pour autre avantage de sensiblement réduire l'importance de l'ordre d'exécution, celui-ci étant assuré par l'ordre d'appel des fonctions ; un corollaire est que la parallélisation d'un programme fonctionnel peut être réalisée de façon automatique.
Une plus grande expressivité
Les langages fonctionnels emploient des types et des structures de données de haut niveau comme les listes extensibles. Il est ainsi généralement possible de réaliser facilement des opérations comme la concaténation de liste, ou l'application d'une fonction à une liste, - le parcours de la liste se faisant de façon récursive -, en une seule ligne de code.
Un mécanisme puissant des langages fonctionnels est l'usage des fonctions d'ordre supérieur. Une fonction est dite d'ordre supérieur lorsqu'elle peut prendre des fonctions comme argument et/ou retourner une fonction comme résultat. On dit aussi que les fonctions sont des objets de première classe, ce qui signifie qu'elles sont manipulables aussi simplement que les types de base. Elles correspondent, en mathématiques, aux fonctionnelles. Les opérations de dérivation et d'intégration en sont deux exemples simples. Les fonctions d'ordre supérieur ont été étudiées par Alonzo Church et Stephen Kleene dans les années 1930, à partir du formalisme du lambda-calcul, un formalisme qui a grandement influencé le design de nombreux langages fonctionnels, en particulier Haskell.
Popularité du paradigme fonctionnel
Nombreux sont les programmeurs expérimentés qui pensent que la programmation fonctionnelle offre des avantages sans équivalent en impératif. Malgré cela, elle reste peu utilisée en dehors du milieu universitaire et des hobbyistes. Dans le milieu industriel, les langages Erlang (développé par Ericsson pour des besoins de programmation concurrentielle et des impératifs de robustesse), Common Lisp et Scheme sont utilisés. Mais le développement des langages fonctionnels est limité par le déficit en outils et en bibliothèques de qualité commerciale et surtout par le manque de programmeurs formés.
Enfin, les langages fonctionnels souffrent encore d'une réputation de lenteur aujourd'hui complètement injustifiée : certains compilateurs Scheme, comme les compilateurs Stalin ou Bigloo, les compilateurs pour Common Lisp, les langages dans la lignée de ML, tels que Objective Caml, ou encore Haskell présentent des performances moyennes comparables ou supérieures à celles des compilateurs C++.
Catégorie:Programmation informatique
ja:関数型言語
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 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]
- [http://www.dotnetguru.org/articles/dossiers/aop/quid/AOP15.htm AOP, Intérêts et Usages]
Catégorie:Programmation informatique
ja:アスペクト指向プログラミング
Programmation concurrenteLa programmation concurrente est un style de programmation tenant compte, dans un programme, de l'existence de plusieurs flots de contrôle. Ces flots de contrôle multiples peuvent être appelés threads, processus ou tâches. Ils sont matérialisés en machine par une pile d'exécution et un ensemble de données privées. Les threads disposent d'une zone de mémoire partagée alors que les processus sont strictement isolés.
La concurrence est indispensable lorsqu'on souhaite écrire des programmes interagissant avec le monde réel (qui est concurrent) ou tirant parti de multiples unités centrales (couplées, comme dans un système multiprocesseurs, ou distribués, éventuellement en grille ou en grappe).
On distingue trois types de concurrence :
- disjointe : les entités concurrentes ne communiquent et n'interragissent pas, c'est le degré zéro,
- compétitive : un ensemble d'entités concurrentes en compétition pour l'accès à certaines ressources partagées (par exemple le temps CPU, des données partagées),
- coopérative : un ensemble d'entités concurrentes qui coopèrent pour atteindre un objectif commun.
Classification
La programmation concurrente est plus complexe et difficile que la programmation impérative, fonctionnelle ou encore déclarative. En fait, à chacun de ces modèles de programmation on peut associer une version concurrente, par extension de la sémantique du langage de programmation associé. Par exemple, Prolog a été étendu en Concurrent Prolog, Haskell avec Concurrent Haskell, Java et Ada sont des langages à objets avec des primitives pour la concurrence, etc.
Les techniques spécifiques pour le traitement de la concurrence peuvent être étudiées en allant de la moins expressive (mais la plus facile à utiliser) à la plus expressive. On peut utiliser les niveaux suivants :
# concurrence déclarative (langage fonctionnel étendu avec des threads)
# concurrence en programmation logique
# concurrence déclarative avec des ports et envoi de messages
# concurrence impérative avec ports et envoi de messages
# concurrence impérative avec mémoire partagée
Problèmes
Le phénomène central introduit par la concurrence est le suivant : dans un programme non concurrent, ou séquentiel, l'ordre d'exécution des instructions élémentaires du programme est un ordre total qui reste le même d'une exécution à l'autre pour les mêmes paramètres en entrée. Dans un programme concurrent, l'exécution forme un ordre partiel. Comme la politique d'ordonnancement est généralement inconnue (elle est déterminée par le noyau du système d'exploitation par exemple) ou incontrôlée, on parle de l'indéterminisme de l'ordre d'exécution.
Les problèmes induits par la concurrence se manifestent dans les cas de la concurrence compétitive et coopérative. À cause de l'indéterminisme de l'exécution, l'accès à des données partagées par les entités concurrentes peut conduire à des inconsistances au niveau des relations liant ces données. Pour cela, on utilise différentes techniques comme les verrous, les mutex, les moniteurs ou encore les sémaphores. À leur tour, l'utilisation de ces mécanismes peut conduire à deux types de problèmes :
- interblocages (ou deadlocks) entre processus concurrents qui attendent, par exemple, que l'autre relâche un verrou acquis pour pouvoir progresser,
- famines (ou livelocks) d'un processus essayant d'acquérir une ressource, mais jamais ordonnancé au moment où elle est disponible.
Solutions
De nombreux types de solutions sont possibles pour maîtriser la programmation concurrente.
Il existe des calculs formels pour la concurrence : CCS, gamma-calcul, Chemical Abstract Machine, pi-calcul ...
Les systèmes transactionnels, utilisés principalement pour des bases de données partagées, s'appuient sur la théorie de la sérialisabilité pour garantir un accès concurrent à des ressources partagées (concurrence de type 4 et 5).
Des langages de programmation modernes comme Erlang ou Oz permettent d'écrire des applications concurrentes, avec un soin très particulier apporté aux questions de gestion d'exception concurrente et distribuée.
Voir aussi
- Calcul distribué : la concurrence est souvent liée à la distribution. Un programme dont l'exécution est distribué sur plusieurs hôtes est intrinsèquement concurrent. Pourtant, ce n'est pas toujours le cas ; l'utilisation de protocoles de communication entre parties distribuées d'un programme, comme les RPC (appels de procédure à distance) ou RMI (invocation de méthode à distance), peut rendre non-concurrent un programme ditribué.
- Calcul parallèle
Catégorie:Programmation concurrente
Système de gestion d'exceptionsSujets 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.
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.
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 particuli |