P-titre.fm Page i Jeudi, 30. janvier 2014 10:46 10 Programmation fonctionnelle Cours + exos corrigés Éric Violard Maître de conférences HDR à l’université de Strasbourg P-titre.fm Page ii Jeudi, 30. janvier 2014 10:46 10 Au terme de ce projet, je souhaite dédier ce livre à ma fille Julie et à son bonheur de lire et d'apprendre. Je ne saurais assez dire merci à tous les membres de ma famille, aussi je pense particulièrement à ma compagne (Myriam), à son écoute et son aide précieuse, à mes parents (Lyne et Raymond) et leur implication dans mon parcours scolaire et universitaire, et à mon frère (Christophe) au sens artistique inné. Je tiens également à remercier M. Jean-Luc Blanc des éditions DUNOD pour m'avoir fait confiance, ainsi que mon interlocutrice privilégiée Mme Carole Trochu et tous ceux et celles qui ont contribué à leur manière à la réalisation effective de ce manuel. Enfin, je veux adresser un grand remerciement à tous mes étudiants pour la curiosité et l'enthousiasme qu'ils ont manifesté lors de mes enseignements et qui sont à l'origine de ma motivation pour écrire ce manuel. © Dunod, Paris, 2014 ISBN 978-2-10-070385-2 ViolardTDM.fm Page iii Jeudi, 30. janvier 2014 10:48 10 Table des matières Avant-propos 1 2 3 4 Introduction VI 1 1.1 Quelques termes courants en programmation 1.2 Modèles 1.3 Significations du mot « variable » 1.4 Éléments purs ou impurs 1.5 Familles de langages fonctionnels Points clefs 1 2 9 10 11 15 Programmer avec des fonctions 17 2.1 Notions élémentaires 2.2 Définir une fonction 2.3 Fonctions plus complexes 2.4 Quelques facilités dans les langages Points clefs Exercices Solutions 17 20 25 27 31 31 33 Récursivité 39 3.1 Introduction 3.2 Définition récursive 3.3 Inventer une définition récursive 3.4 Récursivité et preuve 3.5 Problème de la terminaison 3.6 Récursivité terminale 3.7 Autres formes de définitions récursives Points clefs Exercices Solutions 39 40 43 44 45 48 55 57 58 59 Types 63 4.1 Introduction 4.2 La notion de type dans les langages fonctionnels 4.3 Les langages faiblement/fortement typés 63 64 65 ViolardTDM.fm Page iv Jeudi, 30. janvier 2014 10:48 10 iv Programmation fonctionnelle 4.4 Les constructeurs de types « produit » et « flèche » 4.5 Fonctionnelles et fonctions d’ordre supérieur 4.6 Polymorphisme 4.7 Calcul ou inférence de type Points clefs Exercices Solutions 5 6 7 67 68 78 81 91 92 95 Types structurés 103 5.1 Introduction 5.2 Types abstraits algébriques 5.3 Quelques catégories de types structurés 5.4 Types structurés polymorphes 5.5 Filtrage par motif 5.6 Exemple des listes 5.7 Exemple des arbres Points clefs Exercices Solutions 103 104 111 113 115 120 128 134 135 139 Schémas de programmes 147 6.1 Introduction 6.2 Toujours le même schéma 6.3 Capturer un schéma de programmes 6.4 Schéma reduce 6.5 Cacher la récursivité 6.6 Autres schémas classiques Points clefs Exercices Solutions 147 148 150 150 152 153 158 159 162 Lambda-calcul 169 7.1 Introduction 7.2 Syntaxe du lambda-calcul 7.3 Signification des lambda-expressions 7.4 Notion de réduction 7.5 Règles de réduction 7.6 Réduction généralisée 7.7 Stratégie de réduction Points clefs Exercices Solutions 169 170 172 175 176 182 185 194 195 197 ViolardTDM.fm Page v Jeudi, 30. janvier 2014 10:48 10 Table des matières v Annexe A – Quelques éléments sur les langages de ce manuel 205 A.1 Interpréteur et compilateur A.2 Commentaires A.3 Types de base et littéraux A.4 Principaux opérateurs A.5 Règles de typage A.6 Notations prédéfinies pour les listes A.7 Schémas de programmes prédéfinis Ressources complémentaires Index 205 207 208 208 209 210 211 212 213 Disponibles en ligne sur www.dunod.com/contenus-complementaires/9782100703852 Annexe B – Traduction d’un langage fonctionnel en lambda-calcul B.1 Traduction des expressions de base B.2 Traduction des expressions fonctionnelles B.3 Traduction des définitions de fonction B.4 Traduction de la récursivité B.5 Traduction des types structurés Annexe C – Réalisation des entrées/sorties C.1 Notion de monade C.2 Fonctions monadiques d’entrée/sortie C.3 Exemples Avant--propos.fm Page VI Jeudi, 30. janvier 2014 10:54 10 Avant-propos La programmation fonctionnelle est un paradigme de programmation, c’est-à-dire un mode de programmation et aussi une démarche, indépendante de tout langage de programmation, permettant de construire un programme résolvant un problème donné. Ce mode de programmation est fondé sur une théorie mathématique éprouvée, celle des fonctions, du lambda-calcul et des systèmes de type. Les concepts présentés sont donc fondamentaux et peuvent resservir pour répondre à d’autres questions liées à la programmation (certification de programmes, sémantique des langages, compilation, parallélisme, etc.). La programmation fonctionnelle est de plus en plus présente et facilitée dans les langages. Même les langages traditionnels sont étendus pour permettre le mode fonctionnel comme en témoigne l’introduction des fonctions anonymes (ou lambda-expressions) dans C++ (depuis la norme C++11) et Java 8. Les langages fonctionnels purs s’imposent de plus en plus comme une alternative sérieuse aux langages traditionnels non purs. Les qualités de ce mode de programmation sont en effet indéniables. D’une part, ce mode offre un grand pouvoir expressif permettant au programmeur d’écrire des expressions concises, de se rapprocher des termes du problème, de définir et d’utiliser simplement des types abstraits comme les listes ou les arbres. D’autre part, ce mode autorise la réalisation d’outils qui facilitent l’activité de programmation : interpréteurs permettant l’exécution interactive, compilateurs qui gèrent automatiquement l’utilisation de la mémoire, contrôleurs de types qui trouvent et vérifient automatiquement le type des expressions. En comparaison du mode impératif, le prix à payer en termes de performances du code exécutable produit par le compilateur, s’il fut dans le passé un facteur limitant l’utilisation du mode fonctionnel, est aujourd’hui très faible pour la grande majorité des applications. En résumé, ce mode est transcendant, et pour paraphraser le slogan du langage fonctionnel Haskell : « Utiliser le mode fonctionnel vous fera le plus grand bien ! ». CONSEILS DE LECTURE Les concepts présentés dans ce manuel sont indépendants de tout langage et illustrés en utilisant les implémentations récentes de trois langages fonctionnels : Scheme (prononcer [’ski:m]), OCaml et Avant--propos.fm Page VII Jeudi, 30. janvier 2014 10:54 10 Avant-propos VII Haskell. Il n’est pas nécessaire d’apprendre complètement la syntaxe d’un ou de plusieurs de ces langages ! Un programmeur expérimenté ne fait jamais cela : il a acquis suffisamment de connaissances et de recul au sujet d’un mode de programmation pour pouvoir utiliser n’importe quel langage qui supporte ce mode, et trouver dans un manuel de référence (destiné à un programmeur expérimenté), la syntaxe correspondant au concept qu’il souhaite utiliser. À ce propos, ce livre n’est en aucun cas un manuel de référence, et pour connaître les subtilités ou raccourcis syntaxiques, le lecteur devra consulter le manuel de référence du langage au fur et à mesure des connaissances acquises. Au contraire, ce livre a l’objectif premier de présenter les concepts de façon incrémentale. La syntaxe correspondante est donnée incidemment à titre indicatif et dans un ordre et un découpage différent de celui d’un manuel de référence, car les objectifs sont bien différents (par exemple, certaines constructions syntaxiques ont été omises volontairement, et inversement certains concepts ne se traduisent pas directement par de la syntaxe). Au-delà de la syntaxe différente, chacun de ces langages possède des caractéristiques propres (notamment en termes d’exécution) et apporte un éclairage différent : – Scheme est « très souple » pour le programmeur au sens où il n’interdit pas certains raccourcis qui peuvent cependant être dangereux pour la correction des programmes ; – OCaml est un produit universitaire plus éducatif, mais beaucoup plus rigide ; – Haskell est totalement pur avec un pouvoir d’expression accru. Il est conseillé au lecteur de commencer par l’un des langages OCaml ou Haskell, pour tester et expérimenter les concepts, puis de revenir ensuite sur les raccourcis et la souplesse de Scheme, une fois le mode fonctionnel mieux assimilé. RESSOURCES NUMÉRIQUES Ce manuel est accompagné de compléments en ligne qui permettront au lecteur d’aller plus loin. Ces compléments sont accessibles depuis le site de Dunod sur la page web dédiée à ce livre ou sur http:// www.dunod.com/contenus-complementaires/9782100703852. Ces ressources numériques contiennent : – deux annexes (B et C) : l’annexe B montre comment un programme écrit dans un langage fonctionnel peut se traduire en une expression du lambda-calcul. L’annexe C montre comment les opérations Avant--propos.fm Page VIII Jeudi, 30. janvier 2014 10:54 10 VIII Programmation fonctionnelle d’entrée/sortie (saisie au clavier et affichage à l’écran) peuvent être définies proprement dans un langage fonctionnel pur. – des programmes écrits en OCaml, Haskell et Scheme : ces programmes sont des solutions aux exercices énoncés à la fin de chaque chapitre. Dans ce manuel, la solution de chacun des exercices est écrite en utilisant un seul des trois langages. Ce complément révèle l’écriture de la solution dans les deux autres langages. – une application web (http://tolambda.sourceforge.net/fr) : cette petite application permet à l’utilisateur d’expérimenter le lambdacalcul et en particulier de vérifier les solutions aux exercices du dernier chapitre. À l’aide de cette application, l’utilisateur peut réduire automatiquement une lambda-expression en utilisant la stratégie de son choix (AOR, NOR ou leurs variantes) et aussi effectuer la traduction présentée dans l’annexe B. Elle a été conçue et réalisée par l’auteur de ce manuel dans le seul but de renforcer la compréhension du lambda-calcul (le programme source a été écrit en OCaml). Elle est libre de droits (licence MIT) : la seule condition de réutilisation est de citer l’auteur. USAGES TYPOGRAPHIQUES Un mot est mis en gras s’il est important et que sa définition est présente dans la page où il apparaît. Dans le contexte d’expressions écrites en utilisant un formalisme (par exemple un langage de programmation, le lambda-calcul ou le langage des mathématiques), la couleur rouge est parfois utilisée pour mettre en évidence une partie d’une expression (par exemple, une certaine occurrence de la variable a dans cette expression ((λx.λa.a) a) du lambda-calcul). Elle est utilisée systématiquement lorsqu’il s’agit d’expressions qui sont affichées à l’écran par un interpréteur ou un code exécutable (par exemple l’invite Prelude> de l’interpréteur Haskell). Une expression de type est colorée en rouge pour signaler qu’elle est écrite en utilisant la syntaxe d’un certain langage de programmation (OCaml ou Haskell) : si une expression de type est en noir, cela signifie qu’elle n’est pas écrite dans un langage en particulier et qu’elle représente toutes les expressions qu’il est possible d’écrire dans les différents langages de programmation pour exprimer le même type (par exemple, List Float exprime un type qui s’écrit float list en OCaml et List Float en Haskell). Ces conventions seront rappelées juste avant leur utilisation. chap1.fm Page 1 Mardi, 4. février 2014 12:15 12 1 PLAN Introduction 1.1 Quelques termes courants en programmation 1.2 Modèles 1.3 Significations du mot « variable » 1.4 Éléments purs ou impurs 1.5 Familles de langages fonctionnels OBJECTIFS ➤ Savoir situer le mode fonctionnel par rapport aux autres modes de programmation. ➤ Découvrir sur quoi repose la programmation fonctionnelle. ➤ Connaître le modèle sous-jacent et les concepts de base. 1.1 QUELQUES TERMES COURANTS EN PROGRAMMATION Les termes cités dans cette section sont considérés comme des prérequis. L’activité de programmation consiste à écrire un programme résolvant un problème donné. Généralement, la solution d’un problème est un ensemble de résultats qui dépendent d’un ensemble de données. Un programme est un texte qui décrit de manière formelle un algorithme, c’est-à-dire une suite d’étapes ou d’opérations permettant d’obtenir la solution du problème. Ces opérations sont réalisables (on dit plutôt exécutables) par le processeur d’un ordinateur. Ce texte peut être saisi, modifié et enregistré dans un fichier à l’aide d’un éditeur de texte. Un programme est écrit en respectant la syntaxe rigoureuse d’un langage de programmation. Il a une signification précise ou sémantique, qui est définie par le langage. Il n’est pas directement exécutable par l’ordinateur, car celui-ci ne sait interpréter qu’un code binaire, c’est-à-dire une suite de 0 ou 1. Des outils logiciels (compilateur ou interpréteur) associés au langage permettent d’exécuter un programme : soit en réalisant sa traduction complète en un code exécutable par l’ordinateur (compilateur), soit, lorsque le langage le permet, en interprétant le texte au fur et à mesure de sa lecture (interpréteur). chap1.fm Page 2 Mardi, 4. février 2014 12:15 12 2 Chapitre 1 • Introduction Tous les termes informatiques utilisés dans ce manuel sont mis en italique. Pour la plupart, leur sens figure dans le contexte où ils apparaissent. Certains termes font référence à des notions qui ne sont pas plus développées dans ce manuel (car elles dépassent les objectifs fixés), mais sont intéressantes à connaître dans le cadre d’études en informatique (c’est le cas de certains termes utilisés dans la section suivante qui a pour but de permettre au lecteur de différencier ce qui est propre au mode fonctionnel de ce qui appartient aux autres modes). 1.2 MODÈLES La programmation fonctionnelle est un mode de programmation, c’est-à-dire une façon de construire un programme résolvant un problème donné. Les principaux modes de programmation sont : ➤ le mode logique, ➤ le mode fonctionnel, ➤ le mode impératif, ➤ et le mode orienté objet. Le mode orienté objet peut être placé à part dans la mesure où il adresse une problématique différente (et complémentaire). Ce mode a en effet l’objectif premier de faciliter le développement et la maintenance de grands logiciels complexes en permettant de gérer séparément des parties conceptuellement disjointes. Les autres modes ont eux l’objectif premier de définir et organiser les calculs à réaliser par l’ordinateur ou la plateforme d’exécution. Une problématique différente Une distinction similaire apparaît parfois lorsqu’on parle de programmation « in the large » ou « in the small » pour opposer la réalisation et la maintenance d’un programme par une équipe dont chaque membre ne connaît qu’une partie, et l’écriture d’un programme par un seul programmeur qui connaît la totalité du programme. Lorsqu’on veut apprendre et utiliser un mode de programmation, il est important de bien séparer le mode de programmation, du langage de programmation qui n’est qu’une syntaxe pour faciliter l’utilisation d’un ou plusieurs modes de programmation. (Ce manuel a l’ambition d’apprendre au lecteur à utiliser le mode fonctionnel avec tout langage le permettant). chap1.fm Page 3 Mardi, 4. février 2014 12:15 12 1.2 • Modèles 3 Un langage de programmation est donc qualifié respectivement de « logique », « fonctionnel », « impératif » ou « orienté objet », dès lors qu’il encourage le programmeur à utiliser respectivement le mode logique, fonctionnel, impératif ou orienté objet. En théorie, il est possible d’utiliser un mode de programmation avec un langage de programmation qui n’encourage pas ce mode. Par exemple, utiliser le mode orienté objet avec un langage qui ne l’est pas. Cependant, dans ce cas, le programmeur doit gérer « à la main » de nombreuses contraintes d’accès aux données qui sont gérées automatiquement par le compilateur d’un langage orienté objet. La plupart des langages de programmation récents sont multi-paradigmes, c’est-à-dire qu’ils permettent d’utiliser plusieurs modes de programmation (et un grand nombre sont orientés objet puisque ce mode est complémentaire des autres). Par exemple, le langage OCaml est à la fois fonctionnel, impératif et orienté objet. Nous nous intéressons ici particulièrement aux modes que nous qualifions de « fondamentaux », c’est-à-dire ceux axés sur les calculs. Chacun de ces modes peut être défini ou caractérisé par un ensemble de notions mathématiques qui en constitue le modèle et sur lequel le mode est basé. Un modèle est une façon de formaliser la relation entre les données et les résultats d’un problème en utilisant le langage des mathématiques. Une fois cette relation formalisée, on obtient un énoncé rigoureux et non ambigu. Lorsque l’énoncé a une certaine forme, il est associé à un certain modus operandi ou procédé de calcul permettant d’obtenir étape par étape les résultats à partir des données. Un tel énoncé peut être directement codé dans un langage de programmation qui supporte le mode de programmation. Dans la suite, nous montrons pour chacun des modes de programmation quel est son modèle sous-jacent. Les notions sont illustrées par le problème du calcul du maximum de deux nombres. Nous en donnons un énoncé et une solution sous la forme d’un programme écrit dans un langage qui supporte le mode de programmation. Mode logique Le mode logique est naturel puisqu’il s’agit de définir la relation entre les données et les résultats et qu’en logique mathématique, un prédicat définit une telle relation. Son modèle est celui des formules logiques (plus précisément les formules du calcul des prédicats du premier ordre). Un énoncé est une formule logique qui définit un prédicat, et ce prédicat est la propriété qui relie les données aux résultats. chap1.fm Page 4 Mardi, 4. février 2014 12:15 12 4 Chapitre 1 • Introduction Exemple Un énoncé pour le maximum de deux nombres est par exemple : (A ≥ B ∧ M = A ⇒ max(A,B,M)) ∧ (B ≥ A ∧ M = B ⇒ max(A,B,M)) où max(A, B, M) est un prédicat à trois places signifiant que M est le plus grand parmi A et B. Une formule logique est ainsi construite en utilisant des connecteurs logiques : l’implication , le « et » logique ∧, le « ou » logique ∨et le « non » logique ¬ (la formule P ⇒ Q peut aussi s’écrire Q ∨ ¬ P). Lorsque l’énoncé est une conjonction de clauses de Horn, c’est-à-dire de formules de la forme P1 ∧ P2 ∧ ··· ∧ Pn Q, comme c’est le cas de l’exemple, alors il est associé à un procédé de calcul permettant d’obtenir le résultat à partir des données, et peut s’écrire en utilisant un langage logique comme Prolog. Exemple Le texte qui suit est un programme écrit en Prolog, et correspond à l’énoncé précédent: max(A,B,M) :– A >= B, M = A. max(A,B,M) :– B >= A, M = B. Ce programme comporte deux clauses de Horn terminées par un point. La virgule note le « et » logique et :– note l’implication (la notation Q :– P signifie P ⇒ Q, autrement dit « Q est vrai si P est vrai »). L’exécution d’un programme consiste à faire la démonstration qu’un prédicat est vrai. Un prédicat dont la vérité est à démontrer est appelé but, et le procédé de calcul (appelé chaînage arrière) consiste à tenter de démontrer tous les prédicats permettant de déduire le but. Exemple Un but est par exemple max(2,7,M) où M est une variable. Ce prédicat signifie que M est le plus grand parmi 2 et 7, il n’est vrai que pour certaines valeurs de M. L’exécution correspondant à ce but consiste à démontrer que ce prédicat est vrai et permet d’obtenir toutes ces valeurs de M. Le procédé de calcul consiste à rechercher une clause de Horn dont la conclusion (le prédicat à gauche de ":–") correspond (on dit plutôt dans ce contexte s’unifie) avec le but à démontrer. Ici, la première clause convient, car la partie gauche est max(A,B,M) qui s’unifie avec max(2,7,M) à condition de substituer la variable A par 2 et la variable B par 7. Cette unification, définie par ces substitutions, est propagée dans la partie droite de la clause : à la fin de cette première chap1.fm Page 5 Mardi, 4. février 2014 12:15 12 1.2 • Modèles 5 étape, on obtient les prédicats 2 >= 7 et M = 2 qui sont autant de buts à démontrer en procédant de la même façon. Lorsqu’un but est un prédicat élémentaire, par exemple 2 >= 7, un test a lieu pour décider s’il est vrai ou faux. S’il est faux, alors il faut retourner en arrière et chercher une autre clause qui pourrait convenir. S’il est vrai, alors il faut démontrer les buts restants. L’exécution s’arrête lorsqu’il ne reste plus de but à démontrer (le but initial est vrai) ou qu’il n’y a plus de clause à essayer (le but initial est faux). Voici ci-dessous les étapes de l’exécution du programme précédent avec un interpréteur du langage Prolog. Les affichages de l’interpréteur sont en rouge et les étapes du calcul sont numérotées de (1) à (5) : | ?– max(2,7,M). 2 >= 7, M = 2 (1) 2 >= 7 ? faux (2) 7 >= 2, M = 7 (3) 7 >= 2 ? vrai (4) M = 7 ? vrai (5) M = 7 yes À la fin de l’étape (2), il y a un retour arrière et la recherche d’une autre clause. À l’étape (5), le prédicat élémentaire M = 7 est vérifié en unifiant la variable M à la valeur 7. L’exécution se termine donc avec l’affichage de M = 7 et la réponse yes pour signifier que le but a été démontré. Mode fonctionnel Dans ce mode, la relation entre les données et les résultats est restreinte au cas particulier d’une fonction. Le modèle est donc celui bien connu des fonctions en mathématiques. Un énoncé est la définition d’une fonction qui à un n-uplet de données associe un n-uplet de résultats. Exemple Un énoncé pour le maximum de deux nombres est par exemple : max : E × E → E ⎧ a, si a ≥ b (a,b) max(a,b) = ⎨ ⎩ b, sin on où E est l’ensemble des nombres (ou n’importe quel ensemble totalement ordonné), E × E est le domaine de la fonction, et a et b sont des variables qui désignent n’importe quels éléments de E. chap1.fm Page 6 Mardi, 4. février 2014 12:15 12 6 Chapitre 1 • Introduction Un tel énoncé peut être associé à un procédé de calcul permettant d’obtenir les résultats à partir des données. Ce procédé termine seulement si la définition (éventuellement récursive) de la fonction est correcte au sens où l’image par la fonction est bien définie pour tous les éléments de son domaine. L’ensemble des fonctions que l’on peut énoncer de cette façon est l’ensemble des fonctions calculables au sens des théories de la calculabilité. On peut écrire un tel énoncé dans un langage fonctionnel par exemple OCaml. Exemple Le texte ci-dessous est un programme OCaml issu de l’énoncé précédent : let max(a,b) = if a >= b then a else b;; Ce programme est la définition d’une fonction max dont l’argument est un couple. Le mot-clé let introduit une définition et toutes les phrases OCaml se terminent par ;; . La fonction est définie au moyen d’une équation dont le membre gauche, c.-à-d. max(a,b), note l’image par la fonction du couple (a,b) où a et b sont des variables, et le membre droit, c.-à-d. if a >= b then a else b, est une expression appelée alternative, égale à a si la valeur de a est plus grande que celle de b, et égale à b sinon (ce programme exprime implicitement que les valeurs de a et b appartiennent à un ensemble ordonné). L’exécution d’un programme consiste essentiellement à calculer formellement le résultat de l’application d’une fonction. Le procédé de calcul, appelé évaluation, consiste à réécrire l’expression du résultat en utilisant des règles (dites de réécriture) qui substituent une partie de l’expression par une autre expression de même valeur, comme on le ferait en mathématiques pour calculer la valeur d’une expression arithmétique. Ces règles sont celles du lambda-calcul, théorie sur laquelle sont fondés tous les langages fonctionnels et qui fait l’objet du chapitre 7 de ce manuel. Le chapitre 7, entièrement dédié à cette théorie, décrit donc plus en détail le comportement opérationnel d’un programme. Exemple Pour exécuter le programme précédent, on peut par exemple appliquer la fonction max au couple (2,7), c.-à-d. demander à un interpréteur d’évaluer l’expression max(2,7) qui note l’image de ce couple par la fonction. L’évaluation de cette expression implique de substituer max par sa définition et de remplacer les variables a et b respectivement par 2 et 7. Elle consiste en l’application, dans un certain ordre, des règles de réécriture du lambda-calcul. À l’issue de ces substitutions, on obtient l’expression if 2 >= 7 then 2 else 7 chap1.fm Page 7 Mardi, 4. février 2014 12:15 12 1.2 • Modèles 7 qui est une alternative. L’évaluation de cette alternative, consiste à évaluer d’abord la condition 2 >= 7, c’est-à-dire remplacer cette expression par false selon une règle prédéfinie pour l’opérateur >=. On obtient l’expression if false then 2 else 7 qui est ensuite remplacée par 7 selon une autre règle prédéfinie pour l’alternative. En résumé, les étapes de l’exécution sont les suivantes : # max(2,7);; = if 2 >= 7 then 2 else 7 (1) = if false then 2 else 7 (2) = 7 (3) – : int = 7 Chaque étape peut contenir plusieurs applications de règles du lambda-calcul. L’interpréteur OCaml affiche le résultat et il indique que la valeur obtenue représente un entier (de type int). Mode impératif Ce mode de programmation est atypique et son modèle, plus complexe. On modélise d’abord la mémoire de l’ordinateur ou plus exactement un état mémoire : un état mémoire est une fonction qui à tout nom de variable (identificateur) associe une valeur. Le modèle est celui des fonctions entre états mémoire (également appelées fonctions de transition). Un énoncé est la définition d’une fonction de transition qui à un état mémoire « initial » associe un état mémoire « final ». Une donnée du problème est la valeur associée à un identificateur dans l’état initial, et un résultat est la valeur associée à un identificateur dans l’état mémoire final. La relation entre données et résultats est ainsi établie par une fonction de transition. Exemple Un énoncé pour le maximum de deux nombres est par exemple la fonction de transition suivante qui à un état mémoire s, associe un état mémoire s' : max : S → S s s' tel que s'(m) = ⎧ s (a), si s (a) ≥ s(b) ⎨ ⎩ s(b), sinon. où S est l’ensemble des états mémoire, et a et b et m sont des identificateurs de variable, s(a) (respectivement s(b)) note l’image de a (respectivement b) par la fonction s, c’est-à-dire la valeur associée à a (respectivement b) dans l’état mémoire s. Dans l’état initial s, les « variables » (au sens impératif du terme) a et b ont les valeurs des données et dans l’état final s', la variable m a la valeur du résultat. chap1.fm Page 8 Mardi, 4. février 2014 12:15 12 8 Chapitre 1 • Introduction Un tel énoncé peut s’écrire comme une composition (on dit séquence dans ce contexte) de fonctions de transition élémentaires appelées affectations. Une affectation modifie un état mémoire en associant une autre valeur à un identificateur. Lorsque l’énoncé prend cette forme, il est associé à un procédé de calcul, et peut s’écrire dans un langage impératif comme C. Exemple Voici un programme C pour le maximum de deux nombres : #include <stdio.h> main() { int a,b,m; scanf("%d %d",&a,&b); m = a; if(b > m) m = b; printf("%d\n",m); } Les éléments en rouge concernent les calculs, les autres éléments concernent la saisie des données au clavier et l’affichage du résultat à l’écran. Le point-virgule note la séquence de plusieurs fonctions de transition. Ce programme note une fonction de transition qui est la séquence d’une affectation notée m = a qui affecte à m, la valeur associée à a, et d’une conditionnelle notée if(b > m) m = b, qui change un état mémoire (en affectant à m, la valeur associée à b) si dans cet état mémoire la valeur de b est plus grande que celle de m, et laisse l’état mémoire inchangé sinon. L’exécution d’un programme consiste à calculer l’état mémoire final étant donné un état mémoire initial. Le procédé de calcul est d’appliquer successivement chaque fonction de transition élémentaire dans l’ordre de la séquence. Exemple Le déroulement de l’exécution du programme précédent (plus exactement du code exécutable obtenu en utilisant un compilateur) est résumé ci-dessous. On note s0, s1 et s2, les états mémoires successifs. L’état mémoire s0 est l’état initial dans lequel les variables a et b ont respectivement les valeurs 2 et 7 qui sont les données du problème, et s2 est l’état mémoire final dans lequel la variable m a la valeur du résultat : 2 7 (saisie des données) s0 : a 2 b 7 m 0 (1) chap1.fm Page 9 Mardi, 4. février 2014 12:15 12 1.3 • Significations du mot « variable » 9 s1 : a 2 b 7 m 2 = s0(a) s2 : a 2 b 7 m 7 = (s1(b) si 7>2, s1(m) sinon) (2) (3) 7 (affichage du résultat) À l’étape (1) (dans l’état mémoire initial s0), la valeur de m est quelconque et indéfinie (on suppose ici que cette valeur est 0). La première transition (l’affectation m = a) mène à l’état intermédiaire s1 dans lequel la variable m a la valeur qu’avait a dans l’état précédent (s0) c.-à-d. la valeur 2. À l’étape (3), un test est effectué pour déterminer la valeur de m dans l’état final et cette valeur est celle du résultat. 1.3 SIGNIFICATIONS DU MOT « VARIABLE » Nous venons de voir que tous les modes de programmation fondamentaux utilisent des variables, mais il est important de souligner que le sens du mot « variable » diffère : ➤ En programmation logique et fonctionnelle, une variable a le même sens qu’en mathématiques : une variable est une inconnue dans une équation. Par exemple, la variable x dans l’équation 2x – 6 = 0. Résoudre l’équation en donne une définition explicite : 2x – 6 = 0 x = 3. ➤ En programmation impérative, une variable est l’expression abstraite d’une adresse en mémoire. Une définition est appelée « affectation » et signifie le rangement d’une valeur à cette adresse. Par exemple, on peut écrire a = 1 ; a = 2 ou x = x + 1, ce qui n’a pas de sens (ou pas de solution) en mathématiques. Cette différence est fondamentale et a des conséquences très importantes lorsqu’il s’agit de concevoir ou transformer un programme. La notion de variable au sens impératif est en effet très dangereuse pour raisonner sur les programmes : elle interdit, par exemple, toute substitution d’une variable par sa définition. Exemple Considérons la substitution de la variable x par sa définition dans le programme impératif ci-dessous à gauche (écrit en utilisant la syntaxe du langage C) : t = x + 1; t = x + 1; x = t; x = x + 1; y = t; y = x + 1; Si l’on substitue dans ce programme, la variable t par sa définition c.-à-d. l’expression x + 1, on obtient le programme à droite, qui chap1.fm Page 10 Mardi, 4. février 2014 12:15 12 10 Chapitre 1 • Introduction n’est pas équivalent : par exemple, si dans l’état initial, la variable x a pour valeur 1, alors dans l’état final, la valeur de y sera 2 avec le programme à gauche, et 3 avec le programme à droite. 1.4 ÉLÉMENTS PURS OU IMPURS L’utilisation de variables au sens mathématique du terme facilite la transformation ou l’amélioration d’un programme. Le programmeur peut s’appuyer sur des principes mathématiques bien connus, par exemple la substitution d’une variable par sa définition (ce qui est la base même des mathématiques) ou une propriété plus générale de certains langages de programmation appelée propriété de transparence référentielle. Cette propriété s’énonce informellement comme suit : Propriété de transparence référentielle « On obtient un programme équivalent si l’on remplace une expression par une expression de même valeur. » Cette propriété est vraie en programmation logique et fonctionnelle, et fausse en programmation impérative (l’exemple précédent est un contreexemple). Définition : (Élément impur) Nous dirons qu’un élément d’un langage (une construction syntaxique autorisée par le langage et associée à une certaine sémantique) est impur s’il contredit d’une manière ou d’une autre la propriété de transparence référentielle. Par exemple, l’affectation qui modifie la valeur associée à une variable est un élément impur. Un élément impur fait référence à un état global ou externe (par exemple un état mémoire) et modifie cet état sans que cela soit explicite dans un programme (on parle d’effet de bord). De tels éléments nuisent à la lisibilité des programmes et leur absence augmente leur réutilisabilité. Certains langages fonctionnels ont des éléments impurs. C’est le cas des langages OCaml et Scheme par exemple. D’autres sont purs, c’est-à-dire totalement dépourvus d’éléments impurs, par exemple Haskell. En conséquence, « programmer en utilisant le mode de programmation fonctionnelle » ne signifie pas simplement utiliser un langage fonctionnel, car il est possible de programmer en OCaml en adoptant le mode impératif (en utilisant