Epreuve intégrée : La Programmation Fonctionnelle

publicité
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
Téléchargement