Epreuve intégrée : La Programmation Fonctionnelle 2007 1 EPFC | Bashevkin Nathan & Polazzi Fabio Epreuve intégrée : La Programmation Fonctionnelle 2007 Introduction........................................................................................................................ 3 Le Cours ......................................................................................................................... 4 1. Définition simple ........................................................................................................ 5 2. Programmation impérative et fonctionnelle ............................................................. 5 La programmation impérative : .............................................................................................. 5 La programmation fonctionnelle :........................................................................................... 5 3. Attributs de la programmation fonctionnelle............................................................ 6 La transparence référentielle :................................................................................................. 6 Fonctions d’ordre supérieur : .................................................................................................. 7 4. Présentation de quelques langages connus .............................................................. 7 Haskell : ..................................................................................................................................... 7 Erlang :....................................................................................................................................... 8 Miranda : ................................................................................................................................... 8 Lisp : ........................................................................................................................................... 9 5. Haskell ...................................................................................................................... 10 Premiers pas ............................................................................................................................ 11 Les types simples ..................................................................................................................... 11 Les fonctions ............................................................................................................................ 12 Evaluation paresseuse ............................................................................................................. 14 Structures de données ............................................................................................................. 15 a. b. c. d. e. Liste : ..........................................................................................................................................15 Queue : .......................................................................................................................................22 Arbre binaire: .............................................................................................................................24 Graphe : ......................................................................................................................................25 Pile..............................................................................................................................................26 Quelques opérateurs importants ........................................................................................... 28 Le programme .............................................................................................................. 31 6. Programme : Algorithme de Dijkstra ...................................................................... 32 Fin ................................................................................................................................... 42 Conclusion........................................................................................................................ 43 Appendice ......................................................................................................................... 44 Bibliographie .................................................................................................................... 45 EPFC | Bashevkin Nathan & Polazzi Fabio 2 Epreuve intégrée : La Programmation Fonctionnelle 2007 Introduction Dans le cadre de notre travail de fin d’études, nous avons décidé de vous parler de la programmation fonctionnelle. Nous avons choisi ce sujet afin de compléter notre répertoire de types de langage informatiques. En effet, la programmation fonctionnelle est une des trois grandes familles de langages, avec les langages orientés objets (Java, C++) et les langages impératifs (Pascal, Perl, Assembleur), que nous avons étudié durant ces trois années. Notre objectif est de « vulgariser » la programmation fonctionnelle. Pour cela, notre travail se divise en deux parties : - Un cours, que vous êtes libres de réutiliser pour une nouvelle matière - Un programme, afin d’illustrer concrètement ce qu’est la programmation fonctionnelle Notre cours se divise lui-même en deux parties. Premièrement, nous présenterons la programmation fonctionnelle en général, ses caractéristiques, ainsi que les principaux langages existants. Deuxièmement, nous étudierons ensemble un langage particulier : Haskell. C’est le langage que nous avons utilisé pour notre programme. Il est probablement le langage le plus adapté pour la pédagogie, grâce à ses nombreuses documentations. Nous verrons les fonctions principales utilisés en informatique et leur implémentation en Haskell, et les structures de données tels que les piles, les listes, les arbres… Le tout sera illustré par des exemples. Concernant le programme, nous avons réalisé un « jeu » (qui n’est pas très drôle) qui utilise l’algorithme de Dijkstra. Ce jeu consiste en un parcours minimal d’une case d’un échiquier à une autre, par un cavalier. Le très renommé algorithme de Dijkstra est bien sûr utilisé pour calculer le coût minimal d’un trajet. Notre travail a été réparti comme suit : Fabio Programmation impérative et fonctionnelle, Présentation de quelques langages connus (Haskell, Erlang), Haskell (Listes, Graphe et Arbres), Algorithme de Dijkstra Nathan Attributs de la programmation fonctionnelle, Présentation de quelques langages connus (Miranda, Lisp), Haskell (Listes, Queues et Piles), Algorithme de Dijkstra Sans plus tarder, commençons par une définition simple. EPFC | Bashevkin Nathan & Polazzi Fabio 3 Epreuve intégrée : La Programmation Fonctionnelle 2007 Le Cours 4 EPFC | Bashevkin Nathan & Polazzi Fabio Epreuve intégrée : La Programmation Fonctionnelle 2007 1. Définition simple La programmation fonctionnelle est, comme son nom l’indique, une façon de programmer au moyen de fonctions mathématiques : il faut respecter des notions de logique mathématique. Un concept fondamental est l’abstraction. Il faut aborder un problème complexe en le divisant en éléments plus simples et en ignorant les détails. La programmation fonctionnelle rassemble une famille de langages très proches entre eux. Elle regroupe des langages interprétés, c’est-à-dire que les instructions sont lues au fur et à mesure de l’exécution. 2. Programmation impérative et fonctionnelle Nous allons comparer deux types différents de programmation. La programmation impérative : On travaille sur la mémoire centrale, grâce à une succession d’instructions qui modifient son état au moyen d’assignations. On peut représenter l’évolution du programme à chaque instant en regardant la mémoire centrale, comme nous l’avons fait en première année au cours de LAPR. Dans un souci d’économie de mémoire, plusieurs techniques sont disponibles, notamment la portée définie d’une variable. L’espace mémoire alloué à une variable locale n’est pas définitif. Il pourra être réalloué au fil de l’exécution. Cependant, il arrive que le programmeur veuille modifier une variable définie ailleurs dans son programme, pour qu’elle soit dans un état précis. On appelle ces modifications « effets de bord » (side effects). Ces effets de bord sont responsables de nombreux bugs, et compliquent la compréhension du programme. La programmation fonctionnelle : La programmation fonctionnelle s’affranchit des effets de bord (et des problèmes qui l’accompagnent !). L’avantage d’une absence d’effet de bord est la lisibilité ainsi que la facilité à tester les fonctions séparément. Le programme peut être vu comme un emboîtement de plusieurs fonctions indépendantes les unes par rapport aux autres. Ces fonctions, que l’on nomme grossièrement « boite noire », peuvent posséder plusieurs paramètres en entrée, mais qu’un seul paramètre en sortie. Le programme est donc une application, au sens mathématique, qui ne renvoie EPFC | Bashevkin Nathan & Polazzi Fabio 5 Epreuve intégrée : La Programmation Fonctionnelle 2007 qu’un seul résultat pour un ensemble de valeurs. Cette façon de programmer représente un obstacle pour les programmeurs habitués aux langages impératifs. L’implémentation de la programmation fonctionnelle fait un usage sophistiqué de la pile. En effet, afin d’éviter de stocker des variables dans un tableau, il faut faire appel à la récursivité. On parle aussi de récursivité terminale (tail-recursion), qui va mettre sur la pile des résultats intermédiaires et qui les passe en paramètre lors des appels récursifs. Cela remplace l’empilage d’appels récursifs par un système de sauts, le code généré par le compilateur équivalant alors à une boucle dans les langages impératifs. 3. Attributs de la programmation fonctionnelle Deux propriétés intéressantes de la programmation fonctionnelle sont la transparence référentielle et sa grande expressivité. La transparence référentielle : Cette propriété a comme principe de permettre de remplacer une expression par une autre de valeur égale. Ce principe est violé dans le cas des langages impératifs car une expression n’aura pas la même valeur à différents moments de l’exécution. Voyons un exemple avec le langage C. Soit une fonction inc(k) qui va incrémenter une valeur x de k. int x = 10 ; int inc(int k){ x = x + k ; return x ; } f(inc(1) + inc(1)) ; En C, la fonction inc(1) ne retournera pas deux fois le même résultat. En effet, lors de son premier appel, la fonction retourne 10 + 1 = 11, et au deuxième 11 + 1 = 12. Ce comportement n’est pas du tout mathématique. Remplacer une fonction par une autre entraînera des modifications plus ou moins importantes, car on n’est pas assuré qu’un changement ne modifiera pas le comportement global. Ces modifications peuvent être très fatigantes à l’échelle d’un programme plus conséquent, sans compter les bugs qu’elles causeront. Mais avec la transparence référentielle, ce n’est pas un problème, car elle permet de remplacer des fonctions sans crainte de surprise. Par conséquent, la maintenance logicielle est plus aisée, on peut facilement vérifier un programme en remplaçant les fonctions et l’ordre des fonctions n’est plus important. EPFC | Bashevkin Nathan & Polazzi Fabio 6 Epreuve intégrée : La Programmation Fonctionnelle 2007 f(inc(1) + inc(1)) ; == f(2*inc(1)) ; Fonctions d’ordre supérieur : Une fonction est d’ordre supérieur lorsqu’elle peut en prendre une autre comme paramètre, et retourner une fonction comme résultat. La fonction de dérivation en est un exemple. Les fonctions d’ordre supérieur permettent la technique du Currying, une technique dans laquelle une fonction est appliquée sur des paramètres, un à la fois, en retournant une nouvelle fonction d’ordre supérieur qui utilise les paramètres suivants. 4. Présentation de quelques langages connus Haskell : Suite à une conférence sur les langages de programmation fonctionnelle, et dans un soucis d’uniformisation, Haskell est défini pour la première fois en 1990 dans le premier Haskell Report. Nommé en hommage au mathématicien Haskell Brooks Curry, c’est un langage en constante évolution et qui est fondé sur le lambda calcul (une méthode de calcul) et la logique combinatoire (une façon de se passer de variables en mathématique). Haskell gère la récursivité, l’inférence des types (il recherche lui-même les types nécessaires aux variables), et l’évaluation paresseuse (il n’exécute pas de code avant qu’un résultat ne soit nécessaire). Il utilise des monades, ce qui permet de modéliser le monde extérieur au programme et d’éviter ainsi les effets de bord (par exemple : le main). Haskell est le langage fonctionnel le plus utilisé. Il en existe plusieurs versions, dont certaines didactiques comme HUGS (Haskell User's Gofer System). C'est le langage que nous avons décidé de voir en profondeur dans le cadre de ce travail. Exemple de codes : Fibonacci (version naïve) : fib 0 = 0 fib 1 = 1 fib n = fib (n - 2) + fib (n - 1) Fibonacci (deuxième version) : fibs = 0 : 1 : (zipWith (+) fibs (tail fibs)) Fibonacci (troisième version) : fib n = fibs !! n 7 EPFC | Bashevkin Nathan & Polazzi Fabio Epreuve intégrée : La Programmation Fonctionnelle 2007 Erlang : Erlang est un langage fonctionnel concurrent (c’est-à-dire qu’il gère plusieurs piles différentes) temps réel, et distribué. Il possède des fonctionnalités de tolérance aux pannes, et de mise à jour du code en temps réel pour des applications à haute disponibilité. Il est utilisé par des compagnies de télécommunication dans la fabrication de routeurs, et possède des interfaces, comme Haskell, avec d’autres langages de programmation comme Java pour la réalisation d’interfaces graphiques. Exemple de code : Fonction factorielle : -module(fact). -export([fac/1]). fac(0) -> 1; fac(N) when N > 0 -> N * fac(N-1). Miranda : Miranda est le premier langage fonctionnel destiné à des fins commerciales plutôt que académiques. Comme les autres langages fonctionnels, ses utilisateurs apprécient la brièveté des algorithmes, la fiabilité ainsi que l'investissement réduit du temps consacré à la programmation. Un seul compilateur a été écrit pour ce langage, et notons aussi que Haskell s'en est beaucoup inspiré. Miranda est un langage de programmation fonctionnel qui utilise l'évaluation paresseuse (voir chapitre sur Haskell). Un programme écrit en Miranda, communément appelé un script, est un ensemble d'équations qui définissent des fonctions mathématiques. Il est important de comprendre la notion d'ensemble, car en général l'ordre des équations n'est pas important, et il n'est pas nécessaire de déclarer des entités avant de les utiliser. La méthode d'écriture des algorithmes requiert rarement l'utilisation de parenthèses ou d'autres balises. On retrouve cette liberté d'interprétation dans le Haskell. Exemple de code : Une liste : jour_semaine = ["Lun","Mar","Mer","Jeu","Ven"] Concaténation de liste: jour = jour_semaine ++ ["Sam","Dim"] EPFC | Bashevkin Nathan & Polazzi Fabio 8 Epreuve intégrée : La Programmation Fonctionnelle 2007 Compréhension de liste: squares = [ n * n | n <- [1..] ] Lisp : Le Lisp est un des tous premiers langages de programmation de haut niveau, seul Fortran lui est antérieur. D'abord défini en 1958, il a quand même énormément évolué depuis. Aujourd'hui on reconnaît plusieurs dialectes basés sur le Lisp, dont Common Lisp et Scheme. Le Lisp a d'abord été créé afin de faire des annotations mathématiques sur des programmes informatiques, basé sur le principe du λ-calcul. Il est rapidement devenu le langage de prédilection pour les recherches sur l'intelligence artificielle. Il est également lié à l’apparition de nombreuses idées de la science des ordinateurs, tels que la structure d'un arbre, la programmation orientée objet etc… Le Lisp doit son nom à la contraction des mots "List Processor". Les listes liées sont une des structures de données les plus importants du langage, dont le code source est luimême construit à partir de listes. Exemple de code : Une liste : (list '1 '2 'foo) Le caractère ‘ sert à préciser au compilateur que l’argument suivant ne dois pas être évalué. Algorithme de la fonction factorielle : (defun factorielle (n) (if (<= n 1) 1 (* n (factorielle (- n 1))))) 9 EPFC | Bashevkin Nathan & Polazzi Fabio Epreuve intégrée : La Programmation Fonctionnelle 2007 5. Haskell Le langage Haskell est un langage fonctionnel pur, non strict (il n’est pas nécessaire d’évaluer tous les arguments d’une expression) et fortement typé même s'il permet le polymorphisme et possède un système de classes. Nous allons travailler avec l’interpréteur Hugs, mais surtout avec GHC qui lui est un vrai compilateur. Il est plus compliqué à utiliser, mais nous donne plus de liberté. Les principales commandes sont : :q ou :quit :e ou :edit :l nom_fichier ou :load (charge le fichier) :r ou :reload :t expression ou :type expression :? ou :help (liste d’ensemble des commandes) « tes » Il est intéressant de remarquer que lorsqu’on lance GHC, la ligne où nous pouvons commencer à taper commence par "Prelude>". En fait, Prelude est le module standard en Haskell. Concrètement il signifie que l'on peut utiliser un ensemble de fonctions analogues à celles des librairies du langage C. Un programme Haskell est constitué d'un ensemble de modules. Un module a une double utilité : il sert à contrôler les espaces de nommage, et à créer un type de données abstrait. Exemples d’expressions : Prelude> 13+7 20 Prelude> (2+3)*8 40 Prelude>1 == 0 False Prelude>1.0+5 6.0 Prelude>5+’a’ ERROR :.... Le compilateur signale qu’il y a une erreur de typage EPFC | Bashevkin Nathan & Polazzi Fabio 10 Epreuve intégrée : La Programmation Fonctionnelle 2007 Afin d'illustrer comment fonctionne Haskell, nous avons observé plusieurs concepts courant de la programmation en général, tel que les piles, les listes etc... Comme tout a un commencement, il est aussi nécessaire de voir les opérations les plus fondamentales comme les déclarations de variables, les portées et les durées de vie de celles-ci. Premiers pas Nous allons commencer par une déclaration toute simple, commentée. Exemple: Prelude> let a = 3 Prelude> a 3 -– déclaration d'une variable a -- que vaut a ? -- a vaut 3 Il est important de signaler que ces instructions ne sont pas des assignements mais des déclarations. On pourrait traduire "let" par "soit" en Français. On ne peut pas assigner une valeur à une "variable" précédemment déclarée à moins de la redéclarer et c'est pour cela que la programmation fonctionnelle a également été appelée statique, descriptive ou déclarative. Les commentaires sont précédés de "--", et l'interpréteur comprend alors qu'il ne faut pas tenir compte de ce qui suit sur la ligne. Ils peuvent aussi être entourés des balises "{" et "-}" afin de ne pas devoir ajouter les "--" devant chaque ligne. Contrairement aux langages de programmation impérative, on ne doit pas définir à l'avance le type de la variable que l'on utilise. Ainsi on peut tout aussi bien déclarer une chaîne de caractères qu'un réel en faisant "let a =". Les types simples a) Les types de base : 1) Le type booléen Bool. Il contient les constantes True et False et les fonctions : identité ("=="), ou ("||"), and ("&&"), not. 2) Le type caractère noté Char. Il est codé sur 8bits et contient tous les caractères ASCII. Par exemple "ord" est une fonction donnant la valeur ASCII d’un caractère. 3) Le type entier signé noté Int (un entier codé sur au moins 30bits) ou Integer (un entier au sens mathématique du terme, qui n’a pas de limite). Il contient les fonctions classiques (+,-,*,/), abs, mod,... EPFC | Bashevkin Nathan & Polazzi Fabio 11 Epreuve intégrée : La Programmation Fonctionnelle 2007 4) Le type réel noté Float en simple précision ou Double en double précision. Le nombre est représenté avec un point (ex : 6.0). Les fonctions classiques sont également appliquées sur les réels (surcharge). b) Les types fonctions : A partir de ces types de base on peut réaliser le type fonction Exemple : module Test where f :: Int->Int->Int f x y =x+y main :: Int main :: f 5 3 ------- signature de la fonction f définition de f : f(x,y)=x+y type de l’expression main définition de main : évaluer f(5,3) c) Structure de données prédéfinies : Nuplet : Couple : (4,5), ('a','b'), (5," fabio") Triplet : (3,"fifi",'s') … Suites : (les suites sont appelés "list" en Haskell) Suite vide : [] Suite d’entiers : [2,4,3,1,5] Suite de couples :[(3, "fifi"),(5, "coucou")] Suite de suites d’entiers : [[1,7,3], [], [4,5,8]] String : suite de caractères "abc" = ['a', 'b', 'c'] Les fonctions Le nom d’identification d’une fonction ne peut pas commencer par une majuscule. Voyons comme exemple une fonction qui additionnerait 3 entiers dont le code Haskell serait le suivant : som :: (Int,Int,Int)->Int som(a,b,c)=a+b+c -- Signature de la fonction -- Définition de la fonction Si on enregistre ces deux lignes dans un fichier avec l’extension .hs, il devient possible de charger ce fichier et d’utiliser cette fonction avec des paramètres réels. EPFC | Bashevkin Nathan & Polazzi Fabio 12 Epreuve intégrée : La Programmation Fonctionnelle Prelude> :l c:\\ex.hs .. .. Main> som(5,2,3) 10 2007 -- Evaluation de l’expression Remarquons que "\" est souvent utilisé en informatique pour insérer un caractère spécial. Par exemple pour afficher des guillemets dans une chaine de caractère, alors que le signe guillemet est justement utilisé pour délimiter cette chaine. Par conséquent, pour insérer le signe "\", il faut taper deux fois le caractère. On remarque que le module est passé de Prelude à Main. Lorsque l'on est dans le module Main, nous pouvons évaluer des expressions et types d'expressions Main> :t som -- demande du type de -- l’expression som ::(Int,Int,Int)->Int Les parenthèses sont très peu courantes dans la notation des fonctions, on peut donc l'écrire de cette façon : som :: Int -> Int -> Int->Int som a b c= a + b + c Et lors de l’exécution nous taperons donc som 5 2 3 Nous allons ensuite nous intéresser aux codes qui contiennent des conditions. Imaginons une fonction ex2 qui reçoit un couple de deux entiers x et y et retourne (1,y) si x=0, sinon le couple (x,x+y). Il y a plusieurs façons d'écrire cette fonction : 1. à l’aide de l’expression if... then... else (semblable au ?: de C++ et Java) : ex2 :: (Integer,Integer)->(Integer,Integer) ex2(x,y)= if x==0 then(1,y) else(x,x+y) 2. à l’aide de gardes : Une garde est une expression de type booléen qui a pour valeur Vrai si l'exécution du programme doit continuer dans la branche en question. Dans le langage Haskell, les gardes apparaissent entre chaque paire de "|" et "=" . ex2 :: (Integer,Integer)->(Integer,Integer) ex2(x,y) | x==0 = (1,y) | otherwise = (x,x+y) EPFC | Bashevkin Nathan & Polazzi Fabio 13 Epreuve intégrée : La Programmation Fonctionnelle 2007 3. à l’aide d’équations : ex2 :: (Integer,Integer)->(Integer,Integer) ex2(0,y)=(1,y) ex2(x,y)=(x,x+y) Evaluation paresseuse En programmation, on parle d’évaluation paresseuse lorsque l’on retarde l’exécution du code jusqu’au moment où celui-ci est réellement requis. En français, la notion de "paresseuse" a un côté péjoratif, le terme est en fait hérité de l'anglais ("lazy evaluation"). On pourrait aussi parler d'évaluation "retardée" ("delayed evaluation"). Les bénéfices de l'évaluation paresseuse sont multiples : a. Performances accrues puisque l'on évite les calculs superflus b. Disparition des erreurs lors d'exécution d'expressions composées c. Possibilité de construire des structures de données infinies Haskell est un langage qui utilise l'évaluation paresseuse par défaut, par opposition à l'évaluation directe (stricte), utilisée par les langages de programmation impérative (C++, Pascal, Java). L'évaluation paresseuse est surtout utilisée par les langages de programmation fonctionnelle. Une expression n'y est pas évaluée dès qu'on l'attache à une variable, mais lorsque l'on veut produire la valeur de cette expression. Supposons l'instruction suivante : x:=expression; On demande donc d'assigner la valeur de l'expression à la variable x. Dans un langage de programmation impérative, cela se fera immédiatement. En Haskell, cette instruction n'aura pas lieu tant que l'on n'appelle pas la variable x plus tard dans le code. L'évaluation paresseuse a aussi l'avantage de permettre de rendre calculable des listes infinies sans faire de boucle infinie. Un exemple concret de cet avantage est la suite des nombres de Fibonacci. Voici la suite de Fibonacci : 14 EPFC | Bashevkin Nathan & Polazzi Fabio Epreuve intégrée : La Programmation Fonctionnelle 2007 Avec l'évaluation directe, pour calculer F(n), il faut avoir prévu une boucle qui va au moins jusqu'à n. Avec l'évaluation paresseuse, on peut faire une liste infinie qui ne va pas boucler à l'infini, car on ne calculera uniquement que les n premiers nombres de la liste. La fonction s'écrit de cette manière : fibs = 0 : 1 : zipWith (+) fibs (tail fibs) Pour votre information, dans la syntaxe d'Haskell, ":" ajoute un élément à une liste, "tail" retourne la liste décapitée de sa tête, et "zipWith" utilise une fonction définie (dans ce cas, l'addition) pour combiner les éléments correspondants de 2 listes en une 3ème. Cette programmation n'est cependant pas exempte de potentiels problèmes : demander la longueur d'une liste infinie aura bien évidemment toujours le même résultat ! On trouve d'autres exemples de l'évaluation paresseuse dans la technique copy on write où le système ne copie pas les données tant qu'il n'y a pas eu de modification - il espère que la copie ne sera pas nécessaire - ou dans les interfaces graphiques - il n'est pas nécessaire de tout calculer, uniquement ce qui sera affiché. Structures de données a. Liste : Une liste est une séquence linéaire d’un nombre quelconque d’éléments du même type et qui peut accéder à ses éléments grâce à différentes fonctions. Une liste est soit vide, soit composée de deux parties : un premier élément, et une sous liste. En Haskell une liste vide sera représentée par "[]" et une construction d'élément sera séparée de ":". Syntaxe des fonctions d’accès : 1. Les constructeurs : a. val empty : list {retourne une liste vide} b. val cons : item->list->list {retourne la liste à partir de l'élément et de la liste donnés en paramètres, l’élément sera en tête de liste} 2. Les fonctions à prédicat : a. val isempty : list->bool {retourne vrai si la liste est vide, faux sinon} 3. Les sélecteurs : a. val head : list->item {retourne l’élément en tête de liste} b. val tail : list->list{retourne la liste sans l’élément en tête de liste} EPFC | Bashevkin Nathan & Polazzi Fabio 15 Epreuve intégrée : La Programmation Fonctionnelle 2007 Sémantique des fonctions d’accès : 1. isempty empty = true 2. isempty empty(cons x xs) = false 3. head empty = error 4. head(cons x xs)=x 5. tail empty = error 6. tail(cons x xs)=xs L’écriture d’une liste est assez simple. En effet tous les éléments sont compris entre deux crochets et sont séparés pas une virgule. Il est possible de construire une liste grâce à l’opérateur ':' qui est parfois appelé 'cons' (abréviation de constructeur) où par exemple h : t est une liste dans laquelle t est une liste et h est un élémént qui sera ajouté au début de cette liste. Exemple : >let list = [2,3,4] >in 1 : list [1,2,3,4] -- let sert à déclarer A noter que l’écriture des listes peut également, et plus simplement, se faire sans crochets. Dans ce cas, on sépare les éléments de la liste par ':' et ceux-ci auront une écriture plus proche de leur apparence habituelle. Exemple : >’h’ : "ello" "hello" Il est intéressant de savoir que l’opérateur ':' est associatif vers la droite. En effet a : b : c == a :(b : c) != (a : b) : c Il existe également l’opérateur '++' qui au lieu d’ajouter un élément au début d’une liste, permet d’ajouter une liste à une autre. Exemple : >let l = [1,2,3] >let m = [4,5] >in l++m [1,2,3,4,5] 16 EPFC | Bashevkin Nathan & Polazzi Fabio Epreuve intégrée : La Programmation Fonctionnelle 2007 Si par contre nous avions fait "> l : m", nous aurions eu une erreur car le compilateur aurait cherché à créer une liste de liste et attend un argument de type [[4,5]] plutôt que [4,5]. >let l = [1,2,3] >let m = [[4,5]] >in l:m [[1,2,3],[4,5]] Pour trouver un élément d’une liste, il existe l’opérateur '!!' que l'on utilise avec le numéro de rang (l’index) de l’élément recherché. L’indexation commence à zéro. Exemple : > let m = [2,3,5,7,9] > in m !! 3 7 Exemple 2 : > let x = [[1],[2,3],[4,5,6]] > in x !! 1 [2,3] Si on avait une liste qui comprend des sous-listes comme dans le deuxième exemple mais qu’on tapait "(x !! 2) !! 0", quel serait le résultat ? On irait dans la troisième liste (x !! 2) et là on recherche le premier élément (!! 0). On aurait pu omettre les parenthèses autour de x !! 2, car l’opérateur ‘ !! ‘ est associatif vers la gauche !. Tous les opérateurs différents peuvent bien entendu être utilisés dans la même expression. Exemple : > let x = ["ab", "cde"] > in '(' : ( x !!1 !!2 : x !!1 ++ x !!0) ++ ")." "(ecdeab)." Nous allons maintenant voir en détail plusieurs fonctions qui s'appliquent particulièrement aux listes. Les listes sont un objet très important de la programmation et Haskell nous offre des outils pratiques, surtout quand on regarde du côté des langages de programmation impérative. 17 EPFC | Bashevkin Nathan & Polazzi Fabio Epreuve intégrée : La Programmation Fonctionnelle 2007 i. "Maps" Un besoin récurrent en programmation est de faire une opération sur un tableau (ou une liste). On utilise le principe de l'itération, qui va parcourir le tableau élément par élément. En programmation fonctionnelle, c'est très facile. On utilise la fonction map en relation avec une fonction f qui produit le résultat requis. La définition de map est la suivante : map f [] = [] map f (x:xs) = f x : map f xs On applique la fonction f sur l'élément de tête, et on applique récursivement l'ensemble map f sur le reste de la liste. Supposons qu'on ait une liste d'élément [1,3,5,7,9]. On cherche à avoir le carré de chaque élément. Supposons la fonction square qui calcule ce carré. map square [1,3,5,7,9] donnera le résultat [1,9,25,49,81] On peut même définir une fonction squareall qui va calculer tout seul le carré de chaque élément d'une liste. let squareall = map square square x = x * x in squareall [1,3,5,7,9] 18 EPFC | Bashevkin Nathan & Polazzi Fabio Epreuve intégrée : La Programmation Fonctionnelle 2007 ii. "Filters" Les filtres sont une autre façon de parcourir un tableau, mais au lieu d'appliquer la fonction sur chaque élément, ils ne prennent que les éléments qui répondent à un critère particulier booléen (vrai ou faux). Par exemple, nous voudrions que filter negatif [1,-5,-7,3,4,0] retourne [-5,-7] lorsque negatif est défini comme negatif x = x < 0. iii. "Folds" Les pliages (folds en anglais) peuvent être vus comme un mécanisme liant les éléments d'une structure par des fonctions ou des valeurs d'une manière régulière. Le pliage insère une fonction entre chaque élément de la liste. Pour avoir une idée plus concrète, le pliage de la fonction Addition sur une liste [1,2,3,4,5] donnerait le résultat 1+2+3+4+5. Nous distinguons deux types de pliages, les foldl (fold left) et les foldr (fold right). Son implémentation en Haskell peut être représentée comme ceci : foldr f z [] = z -- si la liste est vide, le résultat est z foldr f z (x:xs) = f x (foldr f z xs) -- sinon, on applique f sur le premier élément et sur l'ensemble de fonction foldr f z xs, xs étant le reste de la liste. Ex: foldr f z (a,b,c) = f a (f b (f c z)) 19 foldl f z [] = z EPFC | Bashevkin Nathan & Polazzi Fabio Epreuve intégrée : La Programmation Fonctionnelle 2007 -- si la liste est vide, le résultat est z foldl f z (x:xs) = foldl f (f z x) xs -- sinon, on parcourt récursivement la liste. On applique la fonction f sur le premier élément et sur z, et on applique la fonction f sur ce résultat avec le prochain élément de la liste et ainsi de suite jusqu'à la fin. Ex: foldl f z (a,b,c) = f (f (f z a) b) c Remarquons que foldr est plus pratique et naturel que foldl car il permet de travailler sur des listes infinies, ce qui n'est pas rare en Haskell. foldl quant à lui est remarquable par sa récursivité terminale. iv. "Scans" Un scan est plus ou moins un mélange entre un map et un fold. Plier (fold) une liste retourne une seule valeur qui est une accumulation de tous les éléments, tandis que mapper applique une fonction sur chaque élément sans les accumuler. Un scan fait les deux : il accumule les éléments pour obtenir une valeur comme un pliage, mais au lieu de ne renvoyer que la valeur finale il renvoie les valeurs intermédiaires. De base il y a quatres fonctions de scan. scanl :: (a -> b -> a) -> a -> [b] -> [a] Cette fonction accumule la liste depuis la gauche, et le second paramètre devient le premier de la liste renvoyée comme résultat. Ainsi : scanl (+) 0 [1,2,3] = [0,1,3,6] scanl1 :: (a -> a -> a) -> [a] -> [a] Cette fonction est la même que scanl mais elle utilise le premier élément de la liste comme premier paramètre. Ainsi : scanl1 (+) [1,2,3] = [1,3,6] scanr scanr1 :: (a -> b -> b) -> b -> [a] -> [b] :: (a -> a -> a) -> [a] -> [a] Ces deux fonctions sont les correspondantes des deux premières dans l'autre sens. Ainsi: EPFC | Bashevkin Nathan & Polazzi Fabio 20 Epreuve intégrée : La Programmation Fonctionnelle 2007 scanr (+) 0 [1,2,3] = [6,5,3,0] scanr1 (+) [1,2,3] = [6,5,3] v. "Zip" Nous avons vu dans un exemple précédent la fonction zipWith. Pour rappel, elle applique une fonction entre chaque élément de même indice sur deux listes et retourne le résultat dans une 3ème liste. Dans le cadre de notre travail qui consiste en l'implémentation de Dijkstra, voir plus loin, nous utilisons la fonction zip. Cette fonction crée une liste de tuples à partir des éléments de même indice sur deux listes. zip :: [a] -> [b] -> [(a,b)] Ainsi: zip [1,2,3] [9,8,7] = [(1,9),(2,8),(3,7)] vi. "nub" La fonction nub sert à enlever les éléments déjà présents dans la liste. nub :: [a] -> [a] Ainsi: nub [0,1,2,3,2,1,2,2,1,1,3,3,3,0,0,2,1] = [0,1,2,3] nub "AAAAAAAAAAAABBBBBBBBBBBBBBCCCCC" = "ABC" vii. "Reverse" Reverse permet d'inverser l'ordre des éléments dans une liste. reverse :: [a] -> [a] Ainsi: reverse [1..5] = [5,4,3,2,1] viii. "Delete" Delete supprime le premier élément rencontré sur une liste, cet élément étant passé comme paramètre. Ainsi: delete 2 [1,2,3,2,1] = [1,3,2,1] EPFC | Bashevkin Nathan & Polazzi Fabio 21 Epreuve intégrée : La Programmation Fonctionnelle 2007 ix. "lookup" Dans une liste composée de tuples, on peut concevoir chaque tuple comme un ensemble attribut-valeur. lookup renvoie la valeur d’un attribut donné. Ainsi : lookup 'c' [('a',0),('b',1),('c',2)] = Just 2 lookup 'f' [('a',0),('b',1),('c',2)] = Nothing x. "fromJust" Importé de la librairie maybe, fromJust génèrera éventuellement une exception en combinaison avec une autre fonction Ainsi: fromJust (lookup y [(x,'A'),(y,'B'),(z,'C')]) = 'B' fromJust (lookup w [(x,'A'),(y,'B'),(z,'C')]) = ***Exception= Maybe.fromJust = Nothing xi. "elem" Elem est une fonction booléenne qui indique si un élément est dans une liste ou pas. elem :: a -> [b] -> Bool Ainsi: elem 1 [1,2,3,4,5] = True xii. "minimum" Minimum est une fonction qui renvoie le plus petit élément d’une liste. minimum :: [a] -> b Ainsi: minimum [1,2,5,7,0,9] = 0 b. Queue : Une queue est une séquence linéaire d’un nombre quelconque d’éléments du même type. Il est possible d’ajouter un élément à la queue mais seulement à la fin de celle-ci ; contrairement aux listes et piles. Le mode d’accès est donc FIFO (First In, First Out). EPFC | Bashevkin Nathan & Polazzi Fabio 22 Epreuve intégrée : La Programmation Fonctionnelle 2007 Syntaxe des fonctions d’accès : 1.Les constructeurs : a. val empty : queue {retourne une queue vide} b. val add : item->queue->queue {retourne la queue avec l’élément en plus à la fin de celle-ci} 2.Les fonctions à prédicat : a. val isempty : queue->bool {retourne vrai si la queue est vide ; faux sinon} 3.Les sélecteurs : a. val front : queue->item {retourne l’élément se trouvant en tête de liste} b. val back : queue->queue {retourne la queue sans l’élément de tête} Sémantique des fonctions d’accès : Nous allons vous donner quelques exemples où l’élément sera représenté par x et la queue par xs. Les 4 premiers exemples sont explicites. 1. 2. 3. 4. isempty empty = true isempty(add x xs) = false front empty = error back empty = error Désormais , on va s’intéresser à des exemples plus intéressants... 5. front (add x xs) = if isempty xs then x else front xs Cet axiome dit en effet que si on ajoute un élément à la fin d’une queue vide, ce sera également l’élément de tête ; et donc l’élément à retourner. Autrement, nous ignorons l’élément x (qui est désormais en fin de queue) et demandons de trouver l’élément en tête de queue. 6. back (add x xs) = if isempty xs then empty else add x (back xs) Si on ajoute un élément à une queue vide, cet élément est comme expliqué plus haut l’élément de fin et de tête de queue. Or la fonction back retourne la queue sans l’élément de tête de queue donc le résultat sera vide. Sinon il faudra ajouter l’élément x à la fin et appeler récursivement la fonction pour retourner la queue sans l’élément de liste. Voici un exemple bien précis, où nous avons une queue (1,2,3) Back (1,2,3) back (add 3 (1,2)) –- (1,2,3)=(add 3 (1,2)) add 3 (back (1,2)) –- par commutativité add 3 (add 2 (back (1))) -- par definition de back add 3 (add 2 (back (add 1 empty))) -–(1)=(add 1 empty)) EPFC | Bashevkin Nathan & Polazzi Fabio 23 Epreuve intégrée : La Programmation Fonctionnelle add 3 (add 2 empty) add 3 (2) (2,3) 2007 -– par définition -- add 2 empty = (2) c. Arbre binaire: Les structures de données comme listes, queues, piles ont toutes un ordre linéaire sur leur élément : chaque élément a au plus un prédécesseur et un successeur. Par contre, dans le cas des arbres, l’élément peut avoir plusieurs successeurs. Les recherches peuvent être donc beaucoup plus rapides. Un arbre binaire est une structure de données qui peut se représenter sous la forme d'une hiérarchie dont chaque élément est appelé nœud, le nœud initial étant appelé racine. Un arbre est soit vide, soit composé de deux parties : un nœud, et deux sous-arbres. Syntaxe des fonctions d’accès : 1. Les constructeurs : a. val empty : tree {retourne un arbre vide} b. val cons : item->tree->tree->tree {retourne un nouvel arbre à partir de l’élément et des arbres gauches et droites donnés en paramètres, il sera la racine de ces 2 arbres} 2. Les fonctions à prédicat : a. val isempty : tree->bool {retourne vrai si l’arbre est vide ;faux sinon} 3. Les sélecteurs : a. val root : tree->item {retourne l’élément se trouvant à la racine de l’arbre} b. val left : tree->tree {retourne le sous arbre gauche} c. val right : tree->tree {retourne le sous arbre droit} Sémantique des fonctions d’accès : 1. isempty empty = true 2. isempty (cons i l r) = false 3. root empty = error 4. root (cons i l r) = i 5. left empty = error 6. left (cons i l r) = l 7. right empty = error 8. right (cons i l r) =r 24 EPFC | Bashevkin Nathan & Polazzi Fabio Epreuve intégrée : La Programmation Fonctionnelle 2007 d. Graphe : Le graphe est également non linéaire sur l’ordre des éléments mais est encore plus général que l’arbre puisque l’élément peut avoir un nombre quelconque de successeurs et de prédécesseurs. Le graphe contient un certain nombre de nœuds et de "liens" où le lien est la connexion entre deux nœuds. Il peut y avoir un poids associé à chaque lien pour passer d’un nœud à un autre, poids qui peut être un prix, une distance, un temps… Le graphe peut alors être représenté de la manière suivante : type Weight = Int type Id = Int type Edge = ( Id, Id ) type Graph = [ ( Edge, Weight ) ] Syntaxe des fonctions d’accès : 1. Les constructeurs : a. val empty :graph {retourne un graphe vide} b. val addedge : edge->graph->graph {ajoute un lien au graphe} 2. Les fonctions à prédicat : a. val isempty : graph->bool {retourne vrai si le graphe est vide ;faux sinon} b. val isin : edge->graph->bool {retourne vrai si le lien se trouve dans le graphe ;faux sinon} c. val contains : graph->node->bool {retourne vrai si le nœud est dans le graphe ;faux sinon} 3. Les sélecteurs : a. val delete_edge : edge->graph->graph {efface le lien du graphe si il existe, sinon cette fonction n’a pas d’effets} b. val adj : node->graph->node list {retourne une liste de nœuds qui sont directement accessibles à partir du nœud donné} c. val sources : graph->node list {retourne tous les nœuds d’où les liens du graphe partent} d. val dests : graph->node list {retourne tous les nœuds d’où les liens du graphe arrivent} Sémantique des fonctions d’accès : 25 1. isempty empty = empty 2. isempty (addedge e g) = false 3. addedge e1 g = if (isin e1 g) then g else addedge e1 g EPFC | Bashevkin Nathan & Polazzi Fabio Epreuve intégrée : La Programmation Fonctionnelle 2007 On remarque que si le lien existe déjà, il ne fera rien 4. contains empty n = false 5. contains (addedge (n,m) g) p = if p = n or p = m then true else contains g p 6. isin e1 empty = false 7. isin e1 (addedge e2 g) = if e1 = e2 then true else isin e1 g 8. delete_edge e1 empty = empty 9. delete_edge e1 (addedge e2 g) = if e1 = e2 then delete_edge e1 g else addedge e2 (delete_edge e1 g) 10. adj n empty = [] 11. adj n (addedge (p,q) g) = if n = p then q :: adj n g else adj n g 12. sources empty = [] 13. sources (addedge (n,m) g) = n :: sources g 14. dests empty = [] 15. dests (addedge (n,m) g) = n :: dests g e. Pile En informatique, une pile est une sorte de liste d'élément sur laquelle nous avons la possibilité de faire certaines opérations définies. Visualisons une pile comme… une pile d'objets. Nous pouvons ajouter des éléments sur le sommet de cette pile, et nous pouvons en retirer. Cependant nous ne pouvons pas retirer un élément qui ne se trouve pas au sommet. Nous allons illustrer plusieurs opérations : 1. mettre un élément sur la pile (opération que l'on va appeler push) 2. retirer 2 éléments, en faire une addition, et les remettre sur la pile (add) 3. même chose qu'en 2, mais en faisant une soustraction (sub) 4. même chose qu'en 2, mais en faisant une multiplication (mult) 5. même chose qu'en 2, mais en faisant une division (div) 6. créer une pile vide (emptystack) 7. sortir le dernier élément de la pile (pop) Chaque opération est en fait une fonction. Dans le cas de push, l'opération va prendre un nombre en paramètre, et l'ajouter à une pile qui est également passée en paramètre. Il y a deux façons d'écrire cette fonction : avec un tuple push (x,stack) = x:stack 26 EPFC | Bashevkin Nathan & Polazzi Fabio Epreuve intégrée : La Programmation Fonctionnelle 2007 avec la méthode Curry push x stack = x:stack Pour les 4 opérations mathématiques, cela se fait de manière très simple. add (x:y:stack) = y+x:stack sub (x:y:stack) = y-x:stack mult (x:y:stack) = y*x:stack div (x:y:stack) = y-x:stack Notez notre façon d'appeler les 2 premiers éléments de la pile. Nous indiquons l'élément x, puis y, puis le reste de la pile qui s'appelle stack. Le ":" dans la partie de droite concatène le résultat au reste de la pile. Pour créer une liste vide, nous définissons emptystack = [] et pour accéder au sommet de la pile pop [single] = single. Si nous voulions sortir le sommet de la pile, on aurait pu définir pop tel que pop (top:rest) = (top,rest). Maintenant que nous avons défini ces fonctions, il nous est possible de représenter des opérations mathématiques. Soit 12,2 * ( 7,1 / 6,7 - 4,3 ) + 2,2 En notation polonaise inverse (une représentation qui permet de se passer de parenthèse tout en respectant les règles de priorité) nous avons 12,2 7,1 6,7 / 4,3 - * 2,2 + Ceci est facilement traduisible en Haskell en une série d'opérations sur une pile. let s0 = emptystack s1 = push 12,2 s0 s2 = push 7,1 s1 s3 = push 6,7 s2 s4 = div s3 s5 = push 4,3 s4 s6 = sub s5 s7 = mult s6 s8 = push 2,2 s7 stack = add s8 EPFC | Bashevkin Nathan & Polazzi Fabio 27 Epreuve intégrée : La Programmation Fonctionnelle 2007 in pop stack Il faut cependant comprendre qu'il n'y a pas d'ordre chronologique dans ces définitions. On pourrait en changer l'ordre sans changer le résultat… Quelques opérateurs importants Dans ce chapitre nous allons présenter quelques opérateurs importants que nous utilisons dans notre implémentation de Dijkstra. a. !! L'opérateur !! accepte une liste et un entier a et retourne l'élément de la liste qui se trouve à la position a de la liste. L'indexation commence à zéro. [a] !! int -> a Ainsi: [6,5,4,3,2,1] !! 2 = 4 b. < L'opérateur < accepte deux valeurs numériques (entier ou réels) et retourne une valeur booléenne : vrai si la première valeur est plus petite, fausse sinon. Ord < Ord -> bool Ainsi: 4 < 5 = True c. > L'opérateur > accepte deux valeurs numériques (entier ou réels) et retourne une valeur booléenne : vrai si la première valeur est plus grande, fausse sinon. Ord > Ord -> bool Ainsi: 4 > 5 = False 28 EPFC | Bashevkin Nathan & Polazzi Fabio Epreuve intégrée : La Programmation Fonctionnelle 2007 d. $ L’opérateur $ crée une association des éléments qui se trouvent à droite d’une fonction. La fonction porte alors sur l’ensemble des éléments. a $ b -> a (b) Ainsi: abs $ -12 = 12 e. && L'opérateur && prend deux conditions booléennes et retourne un booléen : vrai si les deux conditions sont vérifiées, faux sinon. bool && bool -> bool Ainsi: 1 elem [1,2,3,4] && odd 3 = True La fonction odd retourne vrai si la valeur est impaire, faux sinon. f. ** L'opérateur ** prend deux réels et retourne la puissance du deuxième réel appliqué sur le premier. Float ** Float -> Float Ainsi: 2 ** 16 = 65536.0 g. : L'opérateur : insère le premier argument dans le deuxième qui est une liste. L'insertion se fera par le début de cette liste. a -> [a] -> [a] Ainsi: 2 : [1,5,3] = [2,1,5,3] 29 EPFC | Bashevkin Nathan & Polazzi Fabio Epreuve intégrée : La Programmation Fonctionnelle 2007 h. == L'opérateur == prend deux "Eq" (char, double, float, int) et retourne vrai si les deux arguments sont égaux, faux sinon. Eq == Eq -> bool Ainsi: 'c' == 'b' = False 2.00 == 2 = True i. || L'opérateur || prend deux booléens et retourne un booléen : vrai si une des deux conditions est vérifiée, faux sinon bool || bool -> bool Ainsi: 1 elem [1,2,3,4] && False = True 30 EPFC | Bashevkin Nathan & Polazzi Fabio Epreuve intégrée : La Programmation Fonctionnelle 2007 Le programme 31 EPFC | Bashevkin Nathan & Polazzi Fabio Epreuve intégrée : La Programmation Fonctionnelle 2007 6. Programme : Algorithme de Dijkstra 1. Description de l’algorithme Pour certaines applications, par exemple l’optimisation de réseaux, il convient de connaître le plus court chemin possible. L’informaticien hollandais Dijkstra a trouvé un algorithme en 1959 qui calcule le plus court chemin entre une source et tous les sommets accessibles de cette source. Le poids des arcs (des chemins) doit être positif ; ces poids peuvent représenter par exemple des distances, des coûts... et ce sont toutes ces mesures qu’il faut économiser. La distance du chemin sera donc la somme de tous les poids des arcs par lesquels on est passé. Entre le départ et l’arrivée il y a plein de points intermédiaires. Pour connaître le plus court chemin il convient donc de calculer le poids minimal entre une source et tous ses sommets ; on mettra à jour chaque sommet qui sera prédécesseur d’un autre sommet lorsque celui-ci est plus petit. On ne retient que le plus court entre deux sommets ; il est inutile en effet de retenir un coût plus important entre deux sommets car il correspondrait à un détour inutile. Nous allons utiliser une technique qui s’appelle relâchement : c’est une méthode qui diminue progressivement une borne supérieure sur le poids du plus court chemin pour chaque sommet jusqu’à ce que le plus court chemin soit atteint. Pour chaque sommet du graphe, nous avons un champ qui est cette borne supérieure. Ce champ est initialisé à +infini. Le relâchement d’un arc (src,dest) est un test permettant de savoir si le chemin passant par src pour arriver à dest est un chemin plus court que le précédent chemin qui arrivait à dest, et si c’est le cas ce nouveau chemin est le plus court. On ne modifiera les estimations que grâce au relâchement. On initialisera les estimations de plus court chemin et les prédécesseurs de cette manière: tous les nœuds du graphe sont blanc, le champ prédécesseur est mis à blanc, et le poids à +infini. L’algorithme de Dijkstra résout le problème de la distance minimale du sommet d’un graphe orienté vers chacun de ses sommets. Il maintient à jour un ensemble contenant les sommets dont les poids finaux de plus court chemin à partir de l’origine ont déjà été calculés. 32 EPFC | Bashevkin Nathan & Polazzi Fabio Epreuve intégrée : La Programmation Fonctionnelle 2007 2. Choix de l’environnement et du logiciel a. Système d’exploitation : comme nous n’avons pas de besoins particuliers ni de contraintes à respecter, nous utilisons Windows XP simplement par commodité. b. Logiciels i. Hugs 98 (Haskell User's Gofer System): Hugs 98 est un système de programmation fonctionnelle basé sur Haskell 98. Nous avons installé la version Windows du programme, WinHugs, mais il existe aussi pour MacOS ou Linux. Hugs est un interpréteur Haskell, ce qui signifie qu’il ne fait qu’exécuter des instructions Haskell directement. Lien : http://www.haskell.org/hugs/ ii. GHC (Glasgow Haskell Compiler) : GHC est un compilateur qui existe également sur plusieurs plateformes. Un compilateur est un programme qui traduit des instructions écrites dans une langue en un programme écrit en langage machine, avant d’exécuter ce programme fraichement créé. Lien : http://www.haskell.org/hugs/ 3. Mise en situation Afin d'illustrer l'algorithme de Dijkstra, nous avons créé un environnement. Nous avons imaginé un jeu d'échec dans lequel un personnage essaie d'aller d'une case de l’échiquier à une autre. Nous avons choisi le cavalier car sa façon de se déplacer est intéressante (en tout cas, plus que le fou ou la tour), mais le programme peut très bien se modifier pour implémenter les déplacements d'un autre personnage. Pour exécuter le programme, nous devons fournir un couple de position qui représentent la case Départ et la case Arrivée. Le programme va ensuite retourner sous forme de liste les cases par lesquelles le cavalier serait passé. 4. Préparation du programme Nous avons importé plusieurs bibliothèques pour pouvoir utiliser les fonctions qui ont été implémentées dans celles-ci. import Data.List (delete, minimumBy, nub) import Data.Char (intToDigit, digitToInt, toLower) import Data.Maybe (fromJust) Nous avons deux fonctions rowInts et rowChars qui nous permettent de pouvoir prendre l'équivalent entre le caractère 'a' et le chiffre '1'. En effet, grâce à la fonction zip nous EPFC | Bashevkin Nathan & Polazzi Fabio 33 Epreuve intégrée : La Programmation Fonctionnelle 2007 allons avoir une liste de tuples avec le caractère et son chiffre correspondant. Ces deux fonctions sont indispensables car dans notre implémentation tout se calcule grâce aux valeurs numériques alors que nous faisons croire à l'utilisateur que c'est une lettre suivi d'un chiffre pour donner la coordonnée de la case. rowInts = zip ['a'..'h'] [1..8] rowChars = zip [1..8] ['a'..'h'] Nous avons le data Square qui correspond à une case de l'échiquier. On remarque que Square est composé de deux entiers d'où la nécessité des deux fonctions qui ont été précédemment expliquées. data Square = Square (Int,Int) deriving Eq L'instance Show Square sert à afficher les coordonnées de la case sur l'écran. Elle utilise la fonction rowChars pour l'afficher comme une lettre suivie d'un chiffre. Nous utilisons la fonction lookup qui a été définie plus tôt dans le cours. instance Show Square where show (Square (a,b)) = show $ (fromJust (lookup a rowChars)) : ((intToDigit b) : []) L'instance Read Square sert à lire les coordonnées de la case entrée par l'utilisateur et à la traduire par un couple de deux chiffres grâce à la fonction rowInts. instance Read Square where readsPrec _ (a : b : []) = [ (Square (fromJust (lookup (toLower a) rowInts), digitToInt b),"")] La variable Cost contiendra le coût pour atteindre la case. Dans notre algorithme, les cases accessibles à partir d’une case donnée ont un coût égal à 1, et toutes les autres à +infini. data Cost = Infinity | Finite Int deriving (Eq, Show) L'instance Ord Cost est un prédicat que nous avons réalisé pour préciser certains comportements liés à l’infini tel que l'infini qui n'est pas plus petit qu'un nombre quelconque, que l'infini est plus petit ou égal à l'infini etc... EPFC | Bashevkin Nathan & Polazzi Fabio 34 Epreuve intégrée : La Programmation Fonctionnelle instance Ord Cost where Infinity <= Finite a Infinity <= Infinity Finite a <= Infinity Finite a <= Finite b = = = = 2007 False True True a <= b L'instance Num Cost est un autre prédicat qui nous permet de faire plusieurs opérations sur l'infini comme par exemple additionner un réel avec l'infini ou prendre la valeur absolue de l'infini. Ce prédicat nous permet aussi de faire les cas généraux comme additionner deux réels par exemple. A noter que dans le prédicat nous utilisons le caractère '_' qui désigne tous les autres cas qui n'ont pas été défini auparavant. instance Num Cost where Finite a + Finite b = Finite (a+b) _ + _ = Infinity Finite a - Finite b = Finite (a-b) _ - _ = Infinity Finite a * Finite b = Finite (a*b) _ * _ = Infinity signum (Finite a) = Finite (signum a) signum Infinity = Infinity abs (Finite a) = Finite (abs a) abs Infinity = Infinity fromInteger a = Finite (fromInteger a) Move contient la case de l'échiquier et son coût correspondant. data Move = Move (Square, Cost) deriving Eq L'instance Ord Move sert à comparer deux coûts. instance Ord Move where compare a b = compare (getCost a) (getCost b) L'instance Show Move nous affichera le mouvement à l'écran. instance Show Move where show (Move x) = show x La fonction getCost reçoit un data Move et retourne le coût. Remarquons de nouveau la présence du caractère ‘_’ qui sert à ne pas devoir se soucier de l’élément. getCost ( Move (_ ,c)) = c EPFC | Bashevkin Nathan & Polazzi Fabio 35 Epreuve intégrée : La Programmation Fonctionnelle 2007 La fonction getSquare reçoit un data Move et retourne la case de l'échiquier. getSquare ( Move (s , _ )) = s 5. L’algorithme de Dijkstra a. Relâchement Le relâchement est la méthode principale que l’on utilise dans l’algorithme de Dijkstra. Très concrètement, elle vérifie s’il est possible, en passant par un sommet, d’améliorer le plus court chemin jusqu’à un autre point et de mettre à jour le coût permettant de l’atteindre, ainsi que son prédécesseur. Pour faire cela, nous devons initialiser le coût pour atteindre chaque sommet à une valeur infinie. Aucun sommet n’a de prédécesseur particulier. Dans le pseudo code suivant, on considère un ensemble G, de sommet initial S, et où le coût pour atteindre chaque sommet sera représenté par d[]. Le sommet initial est quant à lui initialisé à 0. // source unique initialisation pour chaque sommet v d’un ensemble G, faire d[v] <- infini pere[v] <- pas_de_prédécesseur fpour d[S] <- 0 Voyons comment nous adaptons cette méthode en Haskell. singleMoveCost :: Square -> Square -> Cost singleMoveCost (Square (x, y)) (Square (x', y')) | abs (x - x') ==1 && abs (y - y') == 2 || abs (x - x') ==2 && abs (y - y') == 1 = 1 | otherwise = Infinity Nous avons une fonction qui calcule le coût entre deux cases que nous voulons atteindre. Comme nous avons choisi le Cavalier, nous ne pouvons nous déplacer qu’en L, c’est-àdire en variant x de 1 ET y de 2, OU x de 2 et y de 1 dans n’importe quel sens. Si la case n’est pas atteignable en faisant un L, nous lui donnons la valeur infinie. Sinon, elle a un coût égal à 1. Pour cela nous utilisons les gardes | , et nous vérifions la différence entre les coordonnées en valeur absolue, afin de gérer aussi bien les mouvements en arrière qu’en avant. Cette initialisation a lieu dans notre fonction principale. Ainsi, à l’origine, seules les cases accessibles depuis la case Départ ont un coût égal à 1, les autres sont initialisés à Infinity. EPFC | Bashevkin Nathan & Polazzi Fabio 36 Epreuve intégrée : La Programmation Fonctionnelle 2007 Le processus de relâchement peut alors avoir lieu. Dans cette méthode, nous vérifions si le coût pour atteindre une case en passant par les cases précédentes est inférieur à sa valeur actuelle. Sachant que son coût est initialisé à l’infini, s’il fait partie du chemin à parcourir pour atteindre la case Arrivée, son coût sera mis à jour. Dans le pseudo code suivant, u est le sommet actuel, v le sommet que l’on veut rejoindre prochainement, et w la fonction de coût entre deux sommets. Si le coût pour atteindre v est plus grand que le coût mis à jour pour atteindre u, que l’on additionne au coût de l’arc entre u et v, on le met à jour. Ensuite on relie v à u. //relâcher (u,v,w) si d[v] > d[u] + w(u,v) alors d[v] <- d[u] + w(u,v) pere[v] <- u fsi Voyons comment nous avons adapté la fonction relâcher en Haskell. relacher (Move (square, cost)) | alternativeCost < cost = Move (square, alternativeCost) | otherwise = Move (square, cost) where alternativeCost = minCost + (singleMoveCost minSquare square) Nous effectuons notre fonction relâcher sur chaque mouvement caractérisé par une case et un coût. Nous calculons un coût alternatif qui équivaut au coût calculé pour atteindre la case précédente, auquel nous ajoutons le coût éventuel pour atteindre la case actuelle. Si ce coût alternatif est plus petit que le coût actuel de la case, nous le remplaçons. Sinon, nous le gardons. b. Dijkstra Dans le pseudo code suivant, nous considérons un ensemble G d’élément, de sommet initial s, et w la fonction de coût entre deux sommets. Au départ nous initialisons les sommets en utilisant la fonction précédemment expliquée. L’algorithme de Dijkstra utilise l’ensemble E pour contenir les sommets dont les coûts finaux de plus court chemin ont déjà été calculés, ainsi qu’une file (liste) F qui contient les sommets de l’ensemble G. A chaque fois que l’on prend un sommet (le sommet de coût minimum) de la file pour le mettre dans l’ensemble E, nous effectuons un relâchement et ensuite nous l’enlevons de la file. EPFC | Bashevkin Nathan & Polazzi Fabio 37 Epreuve intégrée : La Programmation Fonctionnelle 2007 Ainsi : E est vide, F contient tous les sommets, et E se remplira au fur et à mesure des sommets de F, qui se videra petit à petit. // Dijkstra (G,w,s) Source-unique-initialisation (G,s) E <- 0 F <- S[G] tantque F <> 0 faire u <- extraireMin (F) E <- E U {u} pour chaque sommet v adjacent à u faire relacher (u,v,w) fpour ftantque Voyons comment nous avons adapté cet algorithme en Haskell. dijkstra :: Square -> [Move] -> [Move] -> [Move] dijkstra arrivee moves chemin | elem arrivee (map getSquare chemin) = chemin | otherwise = dijkstra arrivee relachement (minMove:chemin) Notre fonction dijkstra prend en entrée une case d’arrivée, une liste de mouvements possible ainsi qu’une liste vide qui représente le chemin que prendra le cavalier. Notre fonction est une fonction récursive terminale. Nous distinguons deux cas : > Soit nous trouvons la case arrivée dans la liste chemin, et nous renvoyons comme résultat la liste chemin. Ceci est vérifié avec la fonction elem comme nous l’avons vu dans le cours. > Soit nous rappelons la fonction dijkstra avec les paramètres suivants : o arrivée o relachement o minMove :chemin where minMove = minimum moves Move (minSquare,minCost) = minMove EPFC | Bashevkin Nathan & Polazzi Fabio 38 Epreuve intégrée : La Programmation Fonctionnelle relachement moves = map relacher $ delete 2007 minMove minMove est le plus petit mouvement possible que l’on peut effectuer à partir de la case courante. Nous l’obtenons grâce à la fonction minimum qui est définie de base dans Prelude. On l’ajoute en tête de la liste chemin, qui est vide au départ, et qui contiendra au final la liste des mouvements effectués. Nous sauvons également les attributs du mouvement dans les variables minSquare et minCost, que nous utilisons plus loin. Grâce à la fonction map, la fonction relachement exécute la fonction relacher sur la liste de mouvements possibles à partir de la case actuelle, à laquelle nous avons enlevé minMove. Ainsi nous ne pouvons pas revenir sur nos pas. relacher (Move (square, cost)) | alternativeCost < cost = Move (square, alternativeCost) | otherwise = Move (square, cost) where alternativeCost = minCost + (singleMoveCost minSquare square) Nous retrouvons la fonction relacher, que nous avons déjà expliqué plus haut. Nous retrouvons les variables minSquare et minCost qui servent à calculer le cout alternatif. Au final, la fonction dijkstra renvoie une liste de mouvements qui ont été parcouru avant d’atteindre la case arrivée. c. Fonction principale solve :: String -> [Square] solve argument = reverse $ chemin $ dijkstra arrivee (moves depart) [] Notre fonction principale prend une chaine de caractères en paramètre, l’interprète et renvoie le résultat sous forme de liste de cases de l’échiquier. Nous appelons ici dijkstra avec les paramètres expliqués ci-dessus, et qui sont définis dans la suite de la fonction. Remarquez la fonction chemin, nous l’expliquons plus bas. where 39 moves m = map (Move) $ zip echiquier (map (singleMoveCost m) echiquier) EPFC | Bashevkin Nathan & Polazzi Fabio Epreuve intégrée : La Programmation Fonctionnelle 2007 echiquier = [Square (x,y) | x <- [1..8], y <- [1..8]] arguments = map read (words argument) depart = head arguments arrivee = head $ tail arguments La fonction moves va créer une liste de tuples (zip) entre les cases de l’échiquier et le coût pour atteindre chaque case ; coût qui sera infini s’il n’est pas accessible directement depuis la case m. Arguments va prendre la chaine de caractère entrée en paramètres comme une liste de mots. Depart est le premier élément de cette liste, et arrivee le deuxième (et dernier dans notre cas). Enfin, nous avons la fonction chemin qui prend une liste de mouvements et retourne une liste de cases. chemin :: [Move] -> [Square] chemin moves = nub $ scanl1 attachedTo squares where attachedTo x y | singleMoveCost x y == 1 = y | otherwise = x squares = map (getSquare) moves La fonction prend les éléments de la liste passée en paramètre, qui est en fait le résultat de la fonction dijkstra, et ne garde que les cases accessibles entre elle en un mouvement, grâce à la fonction attachedTo. Cette liste de cases forme alors le résultat final inverse : en effet, dans la fonction dijkstra, nous ajoutons les éléments au fur et à mesure au début de la liste. C’est pour cette raison que l’on trouve la fonction reverse dans la fonction principale. 40 EPFC | Bashevkin Nathan & Polazzi Fabio Epreuve intégrée : La Programmation Fonctionnelle 2007 6. Illustration Pour illustration, voici un échiquier que vous pouvez utiliser pour visualiser le déplacement du cavalier. 1 2 3 4 5 6 7 8 A B C D E F G H 41 EPFC | Bashevkin Nathan & Polazzi Fabio Epreuve intégrée : La Programmation Fonctionnelle 2007 Fin 42 EPFC | Bashevkin Nathan & Polazzi Fabio Epreuve intégrée : La Programmation Fonctionnelle 2007 Conclusion Vous voici arrivés à la fin de notre travail. Ensemble nous avons vu un cours parlant de la programmation fonctionnelle, et de Haskell en particulier. Le cours, sans être particulièrement difficile, nous a fait lire plusieurs livres ainsi que des sites internet. Nous espérons en avoir fait une synthèse claire et facile à comprendre, même pour les personnes qui ne sont pas habituées à l’informatique. Nous avons beaucoup détaillé les fonctions qui s’appliquent sur les listes car nous les avons utilisés pour notre programme. Nous avons réalisé notre programme grâce à la division en problèmes plus simples dont nous parlons dans le cours. Au cours de nos recherches, nous avons ainsi pu séparer les (nombreux) problèmes et y trouver des solutions. Notre implémentation utilise un échiquier dont les cases ont un coût entre elle simplifié par rapport à un vrai graphe. Cela nous a paru plus réalisable, mais aussi plus compréhensible. Concernant la réalisation du travail, il ne nous a pas toujours été évident de nous voir pour travailler à deux sur le programme, compte tenu de nos emplois du temps. Nous avons pu nous répartir la réalisation du cours mais cela a été plus laborieux lors de celle du programme. Pour finir nous tenons à remercier Monsieur Pirlot et Monsieur Silovy, pour les livres qu’ils nous ont prêtés et le suivi qu’ils nous ont offert, ainsi que Monsieur Poulain pour son soutien moral et intellectuel. 43 EPFC | Bashevkin Nathan & Polazzi Fabio Epreuve intégrée : La Programmation Fonctionnelle 2007 Appendice Voici pour documentation le code source du programme. 44 EPFC | Bashevkin Nathan & Polazzi Fabio Epreuve intégrée : La Programmation Fonctionnelle 2007 Bibliographie Sites internet : Haskell Wiki, WWW.HASKELL.ORG : http://www.haskell.org/haskellwiki/haskell Haskell, WWW.WIKIBOOKS.ORG : http://en.wikibooks.org/wiki/Haskell Haskell Reference, WWW.ZVON.ORG : http://www.zvon.org/other/haskell/Outputprelude/index.html Index of the Haskell 98 Prelude, WWW.HASKELL.ORG : http://www.haskell.org/onlinereport/prelude-index.html A History of Haskell : Being lazy with class, RESEARCH.MICROSOFT.COM http://research.microsoft.com/~simonpj/papers/history-of-haskell/history.pdf Livres : COUTURIER A. et GERALD J.B. Programmation Fonctionnelle Spécifications & Applications - Cépades-Editions, Toulouse 2003 DAVIE A.J.T. An introduction to functional programming systems using Haskell - Cambridge University Press, Cambridge 1992 HARRISON R. Abstract Data Types in Standard ML - John Wiley & Sons Ltd, Grande-Bretagne 1993 HOLYER I. Functional Programming with Miranda Pitman Publishing, GrandeBretagne 1991 45 EPFC | Bashevkin Nathan & Polazzi Fabio