Professional Documents
Culture Documents
Module Informatique 2
Deug Sciences mention MIAS
-
Spécification et construction d’algorithmes :
Approche fonctionnelle 1
-
12 janvier 19952
1
Disponible à l’adresse : http://www.sciences.univ-nantes.fr/info/enseignement/deug/info2/cours.html
2
Dernière mise à jour : janvier 2000
2
Table des matières
1 Introduction 5
2 Concepts de base 7
2.1 Expressions, Valeurs, Types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
2.1.1 Définitions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
2.1.2 En Caml . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2.1.3 En Pascal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
2.2 Fonctions, composition, application . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
2.2.1 Définitions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
2.2.2 En Caml . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
2.2.3 En Pascal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
2.3 Méthodologie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
2.3.1 Spécification . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
2.3.2 Analyse Descendante . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
2.4 Récursivité . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
2.4.1 Principes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
2.4.2 En Caml . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
2.4.3 En Pascal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
4 Ordre Supérieur 35
4.1 Abstraire par les fonctions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
4.2 Programmation d’ordre supérieur . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
4.2.1 En Caml . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
4.2.2 En Pascal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
5 Preuves de programmes 41
5.1 Eléments de Logique . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
5.2 Induction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
5.3 Preuves et transformations de programmes . . . . . . . . . . . . . . . . . . . . . . 45
3
4 TABLE DES MATIÈRES
6 Conclusion 49
Introduction
Le cours du module Informatique 2, dispensé en 1ère année de Deug Sciences option MIAS
(Mathématiques, Informatique et Applications aux Sciences) à l’Université de Nantes, est consacré
à la spécification et à la construction d’algorithmes, en suivant une approche fonctionnelle.
Les ouvrages de référence sont [SFLM93] pour l’aspect algorithmique et [WL99] pour l’aspect
programmation. On pourra aussi utilement consulter [HHDGV92, AS89, Xuo92].
Cet enseignement a pour objectifs :
– d’apprendre
– à modéliser des problèmes,
– à utiliser une méthode pour les résoudre (spécifier, réaliser, coder, analyser et prouver) ;
– de donner
– une formation de base en algorithmique et programmation, à laquelle le module Informa-
tique 1 a déjà contribué,
– une pratique d’un langage fonctionnel : Caml, associée à des compléments sur un langage
impératif : Pascal.
Les prérequis de ce cours sont la pratique d’un langage impératif, par exemple Pascal tel qu’il
est présenté dans le module Informatique 1.
Ce document est structuré pour amener le lecteur à assimiler les notions progressivement, en
partant d’exemples simples et en allant peu à peu vers davantage d’abstraction :
– d’abord, il présente l’ensemble des notions de base permettant de spécifier puis de programmer
des solutions à des problèmes relativement simples (chapitre 2) ;
– puis, il introduit des mécanismes d’abstraction permettant de concevoir des algorithmes plus
complexes et plus généraux :
– abstraction portant sur les informations (chapitre 3),
– abstraction portant sur les fonctions (chapitre 4).
Le document propose ensuite une réflexion sur l’activité qu’est la programmation, en donnant
des outils théoriques pour prouver et transformer des programmes (chapitre 5).
Enfin, la conclusion (chapitre 6) est suivie d’une définition de la syntaxe du sous-ensemble du
langage Caml considéré dans le module (annexe A).
5
6 CHAPITRE 1. INTRODUCTION
Chapitre 2
Concepts de base
Valeurs : En informatique, on appellera valeur toute information donnée ou qui a été calculée.
Par exemple : 1, 3.1415926536, 22/7 sont des valeurs (respectivement entière, décimale et fraction-
naire). (3, 4) est une valeur dans N 2 .
Types : La nécessité de représenter ces valeurs dans un format interne pour une machine (dont
on ne parlera pas dans ce cours), oblige à préciser l’ensemble auquel appartient une valeur (tout
simplement parce que dans un certain codage, la touche A est représentée par le code 01000001, et
que dans un autre codage le nombre 65 est aussi représenté par le code 01000001 : il faut pouvoir
distinguer les deux).
De plus, une résolution particulière d’un problème peut s’appliquer à un certain ensemble de
valeurs et ne pas s’appliquer pour un autre. Par exemple la division euclidienne est définie pour
les polynômes sur Q, mais pas pour les polynômes sur Z.
Ces deux raisons motivent la notion de type en informatique.
7
8 CHAPITRE 2. CONCEPTS DE BASE
oublier qu’au moment de l’exécution sur une machine le type entier (=Z) sera réduit à un
intervalle d’entiers).
Le type réel contient les nombre réels. La plupart des langages n’offrent des réels, que le sous-
ensemble des décimaux (à virgule fixe ou flottante), avec une précision limitée.
Le type caractère est un ensemble de caractères typographiques correspondant (plus ou moins)
à l’ensemble des touches du clavier. Ce type est nécessaire car c’est l’ensemble des “valeurs
élémentaires” (une touche) que l’utilisateur peut entrer au clavier.
Le type chaı̂ne est l’ensemble des suites de caractères : une valeur de ce type peut donc être une
phrase. On doit noter caractères et chaı̂nes entre ’ ou “ pour dire que l’on parle du caractère
ou de la chaı̂ne en tant que tel, et les distinguer des autres mots ou lettres figurant dans un
algorithme.
Le type booléen est l’ensemble {vrai, faux}. Cet ensemble riche par sa structure algèbrique
(treillis, algèbre de Boole) est fondamental en informatique car il permet d’exprimer comment
et dans quel cas un calcul doit dépendre ou non du résultat d’autres calculs et des données.
Expressions : On appelle expression une forme symbolique, contenant des valeurs et des
opérateurs servant à exprimer une nouvelle valeur.
Exemples : 1 + 1/1! + 1/2!, faux ou (3*4 = 0) sont des expressions.
Toute expression a un type qui détermine l’ensemble dans lequel elle prend sa valeur. La
première expression est de type réel, la seconde de type booléen. La forme d’une expression suffit
à définir son type qui dépend de la définition de l’opérateur le plus global, et du types de ses
opérandes...qui dépendent eux mêmes de leur opérateur et opérandes...
Evaluer une expression consiste à calculer sa valeur. La valeur du premier exemple est 2.5 ; celle
du second est faux.
On utilisera dans les algorithmes toute notation mathématique usuelle pour noter les expres-
sions. En particulier on notera f(x) le résultat d’une fonction f appliquée à une valeur x.
On distinguera :
– les opérateurs arithmétiques (entiers et/ou réels) : +, −, ∗, /, a mod b, a div b, cos, sin, xy ,
ln, exp ...
– les opérateurs booléens : ou, et, non.
– les opérateurs relationnels : =, <, >, 6=, ≤, ≥ (pour entiers, réels, caractères et chaı̂nes).
– l’opérateur de concaténation de caractères et chaı̂nes de caractères : &.
– l’expression alternative :
si expression booléenne alors expression sinonexpression
– l’expression de choix selon des valeurs :
selon selecteur dans valeur : expression ...
– l’expression de choix suivant des conditions :
suivant expression booléenne : expression ...
Les expressions alternatives et de choix ont une valeur : la valeur de l’expression correspondant
à la valeur du sélecteur ou à la condition vraie (qui doit être unique). Les expressions de choix
peuvent comporter un choix autrement.
Définition locale Pour écrire des expressions de manière symbolique et pour éviter d’écrire deux
fois la même sous expression, on introduit la notion de définition locale. Cela consiste à nommer
la valeur d’une sous-expression, et à utiliser ce nom dans l’expression.
Exemple : L’expression soit y = cos (3.1416 * 12) dans y + 1/(2 y^2) est
équivalent à : cos (3.1416 * 12) + 1/(2 (cos (3.1416 * 12))^2)
2.1.2 En Caml
Le langage Caml permet d’écrire des expressions et de connaı̂tre leur type et leur valeur. L’uti-
lisation du langage se fait par interaction constante entre l’utilisateur et le système : l’utilisateur
soumet une expression qui est alors typée et évaluée.
La plupart des expressions vues ci-dessus ont une traduction directe en Caml. Il convient
cependant de distinguer les opérations entières des opérations sur les flottants.
#12 + 8;;
- : int = 20
#if (6 = 7) then "oui" else "non";;
- : string = "non"
#true or false & (4 = 5);;
- : bool = true
#let y = cos (3.1416 *. 12.0)
#in y +. (1.0 /. (2.0 *. y *. y)) ;;
- : float = 1.5
#let x = -3 and y = 9 in
#match ((if x > 0 then 1
# else if x = 0 then 0
# else -1) * (if y > 0 then 1
# else if y = 0 then 0
# else -1))
#with 1 -> ‘+‘
# | 0 -> ‘0‘
# | -1 -> ‘-‘ ;;
Entrée interactive:
>match ((if x > 0 then 1
> else if x = 0 then 0
> else -1) * (if y > 0 then 1
> else if y = 0 then 0
> else -1))
>with 1 -> ‘+‘
> | 0 -> ‘0‘
> | -1 -> ‘-‘...
Attention: ce filtrage n’est pas exhaustif.
- : char = ‘-‘
Une session Caml consiste à soumettre au système une suite de phrases, après le symbole de
disponibilité #. Une phrase peut être soit une expression, soit une définition globale, terminée dans
les deux cas par ; ;
Une définition globale let x = <expression> permet de donner une valeur à l’identificateur
x pour toute la suite de la session.
10 CHAPITRE 2. CONCEPTS DE BASE
#let x = 5 + 2;;
x : int = 7
#x;;
- : int = 7
#let y = x + 1;;
y : int = 8
2.1.3 En Pascal
Pour toutes les expressions simples, la traduction en Pascal a été vue dans le module Informa-
tique 1.
Les expressions alternatives et de choix n’ont pas de traduction directe en Pascal. On peut
utiliser les instructions alternatives et de choix, en mettant dans chaque cas une instruction d’af-
fectation à la même variable.
De même la définition locale n’existe pas en Pascal , mais peut être traduite par une variable
locale supplémentaire et une affectation.
Remarque : Le nom d’une fonction ne sert pas (et pourrait être supprimé de la définition ci-
dessus) à définir son comportement, sauf dans le cas de fonctions récursives (voir 2.4.1). Le nom
sert seulement à nommer une fonction pour pouvoir l’utiliser ailleurs qu’à l’endroit où elle est
définie. De nombreux langages (dont Caml) permettent de définir des fonctions sans leur donner
de nom.
Exemple : On veut écrire une fonction calculant le produit scalaire de deux vecteurs u et v dans
R2 connaissant leurs coordonnées u1,u2, v1 et v2.
fonction produit_scalaire : reel x reel x reel x reel -> reel
u1, u2, v1, v2 -> u1 * v1 + u2 * v2
Remarque : Cette notation se veut proche de la notation de fonction en mathématiques où l’on
écrirait :
. : R x R x R x R -> R
u1, u2, v1, v2 -> u1 * v1 + u2 * v2
2.2. FONCTIONS, COMPOSITION, APPLICATION 11
Application : L’application d’une fonction consiste à fournir des valeurs d’entrée à cette fonc-
tion. Le résultat de cette application de fonction est un P-uplet de valeurs, obtenues en remplaçant
les paramètres par les valeurs fournies en entrée. On notera l’application d’une fonction f à une
donnée x par f(x). C’est ainsi que l’on utilise une fonction.
Exemple : produit_scalaire (1, 0, 2.5, 3)
Ecrivons maintenant une fonction orthogonaux utilisant les mêmes paramètres et donnant un
résultat booléen, vrai si et seulement si les deux vecteurs sont orthogonaux.
fonction orthogonaux : reel x reel x reel x reel -> booleen
u1, u2, v1, v2 -> produit_scalaire(u1,u2,v1,v2) = 0
2.2.2 En Caml
En Caml, on peut traduire les fonctions-algorithmes précédentes par des expressions de la forme
suivante :
function (par1, par2,...) -> (exp1, exp2, ...)
On peut entrer simplement une fonction comme cela ; elle sera, comme toute expression, typée,
mais n’a pas encore de valeur :
#function u1, u2, v1, v2 -> u1 *. v1 +. u2 *. v2 ;;
- : float * float * float * float -> float = <fun>
On peut l’évaluer sur un exemple :
#(function u1, u2, v1, v2 -> u1 *. v1 +. u2 *. v2)
# (1.0, 2.0, 5.5, 4.5) ;;
- : float = 14.5
Mais le plus pratique est d’inclure cette fonction dans une définition globale qui permet de la
nommer, et de l’utiliser ensuite pendant toute la session.
#let produit_scalaire = function
# u1, u2, v1, v2 -> u1 *. v1 +. u2 *. v2 ;;
produit_scalaire : float * float * float * float -> float = <fun>
#let orthogonaux = function
# u1, u2, v1, v2 -> produit_scalaire(u1,u2,v1,v2) = 0.0;;
orthogonaux : float * float * float * float -> bool = <fun>
#orthogonaux(1.5, 2.0, 2.0, -1.5);;
- : bool = true
#let norme = function
# u1, u2 -> sqrt (produit_scalaire(u1,u2,u1,u2));;
norme : float * float -> float = <fun>
#norme(1.0, 2.0);;
- : float = 2.2360679775
12 CHAPITRE 2. CONCEPTS DE BASE
2.2.3 En Pascal
L’écriture de fonctions en Pascal est possible, et peut être considérée comme un cas simplifié
de procédures (voir module Informatique 1). En effet, on n’y utilise pas le mode de passage de
paramètres en var. Les particularités des fonctions en Pascal concernent la façon de spécifier le
type du résultat, et la façon de retourner la valeur d’une fonction.
Le type du résultat doit être un type simple ; il est indiqué dans l’entête de la fonction, après
les paramètres.
La valeur de la fonction est affectée (souvent à la fin de son texte), par une affectation
nomdelafonction := valeur.
Exemple :
program vecteurs ;
var x, y , n: real;
function produit_scalaire (u1, u2, v1, v2:real):real ;
begin
produit_scalaire := u1 * v1 + u2 * v2
end;
function norme (u1, u2: real): real;
begin
norme := sqrt (produit_scalaire(u1,u2,u1,u2))
end;
begin
readln(x,y);
n := norme (x,y);
writeln (’Norme = ’, n)
end.
2.3 Méthodologie
2.3.1 Spécification
Les paragraphes précédents ont introduit successivement les notions de type, d’expressions, de
fonctions et ont donné un certain nombre d’exemples d’algorithmes puis de programmes.
Il s’agit maintenant d’inscrire ces notions dans une méthodologie de conception qui permette
de passer de la manière la plus sûre et la plus efficace, d’un problème à résoudre, à sa solution en
terme de programme exécutable.
La méthodologie proposée comporte les points suivants :
Spécification : Il faut d’abord identifier le problème (lui donner un nom), faire le bilan des
données disponibles, préciser leur type, définir quel type de résultat on attend, et bien sûr
décrire ce résultat (sans dire comment le calculer). Cette phase résulte en une fonction, dont
on choisit le nom, on précise les types, et dont on commente le fonctionnement.
Algorithme : L’écriture de l’algorithme consiste à définir le corps de la fonction spécifiée. On par-
lera souvent de fonction-algorithme pour indiquer qu’on écrit des algorithmes sous forme de
fonction. Ceci se fera éventuellement en utilisant d’autres fonctions qu’il faudra alors spécifier
et pour lesquelles il faudra donner un algorithme. Ces aspects, dits d’analyse descendante,
seront précisés dans le prochain paragraphe.
Programme : C’est la phase appelée aussi codage, où l’on traduit l’algorithme dans un langage
donné (dans notre contexte Caml ou Pascal). Les principaux problèmes sont alors d’utiliser les
constructions adéquates du langage en veillant au respect des règles de syntaxe, et d’utiliser
les types présents dans le langage en veillant à ce que leurs limitations (par exemple sur les
nombres) ne mettent pas la solution en défaut.
Vérification et tests : Dans le cas de la programmation en Caml, une vérification partielle est
fournie par le calcul du type : on doit vérifier que le type calculé correspond au type que l’on
avait spécifié. L’expérience montre que dans beaucoup de cas, une erreur dans l’algorithme,
entraine une erreur de type ; on retourne alors à la phase algorithme.
Dans le cas Pascal, la compilation est précédée par une vérification de types qui permet aussi
des détections d’erreurs.
Il reste ensuite à réaliser des tests fonctionnels, qui peuvent permettre, de déceler d’éventuelles
erreurs de fonctionnement. Par contre, l’impossibilité pratique de réaliser des tests exhaustifs
(quand au moins une donnée appartient à un type infini), ne permet pas de prouver que la
fonction est totalement correcte.
Exemple : Un cinéphile veut enregistrer un film de durée x minutes et qui commence à une
heure donnée ; il veut pour cela connaı̂tre l’heure de fin et savoir si oui ou non, le film finira le
lendemain.
Spécification : On commence par étudier les informations dont on dispose, et celles que l’on
veut calculer. Ce qui nous donne la spécification de fonction suivante :
Fin_du_film : entier*entier*entier -> entier*entier*booleen
(* Fin_du_film ( hdebut,mndebut,duree) calcule l’heure de fin,
la minute de fin (qui correspondent au temps de debut + duree),
et un booleen vrai s’il y a passage a 0h *)
Vérification et tests : le type calculé correspond bien au type spécifié. On fera quelques tests
montrant différents cas possibles.
# fin_du_film (22, 35, 123);;
- : int * int * bool = 0, 38, true
# fin_du_film (20, 45, 92);;
- : int * int * bool = 22, 17, false
Exemple : On se propose d’écrire une fonction de conversion entre unités de longueur du système
métrique et unités anglo-saxonnes. On se limitera aux mètre (m), centimètre (cm), pied (ft) et pouce
(inch). On a : 1 inch = 2.54 cm et 1 ft = 12 inch. On veut pouvoir convertir la représentation
décimale d’une longueur dans n’importe quelle unité (m, cm, ft, inch), en la représentation de la
même longueur dans les deux unités de l’autre système. Par exemple 1.77 m -> 5 ft 10 inch et
30.5 ft -> 9 m 29 cm. On veut de plus, c’est une habitude, obtenir des résultats seulement en
pouces pour des longueurs inférieures à 2 pieds. On écrit la spécification suivante :
2.3. MÉTHODOLOGIE 15
fonction conversion :
reel * chaine -> entier * chaine * entier * chaine
(* conversion (longueur, unite) est un quadruplet
contenant une longueur dans la plus grande unite’,
le nom de l’unite’, une seconde longueur et le nom
de la deuxieme unite’. *)
On commence par décomposer en 2 cas : la conversion d’unité SI vers unités anglo-saxonnes, et
l’inverse. Chacun des 2 cas va aussi se décomposer selon l’unité donnée, mais ces deux cas pourront
être traités ensemble à condition de ramener les données en cm ou en inch.
Pour cela on va spécifier deux fonctions :
fonction cm_to_ft_inch :
reel -> entier * chaine * entier * chaine
(* cm_to_ft_inch (longueur) calcule le
resultat en fonction d’une longueur en cm *)
fonction inch_to_m_cm :
reel -> entier * chaine * entier * chaine
(* inch_to_m_cm (longueur) calcule le
resultat en fonction d’une longueur en inch *)
On peut alors réaliser la fonction conversion :
fonction conversion :
reel * chaine -> entier * chaine * entier * chaine
longueur, unite ->
selon unite
"m" : cm_to_ft_inch (100 * longueur)
"cm" : cm_to_ft_inch (longueur)
"ft" : inch_to_m_cm (12 * longueur)
"inch" : inch_to_m_cm (longueur)
Il faut maintenant réaliser les fonctions spécifiées. On va réaliser la fonction cm_to_ft_inch
en continuant à décomposer (structurellement), ceci en isolant comme résultat intermédiaire la
représentation de la longueur en inch.
fonction cm_to_inch : reel -> entier
(* convertit cm en inch, avec arrondi *)
fonction inch_to_ft_inch :
entier -> entier*chaine*entier*chaine
(* calcule le resultat final a partir d’une
longueur en inch, superieure a 2 ft *)
fonction cm_to_ft_inch :
reel -> entier * chaine * entier * chaine
cm -> soit inch = cm_to_inch (cm)
dans si inch < 24
alors ( 0, "ft", inch, "inch")
sinon inch_to_ft_inch (inch)
Réalisons maintenant les deux fonctions spécifiées :
fonction cm_to_inch : reel -> entier
cm -> arrondi( cm / 2.54)
16 CHAPITRE 2. CONCEPTS DE BASE
fonction inch_to_ft_inch :
entier -> entier*chaine*entier*chaine
inch -> ( quotient (inch, 12), "ft",
reste (inch,12), "inch")
2.4 Récursivité
2.4.1 Principes
Définitions : On dit qu’une fonction f appelle une fonction g, si le texte qui définit f comporte
une application de la fonction g, ou une application d’une fonction h qui appelle la fonction g.
L’écriture d’une fonction f est dite récursive, si elle s’appelle elle même. Un tel appel est
dit appel récursif. On parlera de récursivité simple quand f comporte une application de f, et de
récursivité croisée dans le cas général.
Se pose alors le problème du sens à donner à de telles définitions : dans un dictionnaire on ne
définit pas un mot en l’employant dans la définition. De même, on ne peut se contenter de dire,
par exemple, que la somme de deux entiers x et y est définie comme étant la somme des entiers y
et x. Bien que strictement vraie, cette affirmation ne donne aucun renseignement sur le calcul de
cette somme.
L’utilisation de la récursivité, en informatique, est à rapprocher de l’usage intensif que les
mathématiques font du principe de récurrence. Les rapports entre les deux seront explicités au
chapitre 5 (voir en particulier section 5.2 et section 5.3, page 45).
Le principe de récurrence sur N, permet de prouver une propriété pour tout entier, en la
démontrant pour l’entier 0, et en démontrant que si elle est vraie pour un entier n, elle l’est pour
n + 1.
Définir une fonction récursivement consiste à :
– Définir la valeur de la fonction pour un ensemble de cas de base, (sans utiliser d’appels
récursifs)
– La définir dans le cas général, en fonction de valeurs de la fonction dans des cas plus simples.
La notion de “plus simple” doit pouvoir permettre d’arriver à un cas de base, en un nombre
fini d’étapes.
On peut ainsi, par exemple, définir une fonction f sur N, en définissant f(0) et en définissant
f(n) en fonction de f(n-1).
Exemple : La fonction factorielle est définie par n! = n ∗ (n − 1)! pour n > 0 et 0! = 1. On peut
donc écrire l’algorithme suivant :
On peut aussi utiliser la récursivité quand une fonction s’exprime simplement pour un certain
sous-ensemble des données, et si l’on peut se ramener à ce sous-ensemble.
Remarques : Pour qu’un algorithme récursif se termine, il faut (il ne suffit pas) que seule une
partie de cet algorithme ne comportant pas d’appel récursif soit évaluée pour les cas de base. Les
expressions alternatives et de choix n’évaluent que la bonne expression, et sont donc adéquates pour
exprimer un algorithme où seule une partie doit être évaluée. On peut aussi utiliser les opérateurs
booléens et puis et ou alors, qui ont le même sens respectivement que et et ou, mais dont le
second argument n’est évalué que si nécessaire. Le prédicat suivant indique si un nombre entier est
pair.
fonction pair : entier -> booleen
n -> (n = 0) ou alors ( (n <> 1) et puis pair (n-2) )
L’écriture récursive d’une fonction peut être très simple, dès lors que l’on connaı̂t une formule
de récurrence, où qu’elle est déjà présente dans la spécification du problème (ce qui est souvent le
cas dans des exemples mathématiques).
Si ce n’est pas le cas, cette écriture requiert un travail d’analyse, visant à découvrir une méthode
permettant de résoudre un problème, connaissant une solution à ce même problème dans un cas
plus simple. On utilise souvent pour cela une notion liée à la taille des données.
Par exemple, cherchons à définir le résultat trié d’une suite de n entiers : le problème est
élémentaire pour des suites de 1 ou 2 entiers. Pour une suite de longueur n, on peut la découper en
deux : le résultat sera alors la fusion du tri de chacunes des deux sous-suites, la fusion consistant à
calculer une suite triée issue du mélange de deux suites triées (c’est souvent ce que l’on fait pour
trier à la main un jeu de 52 cartes).
On verra au chapitre 3 comment utiliser la récursivité avec des types de données plus complexes :
listes et types récursifs (voir en particulier section 3.2, pages 23 et 26, ainsi que sections 3.3 et 3.4).
2.4.2 En Caml
La définition d’une fonction récursive doit se faire par la construction :
let rec.
#let rec fact = function n -> if n = 0 then 1
# else n * fact (n - 1);;
fact : int -> int = <fun>
La fonction puissance2 indique si oui ou non, un entier est une puissance de 2. Sa définition
repose sur le fait que l’ensemble des puissances de 2 contient 1 et les nombres pairs dont la moitié
est une puissance de 2. Les opérateurs & et or de Caml correspondent respectivement à et puis
et ou alors.
#let rec pair =
# function n -> (n = 0) or ( (n <> 1) & pair (n-2) )
#and puissance2 =
# function n -> (n = 1) or (pair (n) & puissance2 (n/2));;
pair : int -> bool = <fun>
puissance2 : int -> bool = <fun>
#puissance2 (65536);;
- : bool = true
Le calcul du quotient de la division euclidienne de a par b avec b > 0 peut s’écrire (la preuve
sera donnée au chapitre 5, page 45) :
#let rec quotient = function (a,b) ->
# if (0<=a) & (a<b) then 0
# else if a<0 then -1 + quotient(a+b, b)
# else 1 + quotient (a-b, b);;
quotient : int * int -> int = <fun>
18 CHAPITRE 2. CONCEPTS DE BASE
2.4.3 En Pascal
En Pascal, les fonctions peuvent être récursives, sans déclaration particulière.
function f ;
begin
if n = 1 then f:= 0
else f:= g (n)
end;
function g ;
begin
2.4. RÉCURSIVITÉ 19
g := f ( n div 2) + 1
end;
begin
readln(n);
res := f(n);
writeln (’f (’, n, ’) = ’, res)
end.
20 CHAPITRE 2. CONCEPTS DE BASE
Chapitre 3
Définition : L’abstraction des données est une méthode d’analyse consistant à séparer :
– d’une part, la façon d’utiliser les informations d’un nouveau type : c’est ce que l’on appellera
spécifier un type,
– d’autre part, les moyens permettant l’utilisation de ce type : c’est ce que l’on appelera réaliser
un type.
21
22 CHAPITRE 3. TYPES ET STRUCTURES DE DONNÉES
Spécifier un type, consiste à lui donner un nom, à décrire l’ensemble des valeurs qu’il
doit représenter, et à spécifier un ensemble suffisant de fonctions : constructeur(s), sélecteur(s),
opérateur(s) et prédicat(s), permettant d’utiliser ce type, dans un algorithme, comme un type de
base.
Réaliser un type consiste à décrire comment est représentée l’information en termes des types
disponibles, et à réaliser les fonctions spécifiées : ce point sera esquissé en 3.2, mais ne peut trouver
une solution complète que selon le langage utilisé (voir 3.2.1 et 3.2.2).
Exemple : On veut écrire un logiciel de calcul sur les rationnels. Pour cela on peut spécifier le
type rationnel ainsi :
type rationnel (* l’ensemble Q *)
Remarque : Noter qu’on peut choisir de spécifier un ensemble de fonctionnalités de base plus
petit que celui qui vient d’être indiqué.
Par exemple, on peut ne retenir que les fonctions rationnel, numerateur et denominateur.
On définira alors les fonctions
rationnel_entier, plus_rat, egal_rat et representation_externe
3.2. TYPES CONSTRUITS 23
Exemples :
– Un rationnel peut être représenté par deux entiers,
– Un temps par trois entiers (heure, minute et secondes),
– Une date, sous la forme lundi 6 fevrier 1995, par deux chaines de caractères et deux
entiers.
– Un vecteur de R2 par deux réels
Type produit : Tous ces exemples de valeurs sont des N-uplets qui appartiennent à un produit
cartésien de types de base (respectivement Z 2 , Z 3 ...).
On appelle type produit un type défini comme un produit cartésien de types.
Dans la pratique des langages informatiques, on trouve deux sortes de types produits :
– Les enregistrements (ou types produits à champs nommés) : un type enregistrement comporte
plusieurs champs, chacun défini par son nom et son type. Par exemple, soit un enregistrement
rationnel, comportant les champs numerateur et denominateur, chacun étant de type
entier. Si x est un rationnel, on accède à un champ par la notation suivante : x.numerateur
– Les N-uplets : sont définis par un n-uplet de types. Par exemple, on peut réaliser le type
vecteurR2 par un couple de réels, et noter un élément : (3.5, 9.99)
Type somme : Le mécanisme précédent ne permet pas de construire tous les types nécessaires.
En effet, il impose de faire figurer le même ensemble d’informations pour toutes les valeurs d’un
type. Or, dans la pratique, on aimerait pouvoir exprimer, par exemple, qu’un enregistrement
décrivant l’état civil d’une personne contienne différents champs selon le sexe de cette personne.
On appelle type somme un type construit par union disjointe de types. L’union disjointe de
deux ensembles est une union permettant de savoir à quel sous-ensemble appartient un élément
de l’union. Un type somme peut ainsi contenir tel type d’information, ou tel autre. Ceci est très
utile quand pour une même entité (dont on veut définir le type), on peut disposer d’informations
différentes.
Exemples : On veut définir un type distance qui regroupe des distances terrestres (entières en
m) et des distances nautiques (réelles en milles). On le définit comme la somme des types réel et
entier. 1852 metres, 12.5 Milles sont des éléments de ce type distance. Il est important ici que
l’union des deux types réel et entier soit disjointe, ce qui permet de savoir à quelle partie appartient
un élément de type distance.
Les exemples suivants de types, définis par énumération de valeurs, peuvent être assimilés à
des types sommes, car ils sont définis par une union de singletons comportant chacun une valeur.
L’ensemble des chiffres romains {I, V, X, L, C, D, M } est défini par énumération. C’est l’union
de {I}, {V} ...
Il en est de même pour l’ensemble des jours de la semaine {lundi, mardi,..., dimanche}
Types récursifs : Les définitions de type précédentes permettent de structurer des données qui,
soit comportent toutes plusieurs informations (type produit), soit comportent selon les cas des
informations différentes (type somme).
24 CHAPITRE 3. TYPES ET STRUCTURES DE DONNÉES
On s’intéresse maintenant au cas où le type d’une des composantes contient les mêmes infor-
mations que le type à définir.
Exemple : On veut définir un type personne qui comporte les champs nom, prenom, et le pere et
la mere, qui sont eux aussi des personnes. Une personne contiendra donc les informations suivantes :
nom, prenom, nom et prénom du père, nom et prénom du père du père... etc
Pour qu’une personne soit représentée par une quantité d’information finie, il faut prévoir le
cas d’une personne inconnue, qui servira à désigner le père (ou la mère) d’une personne de père
(ou de mère) inconnu.
Définition : Un type, dont la définition fait référence à lui-même est dit type récursif.
3.2.1 En Caml
Type produit - Enregistrement : Voici une première façon de programmer le type rationnel :
on définit rationnel comme un type enregistrement, en donnant les noms (appelés étiquettes) de
champs num et den, et leurs types.
#type rationnel = {num : int; den:int};;
Le type rationnel est défini.
La construction d’un enregistrement se fait par une expression :
{etiquette = expr ; ... }
Par exemple, soit x = rationnel (1, 2) peut être traduit en Caml :
#let x = {num = 1; den = 2};;
x : rationnel = {num = 1; den = 2}
#let y = {num= 3; den = 4};;
y : rationnel = {num = 3; den = 4}
L’accès à un champ respecte la syntaxe usuelle : expr.etiquette, l’expression numerateur(r)
peut être traduite en Caml : r.numerateur. La fonction plus_rat peut alors s’écrire :
#let plus_rat = function
# (r1, r2) -> {num = r1.num * r2.den + r2.num * r1.den ;
# den = r1.den * r2.den} ;;
plus_rat : rationnel * rationnel -> rationnel = <fun>
#plus_rat (x,y);;
- : rationnel = {num = 10; den = 8}
Remarque : On peut aussi, au lieu de traduire chacune des expressions permettant de construire
ou d’accéder à un type directement avec les constructions du langage Caml, traduire les fonctions
correspondantes. Par exemple :
#let numerateur = function r -> r.num ;;
numerateur : rationnel -> int = <fun>
Cette méthode a l’avantage de continuer à cacher la représentation d’un rationnel, dans les pro-
grammes l’utilisant (cf. oppose_rat en section 3.1). On peut ainsi donner une réalisation complète
des fonctions du type rationnel. Ceci permet à un programmeur utilisant le type rationnel de tout
ignorer de sa réalisation, et d’utiliser, s’il existe plusieurs réalisations, l’une quelconque d’entre
elles.
On peut avoir, par exemple, une réalisation qui réduit les rationnels à chaque opération, une
autre qui ne les réduit qu’au moment de donner la représentation externe. L’essentiel pour l’utili-
sateur est d’obtenir le même résultat pour un calcul comme :
3.2. TYPES CONSTRUITS 25
representation_externe
(plus_rat (rationnel (3,8), rationnel (2,3)))
Type produit - N-uplets : L’utilisation des N-uplets (type produit non nommé) est très simple
en Caml. On les contruit :
#(4, 7, 9);;
- : int * int * int = 4, 7, 9
#(true, "bonjour");;
- : bool * string = true, "bonjour"
L’accès aux éléments d’un couple se fait par les fonction fst (premier) et snd (second).
#fst (4, 8);;
- : int = 4
#snd (4, 8);;
- : int = 8
Type somme - Enuméré : En Caml, les types énumérés sont des cas particuliers de type
somme. La définition suivante, indique qu’une valeur de type jour peut être soit le constructeur
Lundi, soit le constructeur Mardi ...
On utilisera l’expression match ... with... pour distinguer selon le constructeur utilisé.
#type jour = Lundi | Mardi | Mercredi | Jeudi |
# Vendredi | Samedi | Dimanche ;;
Le type jour est défini.
#let lendemain = function jour -> match jour with
# Lundi -> Mardi |
# Mardi -> Mercredi|
# Mercredi -> Jeudi |
# Jeudi -> Vendredi |
# Vendredi -> Samedi |
# Samedi -> Dimanche |
# Dimanche -> Lundi ;;
lendemain : jour -> jour = <fun>
#lendemain (Mardi);;
- : jour = Mercredi
Type somme : Dans le cas général, un type somme est défini comme le choix entre plusieurs
constructeurs, chaque constructeur étant défini par son nom et la liste de ses arguments. Exemple :
le constructeur Mille à un argument de type float.
#type distance = Mille of float
# | Km_metre of int * int ;;
Le type distance est défini.
Pour construire une valeur d’un type somme, on utilise une expression de la forme :
constructeur expression ou constructeur si ce constructeur n’a pas d’argument (exemple
précédent).
#Km_metre (1 , 852);;
- : distance = Km_metre (1, 852)
#Mille (2.5);;
- : distance = Mille 2.5
26 CHAPITRE 3. TYPES ET STRUCTURES DE DONNÉES
L’accès aux arguments d’une valeur d’un type somme, se fait grâce à l’expression de filtrage :
match expression with
constructeur1 motif1 -> expression1 |
constructeur2 motif2 -> expression2 ...
dans laquelle les motifs sont des listes d’identificateurs. L’expression match, selon le construc-
teur utilisé dans l’expression va faire correspondre terme à terme aux identificateurs du motif, les
arguments du constructeur. Ces identificateurs pourront être utilisés dans l’expression correspon-
dante.
#let EnKm = function d ->
# match d with
# Km_metre(km,m) -> float_of_int(m+1000*km)/. 1000.0
# | Mille (x) -> x *. 1.8519 ;;
EnKm : distance -> float = <fun>
#EnKm (Km_metre (3, 750));;
- : float = 3.75
#EnKm (Mille 2.5);;
- : float = 4.62975
Le filtrage peut aussi être effectué dans la définition d’une fonction (équivalente à la formulation
précédente) :
#let EnKm = function
# Km_metre(km,m) -> float_of_int(m+1000*km)/. 1000.0
# | Mille (x) -> x *. 1.8519 ;;
EnKm : distance -> float = <fun>
Types récursifs : Le type personne, discuté en 3.2 (page 24) peut être réalisé en Caml de la
façon suivante :
#type personne = MrouMme of string * string * personne * personne
# | Inconnu ;;
#let ADupont =
#MrouMme ("Antoine", "Dupont",
# MrouMme ("Jean", "Dupont",
# MrouMme ("Pierre", "Dupont", Inconnu,Inconnu),
# MrouMme ("Marie", "Granger", Inconnu,Inconnu)),
# MrouMme ("Desire", "Leduc",
# MrouMme ("Georges", "Leduc",
# MrouMme ("Robert", "Leduc",Inconnu,Inconnu),
# MrouMme ("Raymonde", "Leblanc",Inconnu,Inconnu)),
# MrouMme ("Berthe", "Trognon",Inconnu,Inconnu)));;
La manipulation d’un objet d’un type récursif (constructeur, accès, prédicats) ne fait appel
qu’aux notions de types sommes et produits.
L’écriture d’algorithmes sera basée sur la structure inductive du type : on a vu (cf. 2.4.1) que
pour définir une fonction récursive, il fallait la définir pour des cas de base, et dans le cas général
en fonction de valeurs de la fonction pour des cas plus simples.
Avec un type récursif, les cas de base sont les constructeurs ne comportant pas d’argument
récursif (dans l’exemple suivant : V, F, Prop) ; le cas général consiste à définir la valeur de la fonction
pour une construction récursive, en fonction de sa valeur pour les arguments de la construction.
L’exemple suivant permet de manipuler des formules logiques du genre : “P et (non Q ou vrai)”
en les représentant par un type récursif logique. La définition peut se lire :
3.2. TYPES CONSTRUITS 27
3.2.2 En Pascal
Enregistrements : Les enregistrements en Pascal se déclarent avec le mot-clé record. L’accès
aux champs se fait avec la notation pointée habituelle. L’instruction with permet de mettre en
facteur le nom de variable pour accéder à ses champs, uniquement par leur nom.
La norme Pascal-Iso ne permet pas l’utilisation de type enregistrement en résultat d’une fonc-
tion ; cela est possible dans des versions de Pascal plus riches que cette norme.
program rationnel ;
type rationnel = record
num, den : integer
end;
var x, y, somme : rationnel;
function lire_rationnel : rationnel ;
var x : rationnel;
begin
writeln("fraction ?");
readln(x.num, x.den);
lire_rationnel := x
end;
function plus_rationnel (a,b : rationnel): rationnel;
var z : rationnel;
begin
z.num := a.num * b.den + a.den * b.num ;
z.den := a.den * b.den;
plus_rationnel := z
end;
begin
x := lire_rationnel;
y := lire_rationnel;
28 CHAPITRE 3. TYPES ET STRUCTURES DE DONNÉES
program conversion ;
type
nom_unite = (SI, GB);
distance = record
case unite : nom_unite of
SI : (km, metre : integer);
GB : (mille : real)
end;
var d_lue : distance ;
dkm : real;
function lire_distance : distance ;
var x : distance; reponse : char ;
begin
writeln("Unite SI : o/n ?");
readln(reponse);
if reponse = ’o’
then begin x.unite := SI;
readln (x.km, x.metre)
end
else begin x.unite := GB;
readln (x.mille)
end;
lire_distance := x
end;
function EnKm (d : distance): real;
var x : real;
begin
case d.unite of
SI : x := d.km + d.metre / 1000;
GB : x := d.mille * 1.8519 ;
end;
EnKm := x
end;
begin
d_lue := lire_distance;
dkm := EnKm (d_lue);
writeln ("Distance = ", dkm:10:3, "Km")
end.
Notons au passage, l’utilisation d’un type énuméré : nom_unite, défini par énumération de ses
valeurs possibles, et pour lequel on peut faire affectations et comparaisons.
Les types récursifs ne sont pas directement autorisés en Pascal ; il faut pour le faire utiliser la
notion de pointeur (qui est hors programme).
3.3. LISTES 29
3.3 Listes
3.3.1 Généralités
La plupart des algorithmes vus jusqu’ici manipulent des types de données de taille limitée et
fixe (un entier, un enregistrement ...). Or dans le champ d’applications de l’informatique, beaucoup
de problèmes font appel à de grandes quantités d’informations ; c’est d’ailleurs dans ce cas qu’une
résolution informatique est la plus nécessaire.
Parmi les types étudiés, seuls les types récursifs permettent de modéliser des quantités d’infor-
mation non bornées ; par exemple, avec le type personne, on peut saisir un arbre généalogique, qui
sur 10 générations regroupera déja de l’ordre d’un millier de personnes.
On va maintenant étudier la modélisation de grandes quantités de données, dont on dispose
sous la forme d’une suite de données élémentaires de même type.
Cette situation est très courante : par exemple les fichiers d’individus (sécurité sociale ...) sont
des suites d’enregistrements contenant des informations sur chaque individu, les séries statistiques
sont des suites de valeurs à étudier...
Pour manipuler ce genre de données dans un programme, une technique élémentaire consiste à
utiliser la notion de vecteur (cf. Informatique 1). L’inconvénient majeur est qu’il faut connaı̂tre le
nombre de données, ou fixer une borne à ce nombre au moment de la conception du programme.
Définition : On appelle liste une suite ordonnée d’informations de même type, et élément
chacune de ces informations. Une liste peut contenir plusieurs fois le même élément.
On parlera du type liste, qui regroupe toutes les listes d’éléments. Ce type dépend du type
d’élément considéré pour une liste particulière.
On peut cependant définir un ensemble de fonctions sur les listes générales au type liste. On
notera element le type d’un élément.
On a besoin de pouvoir construire une liste, d’y accéder, et de calculer des propriétés.
Constructeurs : Pour construire une suite ordonnée d’éléments, il suffit de disposer d’une fonc-
tion de construction d’une liste sans élément, et d’une fonction de construction d’une liste composée
d’un élément et d’une liste :
fonction liste_vide : -> liste d’element
fonction ajout : element*liste d’element -> liste d’element
Fonctions d’accès : Pour accéder à tous les éléments d’une liste, il suffit de pouvoir accéder au
premier élément, et à la liste restante (le deuxième n’est que le premier du reste ...etc).
fonction premier : liste d’element -> element
fonction reste : liste d’element -> liste d’element
Prédicat : On a besoin de savoir si une liste est vide ou non, pour ne faire que des appels
cohérents aux fonctions précédentes (premier et reste d’une liste vide n’ont aucun sens)
fonction est_vide : liste d’element -> booleen
Exemple d’utilisation : Dans une application de statistiques, on dispose d’une liste d’entiers
dont on veut calculer, la moyenne, les valeurs maximales, le mode...
fonction maximum : liste d’entier -> entier
l -> si est_vide (reste (l)) alors premier (l)
sinon max (premier (l), maximum (reste (l)))
où max est supposé défini avec le type :
30 CHAPITRE 3. TYPES ET STRUCTURES DE DONNÉES
Remarques : La programmation avec des listes permet de manipuler des suites de données de
taille non bornée à priori. La mémoire de la machine impose cependant une limite à cette taille,
qui si elle est dépassée provoquera une erreur à l’exécution.
Le choix entre l’utilisation de vecteurs ou de listes dépend surtout du type de traitement que
l’on veut effectuer sur les données.
L’utilisation de listes privilégie l’accès aux éléments dans l’ordre où ils sont (même s’il est facile
d’écrire une fonction donnant le nieme élément), sans imposer de contraintes sur la taille.
L’utilisation de vecteurs favorise l’accès à un élément par son indice, mais impose une définition
préalable de la taille. Les vecteurs sont particulièrement adaptés au calcul vectoriel et matriciel
(ils ont d’ailleurs été introduits pour cela, dans les langages de programmation).
Notons que la notion de vecteur (présente en Pascal et en Caml ainsi que dans la plupart des
langages) s’étend simplement avec la notion de tableau (ou matrice), que l’on définit comme un
vecteur de vecteurs.
3.3.2 En Caml
Les constructeurs de listes se notent :
– [] pour la liste vide
– e::l pour l’ajout de e à l
– [e1 ; e2 ; ... en] pour la construction d’une liste à n éléments.
L’accès aux listes en Caml peut se faire dans deux styles différents :
– En utilisant les fonctions d’accès hd pour premier et tl pour reste
– En utilisant le filtrage avec la liste vide [], ou avec une liste composée d’un élément e et
d’une liste l noté e::l
Les exemples des fonctions du 3.3.1 peuvent se coder en Caml, avec les fonctions d’accès :
#let rec maximum =
#function l -> if tl(l) = [] then hd(l)
# else max (hd(l), maximum (tl(l)));;
maximum : ’a list -> ’a = <fun>
#let rec ajout_alafin =
#function l,e -> if l = [] then e ::l
# else hd(l):: ajout_alafin(tl(l),e);;
ajout_alafin : ’a list * ’a -> ’a list = <fun>
ou de façon équivalente avec filtrage :
#let rec maximum =
#function e::[] -> e
# | e::l -> max (e, maximum (l));;
Entrée interactive:
>function e::[] -> e
> | e::l -> max (e, maximum (l))..
Attention: ce filtrage n’est pas exhaustif.
3.3. LISTES 31
Listes et types récursifs : On a introduit les listes sans faire référence aux types récursifs vus
en section 3.2 (page 23).
Les raisons en sont l’importance des listes en tant que telles en informatique, le fait que les
listes soient présentes dans des langages où la notion de type récursif n’existe pas, et le traitement
particulier réservé aux listes dans les langages (dont Caml).
Cependant, les listes ne sont que le cas particulier d’un type récursif ne comportant qu’une
seule référence à lui même.
Par exemple un polynôme de Z[X], est soit le polynôme nul, soit un polynôme construit par
a + X * P où P est un polynôme et a appartient à Z (schéma de Hörner). On peut le définir en
Caml (comme on a défini le type personne, page 26) :
#type polynome = Nul | aplusXfois of int * polynome;;
Le type polynome est défini.
#let P = aplusXfois (3, aplusXfois (2, aplusXfois (1, Nul)));;
P : polynome = aplusXfois (3, aplusXfois (2, aplusXfois (1, Nul)))
Ce type est isomorphe au type int list (i.e. tout élément d’un des types peut être représenté
dans l’autre de façon unique).
3.3.3 En Pascal
Le type liste n’existe pas en Pascal mais peut être défini, pour un type particulier de liste,
de la façon suivante (ceci n’est pas au programme mais est donné à titre d’illustration ; pour une
explication de la notion de pointeur utilisée ici, on consultera un ouvrage de référence sur Pascal) :
program listes ;
(* Definition du type liste et des fonctions *)
type Liste = ^ Element ;
Element = record valeur : integer; reste : Liste; end;
begin
writeln ("Suite d’entiers terminee par 0");
l := liste_vide;
readln (n);
while n <> 0 do
begin l := ajout (n, l);
readln(n)
end;
lemax := maximum (l);
writeln ("Le maximum est :", lemax)
end.
On remarquera, qu’après quelques efforts pour implémenter ce type, on peut l’utiliser simple-
ment : l’écriture de la fonction maximum est très proche de l’algorithme donné en 3.3.1.
Représentation : Un arbre peut être représenté (dans l’exemple des expressions arithmétiques
les noeuds sont les opérations, et les feuilles les entiers) :
– graphiquement (c’est le plus naturel, mais pas le plus simple à saisir pour un ordinateur)
– en notation infixée, par exemple : (12 + 3) * (7 - 2)
3.4. UN EXEMPLE : LE TYPE ARBRE 33
Utilisation : L’usage des arbres est très répandu, pour la structuration d’informations. Citons
par exemple : la classification botanique, une table des matières, les coups possibles aux échecs, un
arbre généalogique... et dans le domaine informatique : les expressions arithmétiques, logiques, et
plus généralement tout programme écrit dans un langage structuré.
L’intérêt de la formalisation informatique des arbres réside dans l’ensemble d’algorithmes que
l’on peut définir sur eux de manière générale, et qui seront autant de traitements disponibles pour
chaque arbre en particulier. On peut par exemple rechercher l’ensemble des descendants, compter
les noeuds, savoir si un noeud est ancêtre d’un autre ...
En Caml : Sur le type personne, qui est un arbre binaire, on définit la fonction
nb_personnes_connues
qui compte le nombre de noeuds de l’arbre généalogique. On pourrait définir cette fonction de
manière similaire pour n’importe quel arbre binaire.
#let rec nb_personnes_connues = function
# MrouMme (nom,prenom,pere,mere) ->
# 1 + nb_personnes_connues(pere)
# + nb_personnes_connues(mere) |
# Inconnu -> 0 ;;
nb_personnes_connues : personne -> int = <fun>
#nb_personnes_connues (ADupont);;
- : int = 9
34 CHAPITRE 3. TYPES ET STRUCTURES DE DONNÉES
Chapitre 4
Ordre Supérieur
Définitions : On appelle fonction d’ordre supérieur, une fonction dont le type comporte plus
d’une fois le symbole fonctionnel ->. Les fonctions d’ordre supérieur peuvent avoir des fonctions
en paramètres, donner des fonctions comme résultat, ou les deux à la fois.
Par ailleurs, rappelons qu’on dit qu’une fonction est polymorphe, si elle s’applique à plusieurs
types différents de données.
Exemples : L’opérateur = est une fonction polymorphe, car il permet de tester l’égalité d’entiers,
de réels, de caractères ...
La fonction de tri vue précédemment est polymorphe car le type d’élément n’est pas précisé :
on peut donc avoir des listes d’entiers, des listes de chaı̂nes de caractères...
35
36 CHAPITRE 4. ORDRE SUPÉRIEUR
Ordre supérieur et polymorphisme : L’ordre supérieur est un moyen efficace pour écrire des
fonctions polymorphes. Face à un problème, pour lequel on dispose d’un algorithme général, et
pour lequel seuls quelques traitements dépendent du type des données, on doit avoir la démarche
suivante : écrire une fonction d’ordre supérieur pour l’algorithme général, en mettant en paramètres
des fonctions pour les traitements particuliers.
Par exemple, le calcul d’un pgcd dans un anneau euclidien par l’algorithme d’Euclide, peut être
décrit par une fonction d’ordre supérieur, ayant en paramètres les opérations dépendant du type
des données (division sur Z ou Q[X]...).
Fonction en résultat : L’intérêt méthodologique d’une fonction qui a une fonction comme
résultat, est d’écrire une “fonction abstraite”, qui appliquée à des données, va permettre de générer
des fonctions. L’objectif est de n’écrire qu’une fonction, et d’en générer plusieurs. Ceci est possible
quand, par abstraction, on constate que plusieurs fonctions “se ressemblent” et ne sont en fait que
des instances particulières d’une fonction plus générale.
Soit à écrire les fonctions racine carrée, racine cubique, ... racine nieme. On pourrait bien
sûr écrire une fonction de deux arguments, mais on préfère, pour l’utilisateur pouvoir les fabriquer
toutes. Au lieu de les écrire, on écrit une fonction dépendant d’un entier n, et donnant une fonction
calculant la racine nieme d’un nombre.
De manière analogue, on définira l’addition dans Z/nZ comme une fonction qui à un entier
donné a associe la fonction d’addition dans Z/aZ.
Schémas de fonctions : On remarque que l’écriture de fonctions, pour un type donné, présente
souvent beaucoup de similarités. Par exemple, pour les listes, nombres de problèmes se résolvent
par un algorithme suivant le schéma :
f : l -> si vide (l) alors ...
sinon ... premier(l) ...f(reste(l))...
Une fonction d’ordre supérieur permet d’écrire ce schéma, en remplaçant les pointillés par des
fonctions en paramètres.
4.2.1 En Caml
Fonction en résultat : L’ordre supérieur est tellement intrinsèque au langage Caml, qu’il est
utilisé jusque dans la réalisation des fonctions du langage, comme par exemple pour la fonction
power.
#power;;
- : float -> float -> float = <fun>
#let puissancede2 = power (2.0);;
4.2. PROGRAMMATION D’ORDRE SUPÉRIEUR 37
Application partielle : le langage Caml offre un mécanisme permettant d’écrire des “fonctions
paramétriques” : en mathématiques on passe souvent, par exemple, d’une fonction de 2 variables
f (x, y) à une fonction gx (y) d’une variable y, paramétrée par x, telle que gx (y) = f (x, y) ; g est
alors une fonction qui à x associe gx , gx étant une fonction qui à y associe f(x,y).
C’est le cas de nombreuses fonctions de Caml dont la fonction power.
La fonction power associe à un float, une fonction float->float. Pour l’utiliser il faut donc
l’appliquer d’abord à un float, puis appliquer ceci à un float. Le type de power doit être compris
comme float ->(float -> float).
On peut écrire une fonction racine nieme, dont l’utilisation sera analogue à celle de la fonction
power :
#let racineN = function n ->
# function x -> power(x) (1./. float_of_int (n));;
racineN : int -> float -> float = <fun>
#let racine3 = racineN (3);;
racine3 : float -> float = <fun>
#racine3 (100.);;
- : float = 4.64158883361
Le type de racineN doit être lu : int -> (float -> float).
Le résultat de son application à l’entier 3 est une fonction de type float -> float, dont la
définition est : function x -> power(x) (1./. float_of_int (3))
La simplicité de l’écriture vient du fait qu’une fonction est considérée en Caml comme un cas
particulier d’expression, ce qui permet d’écrire une fonction n’importe où dans un programme, où
l’on peut écrire une expression (ici, à l’endoit où l’on doit écrire le résultat de la fonction).
L’exemple suivant, est aussi une fonction d’ordre supérieur, donnant une fonction en résultat.
Ce résultat peut être désigné par un nom ou immédiatement appliqué à des données :
#let rec plusZnZ = function n ->
# function (x, y) -> let p = x + y in
# if p < 0 then plusZnZ (n)(x+n, y)
# else if p < n then p
# else plusZnZ (n)(x-n,y);;
plusZnZ : int -> int * int -> int = <fun>
#let plusZ5Z = plusZnZ (5);;
plusZ5Z : int * int -> int = <fun>
#plusZ5Z (4,3);;
- : int = 2
#plusZnZ (3) (2,2);;
- : int = 1
Le type de la fonction plusZnZ doit être lu : int->((int * int)->int). La dernière application
doit être lue : (plusZnZ (3)) (2,2).
Dans les types, l’opérateur * est plus prioritaire que ->, et le parenthèsage par défaut est à
droite. Par contre, le parenthèsage par défaut est à gauche dans les applications.
38 CHAPITRE 4. ORDRE SUPÉRIEUR
Une fonction f : type1->type2->type3 est appliquée par : f(x)(y) où x est de type type1
et y de type type2.
En reprenant le type polynome, défini en 3.3.2, on définit la fonction qui à un polynôme associe
la fonction polynôme correspondante.
#let rec horner = function (Nul,x) -> 0 |
# (aplusXfois(a,p),x) -> a + x * horner(p,x);;
horner : polynome * int -> int = <fun>
#let fonction_polynome = function p ->
# function x -> horner(p,x);;
fonction_polynome : polynome -> int -> int = <fun>
Schémas de fonctions : Le schéma, dit de généralisation, suivant permet d’écrire toutes les
fonctions d’une liste [e1;e2...en], dont le résultat est de la forme :
e1 op e2 op ... op en op e, où op est un opérateur binaire associatif, et e l’élément neutre de
cet opérateur.
Il permet, entre autres, de généraliser + en Σ, ∗ en Π ...
La fonction generaliser prend une fonction en paramètre et donne une fonction en résultat.
#let rec generaliser = function (opbinaire, neutre) ->
# function [] -> neutre |
# a::l -> opbinaire (a,
4.2. PROGRAMMATION D’ORDRE SUPÉRIEUR 39
# generaliser(opbinaire,neutre)(l));;
generaliser : (’a * ’b -> ’b) * ’b -> ’a list -> ’b = <fun>
#let sigma = generaliser ((function x,y -> x + y), 0);;
sigma : int list -> int = <fun>
#sigma ([ 12; 45; 30; 22]);;
- : int = 109
#let maximum = generaliser
# ((function x,y -> if x<y then y else x), -1073741824);;
maximum : int list -> int = <fun>
#maximum([ 12; 45; 30; 22]);;
- : int = 45
On notera dans le type calculé pour la fonction rond, que les trois variables de type ’a, ’b et
’c notent respectivement le type d’entrée de la première fonction (le même que le type de sortie
de la seconde), son type de sortie (qui est aussi celui de la composition) et le type d’entrée de la
deuxième fonction (qui est aussi celui de la composition).
L’exemple suivant calcule une valeur numérique approchée de la dérivée d’une fonction sur les
réels (on suppose que la dérivée existe) :
#let derive = function f -> function x ->
# let dx = 0.00001 in
# ( f(x +. dx) -. f(x -. dx) ) /. (2.0 *. dx);;
derive : (float -> float) -> float -> float = <fun>
#let inv = derive (log);;
inv : float -> float = <fun>
#inv (3.);;
- : float = 0.333333333336
4.2.2 En Pascal
L’utilisation de l’ordre supérieur est très limité en Pascal. Les fonctions ne sont pas considérées
comme des valeurs comme les autres, ce qui empêche d’avoir des fonctions en résultat de fonction.
La seule possibilité d’ordre supérieur consiste à passer des fonctions ou des procédures en
paramètres de fonctions ou de procédures. Il faut pour cela spécifier complètement le profil de la
fonction paramètre, dans la liste des paramètres formels.
Par exemple, on peut définir un algorithme de tri selon le schéma suivant :
40 CHAPITRE 4. ORDRE SUPÉRIEUR
program tri;
type vectentier = array[1..100]of integer;
var vec : vectentier;
...
function inferieur (a,b:integer): boolean;
...
function superieur (a,b:integer): boolean;
...
procedure tri_vecteur (var v : vectentier ;
function comp (x,y:integer): boolean );
begin
...
if comp (v[1], v[2]) then
...
end;
begin
...
(* tri croissant *)
tri (vec, inferieur);
...
(* tri decroissant *)
tri (vec, superieur);
...
end.
Cette solution est limitée par le typage explicite des variables et fonctions passées en pa-
ramètres : cela ne permet pas d’écrire un algorithme de tri générique, opérant aussi bien sur des
vecteurs d’entiers que de réels ou de chaı̂nes (possible seulement avec certaines extensions de la
norme Pascal).
Chapitre 5
Preuves de programmes
Les objectifs de ce chapitre sont de donner des outils théoriques : la logique et l’induction, puis
de les utiliser pour faire des raisonnements sur des programmes.
Définition : L’ensemble des propositions est défini par les règles suivantes :
– V , F sont des propositions, respectivement vraie et fausse,
– les symboles P , Q... sont des propositions, dites propositions atomiques,
– si a, b sont des propositions, alors a et b, a ou b, non a sont des propositions respectivement
vraie ssi a est vraie et b est vraie, fausse ssi a est fausse et b est fausse, vraie ssi a est fausse,
– toutes les propositions sont générées par les trois règles précédentes.
Connecteurs : et, ou, non sont appelés des connecteurs. On peut définir de nouveaux connec-
teurs :
a ⇒ b par : (non a) ou b
a ⇔ b par : (a et b) ou (non a et non b).
Dans l’écriture d’une formule complexe, on pourra ne pas mettre toutes les parenthèses, en
considérant l’ordre croissant de priorité entre les connecteurs : ⇔, ⇒, ou, et, non.
Table de vérité : On peut représenter la valeur de vérité d’une proposition, dépendant des
valeurs des propositions atomiques qu’elle contient, par une table de vérité :
41
42 CHAPITRE 5. PREUVES DE PROGRAMMES
Définitions : Une proposition est dite une tautologie, si elle a toujours la valeur de vérité V .
Les tautologies de la forme a ⇔ b sont particulièrement intéressantes, car elles montrent que
les propositions a et b ont mêmes valeurs de vérité.
Algèbre de Boole
L’ensemble des propositions muni de V , F et des opérations et, ou, non est une algèbre de
Boole.
Ce point de vue est intéressant car il donne les propriétés algébriques suivantes (dont on aurait
pu montrer que ce sont des tautologies en construisant les tables de vérité), où a, b, c sont des
propositions :
Neutre V et a ⇔ a
F ou a ⇔ a
Absorbant F et a ⇔ F
V ou a ⇔ V
Commutativité a et b ⇔ b et a
a ou b ⇔ b ou a
Associativité a et(b et c) ⇔ (a et b)et c
a ou(b ou c) ⇔ (a ou b) ou c
Distributivité a et(b ou c) ⇔ (a et b)ou(a et c)
a ou(b et c) ⇔ (a ou b) et (a ou c)
Idempotence a et a ⇔ a
a ou a ⇔ a
Absorption a et(a ou b) ⇔ a
a ou(a et b) ⇔ a
Complément a et non a ⇔ F
a ou non a ⇔ V
non(non a) ⇔ a
De Morgan non(a et b) ⇔ non a ou non b
non(a ou b) ⇔ non a et non b
Formes canoniques
Toute proposition peut s’écrire sous forme normale disjonctive (respectivement conjonctive),
c’est à dire sous la forme a1 ou...ou an (resp. a1 et...et an ), où les ai sont de la forme b1 et...et bp
(resp. b1 ou...ou bp ), et où les bi sont des propositions atomiques ou des négations de propositions
atomiques.
Pour mettre une proposition sous forme normale disjonctive (resp. conjonctive), il faut :
– Eliminer les connecteurs ⇔, ⇒, s’il y en a (en les remplaçant par leur définition),
– “Déplacer” les négations vers les propositions atomiques (en utilisant les lois de De Morgan),
– Mettre sous la forme ou(et...) (respectivement et(ou...)), en utilisant la distributivité.
Exemple en caml : La définition récursive des propositions suggère la définition d’un type
récursif, pour les représenter. Le type logique présenté en 3.2.1 convient parfaitement :
5.2. INDUCTION 43
#texte (formule);;
- : string = "non (non P ou non (non Q ou non F))"
#texte (Distribuer (NierAtomes (formule)));;
- : string = "((P et non Q) ou (P et V))"
5.2 Induction
On a cité en 2.4.1 le principe de récurrence sur N. On utilise des définitions par récurrence pour
écrire des fonctions récursives sur N. Le principe de récurrence est l’outil théorique permettant de
raisonner sur des fonctions récursives sur N : on le verra à la section suivante.
44 CHAPITRE 5. PREUVES DE PROGRAMMES
Cependant, l’utilisation de la récursivité ne se limite pas au seul domaine des entiers naturels.
La chapitre 3 a montré de nombreux exemples de fonctions récursives opérant sur des types de
données eux-mêmes définis récursivement. On a alors vu en 3.2.1 que l’écriture de telles fonctions
reposait sur la structure inductive du type. Il y a une analogie entre “définir une fonction en 0, et
en fonction de sa valeur en n-1” et “définir une fonction pour les constructeurs constants, et en
fonction de ses valeurs pour les arguments d’une construction”.
On aimerait poursuivre l’analogie en généralisant le principe de récurrence sur N. C’est ce qui
est fait par le principe d’induction structurelle, qui permet de raisonner sur des ensembles définis
par induction.
Objectifs : Raisonner sur des programmes ne doit pas être considéré comme une activité
théorique, déconnectée de la pratique. Le raisonnement doit “fonder” la pratique de la program-
mation. On peut distinguer les situations suivantes, où le raisonnement sur les programmes est
utile :
– Prouver qu’un programme calcule bien le résultat voulu ; il faut pour cela que le résultat
voulu soit spécifié de manière non ambigüe (souvent en langage mathématique),
– Prouver qu’un programme se termine,
– Trouver une relation de récurrence, pour concevoir une fonction récursive,
– Prouver que deux programmes sont équivalents, i.e. ils calculent le même résultat (problème
réputé difficile),
– Transformer un programme, en un autre programme équivalent, mais plus efficace (par
exemple en évitant de calculer plusieurs fois la même chose).
Etude de cas
On va étudier la division euclidienne sur Z. Connaissant a ∈ Z et b ∈ Z ∗ , on cherche q et r tels
que : a = bq + r et 0 ≤ r < |b|.
Cette spécification décrit bien, de façon unique, la solution recherchée. Pour la calculer, il
nous faut exhiber une relation de récurrence. L’idée est d’essayer de se ramener au cas simple où
46 CHAPITRE 5. PREUVES DE PROGRAMMES
Remarque : La preuve d’un algorithme récursif comporte généralement ces deux aspects :
– preuve par récurrence que tous les appels vérifient une certaine propriété, que l’on appellera
“propriété invariante”, (dans l’exemple a = bq + r, 0 ≤ r < |b|) ; cette preuve permet de dire
que l’algorithme calcule le bon résultat s’il se termine.
– preuve que l’exécution se termine parce qu’une propriété dite “propriété variante” vient à
être vérifiée (dans l’exemple 0 ≤ a < |b|). Cette preuve peut être faite, comme dans cet
exemple, en exhibant une suite entière positive décroissante.
Reprenons l’exemple des polynômes sur Z[X], pour prouver la fonction horner.
#type polynome = Nul | aplusXfois of int * polynome;;
#let rec horner = function (Nul,x) -> 0 |
# (aplusXfois(a,p),x) -> a + x * horner(p,x);;
– Pour prouver que horner(p,x) donne le bon résultat si l’exécution se termine, il suffit de
prouver que :
– horner(Nul,x) = 0
– si horner(p,x) calcule p(x) alors horner(aplusXfois(a,p),x) calcule (a + x ∗ p)(x)
– Pour prouver que l’exécution de horner(p,x) se termine, il suffit de prouver que :
– le calcul de horner(Nul,x) se termine,
– si le calcul de horner(p,x) se termine, alors le calcul de
horner(aplusXfois(a,p),x) se termine.
Toutes ces preuves sont triviales.
Les principes énoncés aux chapitres 2 et 3, pour bien programmer récursivement (penser aux
cas de base...), sont en fait les conditions nécessaires et suffisantes de la preuve.
Transformations de programmes
Les définitions suivantes de la fonction longueurpaire sont équivalentes. On passe d’une solu-
tion à la suivante par une simple transformation, en gardant le même sens.
On remarquera la distance qui sépare la solution initiale de la dernière solution.
#let rec longueurpaire = function l ->
# if l = [] then true else
# if tl(l) = [] then false
# else longueurpaire(tl (tl (l)));;
Conclusion
Ce cours, ainsi que celui du module Informatique 1, s’est concentré sur la conception d’algo-
rithmes et la programmation, qui sont au coeur de la discipline informatique.
Deux styles d’expression des algorithmes, impératif (itératif) et fonctionnel (récursif), ont été
abordés. Ils sont complémentaires et peuvent donc être choisis selon la nature du problème. Le
choix des langages de programmation étudiés est justifié pédagogiquement par leur adéquation
chacun à un style d’expression particulier (impératif en Pascal, fonctionnel en Caml). On peut ce-
pendant faire de la programmation fonctionnelle en Pascal (ce qui a été vu) et de la programmation
impérative en Caml (voir [WL99]), ou utiliser d’autres langages (Lisp, Scheme, C...). L’essentiel
est d’avoir une démarche scientifique d’analyse du problème, de savoir abstraire les données (par
création de types) et les traitements (par des fonctions d’ordre supérieur), pour obtenir un algo-
rithme correct et ensuite le traduire dans un langage particulier.
Un des objectifs majeurs de ce cours était d’introduire la nécessité de raisonner pour program-
mer. Cet objectif sera poursuivi dans le module Informatique 3, par l’étude de la complexité des
algorithmes, la preuve de programmes impératifs, l’équivalence entre récursion et itération.
Au travers d’exemples, on a ouvert des portes vers certains domaines de l’informatique, qui
tous utilisent la conception d’algorithmes :
– les quelques exemples mathématiques n’ont permis que d’entrevoir les difficultés du calcul
numérique et du calcul formel ; c’est le domaine de l’algorithmique mathématique ;
– les manipulations d’expressions arithmétiques et logiques (sous forme d’arbres) ne consti-
tuent qu’un aperçu de ce que peut faire un compilateur pour représenter et transformer un
programme ; c’est le domaine de l’étude des langages et de la compilation ;
– le calcul des propositions n’est que le premier maillon vers la démonstration automatique,
et ce qui est communément appelé “intelligence artificielle” (on devrait plutôt parler de
simulation de raisonnement).
Ce panorama ne prétend pas être exhaustif. Il est destiné seulement à appréhender l’informa-
tique en tant que discipline, et en particulier à introduire quelques domaines abordés en 2ème
année et en second cycle informatique.
49
50 CHAPITRE 6. CONCLUSION
Annexe A
Conventions lexicales.
– Les commentaires s’écrivent : (* ceci est un commentaire *)
– Les identificateurs : ident : := lettre {lettre | 0...9 | }
lettre : := A...Z | a...z
– Les entiers : [-] {0...9}+
– Les nombre décimaux : [-] {0...9}+ [. {0...9}] [(e | E) [+ | -] {0...9}+]
– Les caractères s’écrivent entre ‘, par exemple : ‘a‘
– Les chaı̂nes de caractères s’écrivent entre ”, par exemple : ”bonjour”
– Les mots clés sont : and, as, begin, do, done, downto, else, end, exception, for, fun, function,
if, in, let, match, mutable, not, of, or, prefix, rec, then, to, try, type, value, where, while,
with.
– Sont considérés commme blancs : espace, saut de ligne, tabulation, retour chariot, saut de
page.
51
52 ANNEXE A. SYNTAXE SIMPLIFIÉE DU LANGAGE CAML
Définitions de types. La forme d’une phrase de définition de type est définie par :
où nom-de-type est soit un type prédéfini (int, float, char, string, bool) soit un type déjà
défini par l’utilisateur.
57