Vous êtes sur la page 1sur 57

Notes de cours

Module Informatique 2
Deug Sciences mention MIAS
-
Spécification et construction d’algorithmes :
Approche fonctionnelle 1
-

Alain Héaulmé, Christophe Mauras, René Thoraval


Université de Nantes, Faculté des Sciences, Département d’Informatique

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

3 Types et structures de données 21


3.1 Abstraire les données . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
3.2 Types construits . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
3.2.1 En Caml . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
3.2.2 En Pascal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
3.3 Listes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
3.3.1 Généralités . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
3.3.2 En Caml . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
3.3.3 En Pascal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
3.4 Un exemple : le type arbre . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32

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

A Syntaxe simplifiée du langage Caml 51


Chapitre 1

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

2.1 Expressions, Valeurs, Types


2.1.1 Définitions
L’analyse d’un problème à résoudre en informatique amène à mettre en évidence les informations
dont on dispose, et les informations que l’on souhaite calculer.
Ces informations peuvent être de différente nature : par exemple des nombres réels représentant
des mesures physiques, des nombres entiers, rationnels (au sens mathématique : N, Z, Q), un mot
ou une phrase saisi par l’utilisateur, une suite de rationnels décrivant les coefficients d’un polynôme,
des heures, des dates... ou toute combinaison plus complexe par exemple une suite, un ensemble,
un couple d’informations d’une des formes ci-dessus.

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.

Définition : On appelera type un ensemble de valeurs.


Un ensemble de valeurs a un intérêt particulier dès lors qu’il permet d’identifier l’ensemble des
valeurs auxquelles on peut appliquer une certaine opération. Par exemple, le fait que + soit une
loi de composition interne sur Z, contribue à l’intérêt du type des entiers relatifs.
On utilisera donc comme types, des ensembles de valeurs munis d’opérations permettant de
calculer le résultat attendu (une valeur), à partir des données dont on dispose (des valeurs).
Les types (dits de base), que l’on retrouve dans quasiment tous les langages informatiques, et
que l’on utilisera donc dans l’écriture d’algorithmes sont les suivants :
Le type entier qui regroupe en fait les entiers relatifs. On utilise peu les entiers naturels, car les
opérations fournies dans la plupart des langages s’appliquent sur les entiers relatifs (ne pas

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.

Exemple : L’expression suivante calcule le signe du produit x * y.


selon ((si x > 0 alors 1
sinon si x = 0 alors 0
sinon -1) * (suivant y > 0 : 1
y = 0 : 0
y < 0 : -1))
dans 1 : ’+’
0 : ’0’
-1 : ’-’
2.1. EXPRESSIONS, VALEURS, TYPES 9

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.

2.2 Fonctions, composition, application


2.2.1 Définitions
Les expressions sont les briques de base qui permettent d’écrire la valeur d’un résultat en
utilisant des opérateurs et les valeurs données d’un problème. Pour décrire de façon générale,
l’obtention d’un résultat dépendant de données inconnues, on a recours à la notion de fonction.
Cela va permettre d’écrire des expressions dépendant d’inconnues que l’on appelera paramètres de
la fonction.

Définition : Une fonction en informatique est définie par la donnée de :


– son nom
– le N-uplet de ses paramètres (noms arbitraires que l’on donne aux données inconnues)
– le produit cartésien des types des paramètres (le domaine ou ensemble de départ),
– le produit cartésien des types des sorties (le co-domaine ou ensemble d’arrivée),
– un P-uplet d’expressions calculant les valeurs des sorties en fonction des paramètres.

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

Composition : La composition de fonctions est l’application d’une fonction au résultat d’une


application d’une autre fonction. Pour qu’une composition soit correcte, il faut vérifier que les
types des sorties de la première correspondent aux types des entrées de la seconde. Cette notion
fondamentale en mathématiques est aussi fort utile en informatique car elle permet de décrire des
calculs complexes en utilisant au mieux les fonctions déjà définies.
Exemple : Si on dispose d’une fonction racine : reel -> reel alors on peut écrire la fonction
norme :
fonction norme : reel x reel -> reel
u1, u2 -> racine (produit_scalaire(u1,u2,u1,u2))

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

Autre exemple, définissant quelques fonctions élémentaires ; regardons d’abord comment la


fonction exp (prédéfinie) est typée :
#exp;;
- : float -> float = <fun>
#let ch = function x -> ( exp (x) +. exp ( -. x ) ) /. 2.0 ;;
ch : float -> float = <fun>
#let sh = function x -> ( exp (x) -. exp ( -. x ) ) /. 2.0 ;;
sh : float -> float = <fun>
#ch (1.0);;
- : float = 1.54308063482
#sh (1.0);;
- : float = 1.17520119364

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.

Remarque : On notera ici la différence de style de programmation, entre la solution donnée en


Caml, et celle-ci.
L’utilisation d’un langage fonctionnel (comme Caml), associé à une interaction (top-level) per-
met de se concentrer sur la définition d’une fonction qui calcule des valeurs en fonction d’autres
valeurs.
En Pascal, on doit en plus décrire complètement l’interaction, comme ici dans le programme
principal avec readln et writeln. On doit aussi se soucier de déclarer des variables pour stocker les
données lues, les résultats à afficher, et éventuellement des valeurs intermédiaires.
2.3. MÉTHODOLOGIE 13

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 *)

Algorithme : On va utiliser l’arithmétique modulo 60 pour calculer la minute de fin, et


l’arithmétique modulo 24, pour calculer l’heure et savoir si on passe au lendemain. On va effectuer
un calcul intermédiaire pour savoir de combien d’heures, le temps avance.
14 CHAPITRE 2. CONCEPTS DE BASE

Fin_du_film : entier*entier*entier -> entier*entier*booleen


hdebut , mndebut, duree ->
soit hplus = quotient ((mndebut + duree) , 60)
dans( (hdebut + hplus) modulo 24,
(mndebut + duree) modulo 60,
(hdebut + hplus) >= 24 )

Programme : On choisit de coder cet exemple en Caml.


#let fin_du_film = function
# hdebut , mndebut, duree ->
# let hplus = (mndebut + duree) / 60
# in ( (hdebut + hplus) mod 24,
# (mndebut + duree) mod 60,
# (hdebut + hplus)/ 24 = 1 );;
fin_du_film : int * int * int -> int * int * bool = <fun>

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

2.3.2 Analyse Descendante


L’écriture d’algorithmes est souvent une tâche complexe, selon le nombre d’informations
différentes à traiter, et la complexité du problème lui-même.

Définition : Le principe de l’analyse descendante consiste à examiner globalement le problème,


et à essayer de le décomposer en sous-problèmes indépendants. On se contente alors de spécifier
les sous-problèmes, pour pouvoir écrire l’algorithme général, qui va utiliser les solutions des sous-
problèmes. Il reste ensuite à réaliser un algorithme pour chacun des sous-problèmes (cela peut
aussi être décomposé selon le même principe).
La décomposition peut se faire de deux façons complémentaires :
Décomposition par cas : Selon les données, on constate que le traitement à effectuer est
différent, et que les solutions pour chacun des cas sont indépendantes. On décomposera
alors le problème en autant de sous-problèmes qu’il y a de cas.
Décomposition structurelle : On met en évidence des résultats intermédiaires qui semblent
plus faciles à calculer en fonction des données, et qui serviront à calculer le résultat final. Les
sous-problèmes consistent alors à calculer chacun de ces résultats intermédiaires (en fonction
de données et d’autres résultats intermédiaires) et à calculer les résultats finaux (eux aussi
en fonction de données et de résultats intermédiaires).

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")

L’analyse de la fonction de conversion inch_to_m_cm est laissée en exercice.

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 :

fonction fact : entier -> entier


n -> si n = 0 alors 1
sinon n * fact (n-1)

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.

fonction distance : entier * entier -> entier


(a,b) -> si a >= b alors a - b
sinon distance (b,a)
2.4. RÉCURSIVITÉ 17

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

#quotient (25, 3);;


- : int = 8
La récursivité croisée est possible en définissant simultanément les deux fonctions, avec le mot-
clé and :
#let rec f = function x -> if x = 1 then 0 else g(x)
#and g = function y -> f (y / 2) + 1 ;;
f : int -> int = <fun>
g : int -> int = <fun>
#f (148);;
- : int = 7

2.4.3 En Pascal
En Pascal, les fonctions peuvent être récursives, sans déclaration particulière.

Exemple : Le calcul de la factorielle spécifié en 2.4.1.


program factorielle ;
var n, f: integer;
function fact (n : integer):integer ;
begin
if n = 0 then fact := 1
else fact := n * fact (n -1)
end;
begin
readln(n);
f := fact(n);
writeln (’fact (’, n, ’) = ’, f)
end.
Pour les récursions croisées, le problème (dû au compilateur Pascal) est que l’on ne peut utiliser
une fonction avant de l’avoir déclarée. Il est alors impossible de déclarer à la fois f avant g et g
avant f.
La solution est de déclarer d’abord les profils des fonctions (c’est-à-dire les types des pa-
ramètres d’entrée et du résultat), puis d’écrire leur corps. Ceci est d’ailleurs intéressant dans le
cadre d’une analyse descendante où l’on écrit d’abord la spécification d’une fonction (son profil),
puis sa réalisation (le corps).

Exemple : Que calcule ce programme ?


program croise ;
var n, res: integer;
function f (n : integer):integer ; forward ;
function g (n : integer):integer ; forward ;

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

Types et structures de données

3.1 Abstraire les données


Dans le chapitre 2, on s’est intéressé à l’expression, au traitement et à la production d’infor-
mations (valeurs) de nature très simple : entiers, réels, caractères, booléens, chaı̂nes.
On a vu que les langages informatiques mettent à disposition immédiate du programmeur un
certain nombre de types de base permettant de manipuler directement de telles informations.
On a défini en 2.1.1 la notion de type comme un ensemble de valeurs muni d’opérations.
Mais le programmeur doit également pouvoir manipuler des informations de nature plus com-
plexe : nombres rationnels, complexes, polynômes, dictionnaires, etc.
Il doit donc pouvoir se doter de types plus élaborés permettant de prendre en compte la com-
plexité de ces informations.
L’objectif est de transposer ce qui a été fait sur les types de base, à des types plus complexes.
Examinons tout d’abord un type de base qui permet de modéliser des informations relativement
complexes : le type chaı̂ne de caractères.
Le programmeur dispose d’un certain nombre de fonctionnalités que l’on peut classifier ainsi :
Constructeur : Ecrire une constante "Bonjour" dans un programme construit implicitement une
représentation de cette chaı̂ne.
Sélecteur : On peut accéder au nieme caractère d’une chaı̂ne, ce que l’on pourra noter :
nieme_caractere ( "Bonjour", 4) et qui vaut : ’j’
Opérateur : On dispose d’une opération de concaténation de deux chaı̂nes, qui renvoit une chaı̂ne
(Exemple "Bonjour " & "Monsieur" vaut : "Bonjour Monsieur").
Prédicat : On peut comparer deux chaı̂nes par une fonction d’égalité.
De telles fonctionnalités permettent au programmeur de manipuler des chaı̂nes de caractères
dans un algorithme, en ignorant tout de la manière dont les chaı̂nes sont représentées dans l’ordi-
nateur. Il en va de même pour les entiers, les réels, etc.
On souhaiterait qu’il en soit ainsi pour les informations appartenant à des types plus élaborés.
Pour cela, il faut pour chaque nouveau type, disposer d’un ensemble suffisant de fonctions mani-
pulant des informations de ce type.

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 *)

fonction rationnel : entier*entier -> rationnel


(* construit un rationnel a partir de son numerateur
et de son denominateur *)

fonction rationnel_entier : entier -> rationnel


(* construit un rationnel egal a l’entier fourni *)

fonction numerateur : rationnel -> entier


(* le numerateur du rationnel *)

fonction denominateur : rationnel -> entier


(* le denominateur du rationnel *)

fonction representation_externe : rationnel -> chaine


(* donne un rationnel sous la forme " 1/2 " *)

fonction plus_rat : rationnel * rationnel -> rationnel


(* operateur d’addition sur les rationnels *)

fonction egal_rat : rationnel * rationnel -> booleen


(* predicat d’egalite entre rationnels *)
Le programmeur qui va utiliser ce type n’a pas à se préoccuper de la réalisation, par exemple
il peut écrire :
representation_externe
(plus_rat (rationnel (3,8), rationnel (2,3)))
Il peut aussi définir de nouvelles fonctions ainsi :
fonction oppose_rat : rationnel -> rationnel
r -> rationnel( - numerateur(r),denominateur(r))

fonction moins_rat : rationnel * rationnel -> rationnel


r1, r2 -> si egal_rat (r1, r2) alors rationnel_entier (0)
sinon plus_rat (r1, oppose_rat(r2) )

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

en utilisant les trois fonctionnalités de base retenues . . . et sans se préoccuper de la réalisation du


type rationnel.

3.2 Types construits


On s’attache maintenant à décrire comment une valeur d’un type est représentée à partir de
valeurs de types de base.

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

“Une formule logique peut être V, F, le constructeur Et appliqué à 2 formules logiques, le


constructeur Non appliqué à une formule logique ... ou le constructeur Prop qui comporte une
chaı̂ne de caractères”
#type logique = V | F | Et of logique*logique | Non of logique
# | Ou of logique * logique | Prop of string ;;
Le type logique est défini.
Pour donner une représentation textuelle d’une formule, on écrit une fonction récursive texte,
qui calcule la chaine de caractères qui représente chacune des contructions, en fonction de la
représentation de ses arguments.
#let rec texte = function
# Et (x,y) -> "(" ^ (texte (x)) ^ " et " ^ (texte (y)) ^ ")" |
# Ou (x,y) -> "(" ^ (texte (x)) ^ " ou " ^ (texte (y)) ^ ")" |
# Non (x) -> "non " ^ (texte (x)) |
# Prop x -> x |
# V -> "V" |
# F -> "F" ;;
texte : logique -> string = <fun>
#let formule = Et (Non(Prop "P"), Non (Et (Prop "Q", F)));;
formule : logique = Et (Non (Prop "P"), Non (Et (Prop "Q", F)))
#texte (formule);;
- : string = "(non P et non (Q et F))"

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

somme := plus_rationnel (x, y);


with somme do
writeln ("somme = ", num, "/", den)
end.

Enregistrements à champs variant : Ces enregistrements permettent de définir des types


sommes. Il faut déclarer un champ particulier, appelé sélecteur, qui permet ensuite de distinguer
différentes structures d’enregistrements.
On reprend un exemple du paragraphe précédent, en Pascal :

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

fonction max : entier * entier -> entier


La fonction suivante ajoute un élément à la fin d’une liste. Notons que cette fonction s’applique
à une liste d’éléments de type quelconque :
fonction ajout_alafin :
liste d’elements * element -> liste d’elements
l,e -> si est_vide (l) alors ajout (e,l)
sinon ajout (premier(l), ajout_alafin(reste(l),e))

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

maximum : ’a list -> ’a = <fun>


#let rec ajout_alafin =
#function [], e -> e :: []
# | p::l, e -> p :: ajout_alafin(l,e);;
ajout_alafin : ’a list * ’a -> ’a list = <fun>
#let x = [3; 5; 9; 2];;
x : int list = [3; 5; 9; 2]
#maximum (x);;
- : int = 9
#ajout_alafin (x, 1);;
- : int list = [3; 5; 9; 2; 1]
Examinons le type de la fonction ajout_alafin : le type ’a list note une liste
d’éléments dont le type est quelconque ; ’a est appelée une variable de type. On peut utiliser
ajout_alafin avec n’importe quelle liste. Le type se précise uniquement au moment d’un appel :
ajout_alafin (x, 1) est de type int list. On dit qu’une telle fonction est polymorphe, car
elle s’applique à plusieurs types différents de données.

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;

var l:liste; n, lemax :integer;

function liste_vide : Liste; begin liste_vide := NIL end;


32 CHAPITRE 3. TYPES ET STRUCTURES DE DONNÉES

function ajout (e: integer; l: Liste):Liste; var x : Liste;


begin new(x); x^.valeur := e; x^.reste := l; ajout := x end;

function premier (l:Liste): integer; begin premier := l^.valeur end;

function reste (l:Liste): Liste; begin reste := l^.reste end;

function est_vide (l:Liste): boolean; begin est_vide := l = NIL end;

function max (a,b:integer):integer;


begin if a < b then max := b else max := a end;

(* programmation avec des listes *)

function maximum (l:Liste):integer;


begin
if est_vide (reste(l) )
then maximum := premier (l)
else maximum := max (premier(l),maximum(reste(l)));
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.

3.4 Un exemple : le type arbre


Les exemples de types récursifs (personne et logique) vus en 3.2.1 présentent des similarités
qui justifient une présentation plus générale.
Tous deux peuvent être représentés graphiquement par un arbre constitué d’une racine (la
personne ou l’expression), d’où partent des branches vers des noeuds (les parents, ou les sous-
expressions), qui sont eux mêmes des arbres, ces ramifications se terminant par des feuilles (la
personne inconnue ou les constantes).

Définitions : Plus généralement, on appellera arbre un ensemble d’éléments appelés noeuds


et un ensemble d’éléments appelés feuilles, structurés selon la règle suivante : un arbre peut être
réduit à une feuille, ou être un noeud et contenir un certain nombre d’arbres.
Un arbre binaire est un arbre dont tous les noeuds contiennent deux arbres.

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

– en notation préfixée, par exemple : * (+ 12 3) (- 7 2)


– en notation postfixée, par exemple : 12 3 + 7 2 - * (c’est la notation polonaise inverse)

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

4.1 Abstraire par les fonctions


On se pose le problème suivant : écrire une fonction qui trie une liste d’éléments, quel que soit
leur type. La seule connaissance dont on a besoin sur les éléments est une relation d’ordre total, qui
permet de les comparer deux à deux. En examinant les algorithmes usuels de tri, on constate que
ce qui caractérise chaque algorithme, c’est la méthode employée pour comparer tous les éléments
entre eux, en les comparant deux à deux.
Prenons l’exemple du tri par insertion, la méthode (récursive) consiste à remarquer qu’une liste
vide a tous ses éléments triés, et qu’une liste à n éléments peut être triée en distinguant un de ses
éléments, puis en l’insérant à la bonne place, dans la liste triée des n-1 autres éléments. L’insertion
nécessite de comparer l’élément à insérer aux autres.
Sachant que ceci caractérise, de façon intrinsèque, le tri par insertion, on souhaite pouvoir écrire
l’algorithme en faisant abstraction de la fonction de comparaison à utiliser pour un type particulier
d’éléments. L’objectif étant de n’écrire qu’une fonction de tri, utilisable pour tout type d’élément.
On peut exprimer cela, en spécifiant que le tri est une fonction, qui a besoin pour s’exécuter
d’une liste d’éléments à trier et d’une fonction de comparaison entre éléments, et qui donne une liste
(triée) d’éléments. En considérant la fonction de comparaison comme un paramètre supplémentaire,
on évite d’écrire autant de fonctions de tri qu’il y a de types d’éléments. On a ainsi généralisé
l’algorithme de tri, en faisant abstraction de la fonction de comparaison.
L’algorithme de tri sera de la forme :

tri : liste d’elements * (element*element -> booleen)


-> liste d’elements
l, inf -> ... si inf (... , ...) alors ...

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.

Fonctions de fonctions : Les mathématiques, et en particulier l’analyse fonctionnelle, offrent


beaucoup de problèmes dans lesquels on manipule et on transforme explicitement des fonctions.
Par exemple la fonction ◦ : f, g → f ◦ g, la fonction dérivée f → f 0 . Les fonctions d’ordre
supérieur sont la formalisation naturelle, en informatique, des fonctions dans des espaces de fonc-
tions.

4.2 Programmation d’ordre supérieur


On a examiné des situations de résolution de problème, où une bonne méthodologie consiste à
abstraire des fonctions. Il nous reste maintenant à étudier les mécanismes des langages permettant
de mettre en oeuvre les fonctions d’ordre supérieur.

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

puissancede2 : float -> float = <fun>


#puissancede2 (10.0);;
- : float = 1024.0

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>

Fonctions en paramètres : On veut calculer une série Σbi=a ui , connaissant a, b et le terme


général u, qui est une fonction int -> float. On utilise la fonction série pour calculer une valeur
approchée de e = Σ∞ 1
i=0 i !

#let rec serie = function (u, a, b) ->


# if a = b then u (a)
# else u (a) +. serie (u, a+1, b);;
serie : (int -> float) * int * int -> float = <fun>
#serie ((function n -> 1./. float_of_int (fact (n))), 0, 12);;
- : float = 2.71828182829
L’exemple du tri par insertion prend une fonction en paramètre qui est transmise à la fonction
insertion. L’algorithme respecte les spécifications données au paragraphe précédent.
#let rec insertion = function
# (inf, x, []) -> [x] |
# (inf, x, e::r) -> if inf (x,e) then x::e::r
# else e::(insertion (inf,x,r));;
insertion : (’a * ’a -> bool) * ’a * ’a list -> ’a list = <fun>
#let rec tri = function
# (inf, []) -> [] |
# (inf, e::l) -> insertion (inf, e, tri(inf, l));;
tri : (’a * ’a -> bool) * ’a list -> ’a list = <fun>
#tri ((function (x,y)-> x<y) , [4; 9; 2; 0; 13; 1]);;
- : int list = [0; 1; 2; 4; 9; 13]
#tri ((function (a,b)-> int_of_char(a)<int_of_char(b)),
# [‘d‘; ‘p‘; ‘a‘; ‘8‘; ‘A‘; ‘C‘]);;
- : char list = [‘8‘; ‘A‘; ‘C‘; ‘a‘; ‘d‘; ‘p‘]

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

Fonctions de fonctions : La composition de fonctions peut être définie par :


#let rond = function (f,g) ->
# function x -> f(g(x));;
rond : (’a -> ’b) * (’c -> ’a) -> ’c -> ’b = <fun>
#let racine = rond (sqrt, float_of_int);;
racine : int -> float = <fun>
#racine (10);;
- : float = 3.16227766017

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.

5.1 Eléments de Logique


L’objet de la logique est d’étudier des énoncés, leurs preuves, en considérant uniquement leur
forme et en faisant abstraction de leur sens. Son intérêt en informatique est de permettre de
“mécaniser” le raisonnement sur les programmes.
On va étudier une logique très simple, permettant de modéliser les expressions booléennes.

Calcul des propositions


On appelle proposition un énoncé qui peut être soit vrai, soit faux.
Par exemple, “3 < 0”, “la factorielle de 4 est 24”, “Socrate est un homme” sont des propositions.
Pour faire abstraction du sens de ces propositions, on les notera par des symboles : P , Q...
Ce qui nous intéresse, c’est la manière de combiner de telles propositions, pour faire des rai-
sonnements.

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

P Q P et Q P ou Q non P P⇒Q P⇔Q


F F F F V V V
F V F V V V F
V F F V F F F
V V V V F V V

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

#type logique = V | F | Et of logique*logique | Non of logique


# | Ou of logique * logique | Prop of string ;;
La fonction NierAtomes déplace les négations. On reconnaı̂tra dans le texte les lois de De
Morgan.
#let rec NierAtomes = function
# Non (Et (x,y)) -> NierAtomes (Ou (Non x, Non y)) |
# Non (Ou (x,y)) -> NierAtomes (Et (Non x, Non y)) |
# Non (Non x) -> NierAtomes x |
# Non V -> F |
# Non F -> V |
# Non (Prop x) -> Non (Prop x) |
# Et (x, y) -> Et (NierAtomes(x), NierAtomes(y)) |
# Ou (x, y) -> Ou (NierAtomes(x), NierAtomes(y)) |
# V -> V |
# F -> F |
# Prop x -> Prop x;;
NierAtomes : logique -> logique = <fun>

La fonction Distribuer met sous la forme ou(et...) :


#let rec Distribuer = function
# Et(Ou(x,y), z) -> Ou (Distribuer(Et(x,z)), Distribuer(Et(y,z))) |
# Et(z, Ou(x,y)) -> Ou (Distribuer(Et(z,x)), Distribuer(Et(z,y))) |
# Et(x,y) -> if x = Distribuer(x) & y = Distribuer(y) then Et(x,y)
# else Distribuer(Et(Distribuer(x),Distribuer(y))) |
# x -> x ;;
Distribuer : logique -> logique = <fun>

Voici un exemple de mise sous forme normale disjonctive :


#let formule = Non (Ou (Non(Prop "P"),
# Non (Ou (Non (Prop "Q"), Non(F)))));;

#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))"

On pourrait aussi programmer les simplifications de formules contenant V , F (à faire en


exercice).
Une fonction très intéressante est la fonction qui indique si une formule est une tautologie. Il est
possible d’écrire cette fonction, car on dispose d’une méthode (les tables de vérité) pour le savoir
(à noter que cette méthode est très inefficace dès que le nombre de symboles est grand) .
On a ainsi montré que le calcul des propositions justifie des transformations et des preuves
d’équivalence sur les expressions booléennes. On peut utiliser cela, manuellement pour raisonner
sur un programme, ou bien construire des procédures automatiques.

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.

Ensembles définis par induction


L’ensemble Z[X] des polynômes (cf. 3.3.2) est défini par :
– le polynôme nul, noté Nul est un polynôme,
– si a appartient à Z et P est un polynôme, alors a + X * P est un polynôme,
– Z[X] ne contient que des objets obtenus en appliquant un nombre fini de fois les règles
précédentes.
De même, on pourrait définir l’ensemble P, des entiers naturels pairs, par :
– 0 appartient à P,
– si n appartient à P, alors n+2 appartient à P,
– P ne contient que des objets obtenus en appliquant un nombre fini de fois les règles
précédentes.
Ces ensembles ont été définis par induction. De façon générale, définir un ensemble E par
induction consiste à donner un schéma comportant :
– un ensemble d’éléments de base appartenant à E,
– des règles d’induction permettant de décrire de nouveaux éléments de E à partir d’éléments
de E,
– une règle de fermeture (que l’on pourra omettre par la suite car c’est toujours la même) : E ne
contient que des objets obtenus en appliquant un nombre fini de fois les règles précédentes.
Les types récursifs sont des ensembles définis par induction : les éléments de base sont
définis par les constructeurs sans argument récursif ; les règles d’induction sont définies par les
constructeurs comportant (au moins) un argument récursif.

Remarque : N est un ensemble défini par induction (axiomes de Peano) :


– 0 appartient à N,
– si n appartient à N, alors succ(n) appartient à N, où succ(n) représente n+1
– N ne contient que des objets obtenus en appliquant un nombre fini de fois les règles
précédentes.

Principe d’induction structurelle


Soit E un ensemble défini par induction. Pour montrer que tous les éléments de E vérifient une
propriété P, il suffit de montrer que :
– les éléments de base de E vérifient P,
– pour toute règle d’induction f , si e1 , ...en ∈ E vérifient P, alors l’élément f (e1 , ...en ) vérifie
P.
Le principe de récurrence sur N n’est que le cas particulier du principe d’induction structurelle
appliqué à N défini selon les axiomes de Peano. On le rappelle ici.
Pour montrer une propriété P pour tout entier n ∈ N , il suffit de montrer que :
– P (0)
– pour tout n, P (n) ⇒ P (n + 1)
5.3. PREUVES ET TRANSFORMATIONS DE PROGRAMMES 45

La formulation suivante est équivalente :


Pour montrer une propriété P pour tout entier n ∈ N , il suffit de montrer que :
pour tout n, si pour tout k < n on a P (k) alors on a P (n).

5.3 Preuves et transformations de programmes


Les théories présentées au début de ce chapitre vont permettre des raisonnements sur les pro-
grammes.

Exemple de raisonnement utilisant la logique


La logique permet de justifier les calculs sur des expressions booléennes. On considère les
expressions de comparaison comme des propositions atomiques, pour raisonner sur des formules
logiques. Par exemple, on veut simplifier :
(x >= 4) ou ((x < 4) et (y = x + 2))
On l’écrit sous la forme :
P ou (non P et Q) avec P: x >= 4, Q: y = x + 2
Or on peut montrer que : P ou(non P et Q) est équivalent à : P ou Q. L’expression initiale
peut donc se simplifier en :
(x >= 4) ou (y = x + 2)

Exemple de raisonnement par récurrence


Le principe de récurrence va permettre de raisonner sur des fonctions récursives.
Soit f une fonction récursive sur les entiers naturels, dont on veut prouver que le résultat est
juste. Si on prouve que le résultat de f(0) est juste, et que si le résultat de f(n-1) est juste alors
il en va de même pour f(n), alors le résultat de f sera prouvé juste pour tout n.
On peut aussi vouloir prouver que le calcul de f(n) se termine ; l’hypothèse de récurrence sera
alors “le calcul de f(0) se termine”, la phase d’induction sera : “si le calcul de f(n-1) se termine
alors le calcul de f(n) se termine”.

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

0 ≤ a < |b|, on a alors la solution q = 0, r = a qui vérifie : a = bq + r. Pour se ramener à ce cas, on


va choisir un nouveau couple a0 , b0 (selon les signes de a et de b on prendra a0 = a + b ou a0 = a − b
et on prendra b0 = b) dont la division donne q 0 , r0 et calculer q, r en fonction de q 0 , r0 .
On propose ainsi le programme suivant, que l’on cherchera ensuite à prouver :
#let rec division = function (a,b) ->
# if (0<=a) & (a<abs(b)) then (0,a)
# else if a * b > 0
# then let (qp,rp) = division (a-b,b)
# in (1 + qp, rp)
# else let (qp,rp) = division(a+b, b)
# in (-1 + qp, rp) ;;
division : int * int -> int * int = <fun>

Preuve de correction si arrêt : Pour prouver ce programme par récurrence, on va raisonner


sur le nombre n d’appels récursifs engendrés par l’appel division(a,b).
On va prouver que si l’exécution se termine (après n appels récursifs), alors le résultat est
correct.
Pour n = 0, l’exécution se termine sans appel récursif si et seulement si 0 ≤ a < |b|. On a alors
q = 0 et r = a, qui vérifient bien a = bq + r et 0 ≤ r < |b|.
Supposons que toute exécution engendrant n − 1 appels récursifs donne un résultat correct,
montrons-le au rang n. Soit l’appel division(a,b) qui génère n appels récursifs ; on a alors a < 0
ou a ≥ |b|. Prenons le cas a ∗ b > 0, l’hypothèse au rang n − 1 nous donne : a − b = qp ∗ b + rp
et 0 ≤ rp < |b|. On en déduit a = (qp + 1) ∗ b + rp. Le résultat donné : q = 1 + qp, r = rp vérifie
donc a = bq + r et 0 ≤ r < |b|. Dans le cas a ∗ b ≤ 0, l’hypothèse au rang n − 1 nous donne :
a + b = qp ∗ b + rp et 0 ≤ rp < |b|. On en déduit a = (qp − 1) ∗ b + rp. Le résultat donné :
q = −1 + qp, r = rp vérifie donc a = bq + r et 0 ≤ r < |b|. CQFD.

Preuve d’arrêt : Montrons maintenant que tout appel division(a,b) pour a ∈ Z et b ∈ Z ∗


se termine. Pour cela on va associer à la suite des appels récursifs, une suite d’entiers positifs
strictement décroissante, qui est donc nécessairement finie.
Si a ≥ |b|, on prend la suite des valeurs de a pour les appels successifs. La valeur suivante est
a − b si b > 0 ; on a 0 ≤ a − b < a. La valeur suivante est a + b si b < 0 ; on a alors 0 ≤ a + b < a.
On a donc bien, dans les deux cas, une suite positive strictement décroissante.
Si a < 0, on prend la suite des valeurs de −a + |b| pour les appels successifs. La valeur suivante
est −(a − b) + |b| si b < 0 ; on a 0 ≤ −(a − b) + |b| < −a + |b|, car −(a − b) + |b| = −a. La valeur
suivante est −(a + b) + |b| si b > 0 ; on a alors 0 ≤ −(a + b) + |b| < −a + |b|, car −(a + b) + |b| = −a.
On a bien là aussi, dans les deux cas, une suite positive strictement décroissante, ce qui termine la
démonstration.

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.

Preuves et types récursifs


Pour les types récursifs, qui sont des ensembles définis par induction, les preuves de programmes
peuvent souvent se faire par induction structurelle.
5.3. PREUVES ET TRANSFORMATIONS DE PROGRAMMES 47

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)));;

#let rec longueurpaire = function l ->


# if l = [] then true else
# (function tll -> if tll = [] then false
# else longueurpaire(tl (tll)))(tl(l));;

#let rec longueurpaire = function l ->


# if l = [] then true else
# longueurimpaire (tl(l))
#and longueurimpaire = function l -> if l = [] then false
# else longueurpaire (tl (l));;

#let rec longueurpaire = function l ->


# l = [] or longueurimpaire (tl(l))
#and longueurimpaire = function l ->
# not (l = []) & longueurpaire (tl (l));;

On peut par transformations successives, de l’un en l’autre, prouver l’équivalence de deux


programmes. La preuve de l’équivalence de deux programmes dans le cas général (quand on ne sait
pas transformer l’un en l’autre) ne sera pas abordée dans ce cours.
48 CHAPITRE 5. PREUVES DE PROGRAMMES
Chapitre 6

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

Syntaxe simplifiée du langage


Caml

On donne seulement la syntaxe du sous-ensemble du langage Caml considéré dans le module


(pour la syntaxe complète, voir [WL93]).

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

Expressions, filtrage, liaison. Leurs formes sont définies par :

expression ::= identificateur variable


| constante constante (y compris
constructeur 0-aire)
| ( expression )
| expression , expression { , expression } n-uplet
| constructeur expression valeur construite
| expression : : expression liste avec premier et reste
| [ expression { ; expression } ] liste non vide
| { étiquette = expression { ; étiquette = expression } } enregistrement
| expression expression application de fonction
| opérateur-préfixe expression opération unaire
| expression opérateur-infixe expression opération binaire
| expression . étiquette accès à un champ
| if expression then expression else expression alternative
| match expression with filtrage choix selon des motifs
| function filtrage fonction
| let [ rec ] liaison { and liaison } in expression expression avec définition
locale

filtrage ::= motif → expression { | motif → expression } liste de cas

liaison ::= motif = expression

Les opérations suivantes sont classées par ordre de priorité décroissante :


53

Opération Associativité Comportement


application droite
construction –
− −. – Opposé entier et décimal (opérateurs préfixes)
mod gauche Modulo entier
∗ ∗ . / /. gauche Multiplication et division, entières et décimales
+ + . − −. gauche Addition et soustraction, entières et décimales
:: droite Construction de liste
@ droite Concaténation de listes
ˆ droite Concaténation de chaı̂nes
= <> < <= > >= gauche Comparaisons
not – non logique
& gauche et logique
or gauche ou logique
, – N-uplets
if – Alternative
let match function –
54 ANNEXE A. SYNTAXE SIMPLIFIÉE DU LANGAGE CAML

Motifs. Leur forme est définie par :

motif ::= identificateur variable


| constante constante (y compris
constructeur 0-aire)
| n’importe quoi
| ( motif )
| motif | motif l’un ou l’autre
| constructeur motif filtre une valeur construite
| motif : : motif liste avec premier et reste
| [ motif { ; motif } ] liste non vide
| [] liste vide
| motif , motif { , motif } n-uplet
| { étiquette = motif { ; étiquette = motif } } enregistrement

Définitions de types. La forme d’une phrase de définition de type est définie par :

phrase-type ::= type def-type { and def-type } phrase de définition

def-type ::= identificateur = def-constr { | def-constr } somme


| identificateur = { def-champ { ; def-champ } } produit (enregistrement)

def-constr ::= identificateur constructeur 0-aire


| identificateur of expr-type constructeur n-aire

def-champ ::= identificateur : expr-type champ (enregistrement)

expr-type ::= nom-de-type voir ci-après


| ( expr-type )
| expr-type { * expr-type }+ produit cartésien de types
| expr-type list liste d’un type

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.

Phrases. La forme d’une phrase est définie par :

phrase ::= expression


| phrase-type définition de type
55

| let [ rec ] liaison { and liaison } définition globale


56 ANNEXE A. SYNTAXE SIMPLIFIÉE DU LANGAGE CAML
Bibliographie

[AS89] H. Abelson et G. Sussman. Structure et interprétation des programmes informatiques.


InterEditions, 1989.
[HHDGV92] T. Haccart-Hardin et V. Donzeau-Gouge-Viguié. Concepts et outils de programma-
tion. InterEditions, 1992.
[SFLM93] P.C. Scholl, M.C. Fauvet, F. Lagnier et F. Maraninchi. Cours d’informatique : lan-
gages et programmation. Masson, 1993.
[WL93] P. Weiss et X. Leroy. Manuel de référence du langage Caml. InterEditions, 1993.
[WL99] P. Weiss et X. Leroy. Le langage Caml. InterEditions, 1999.
[Xuo92] N. H. Xuong. Mathématiques discrètes et informatique. Masson, 1992.

57

Vous aimerez peut-être aussi