Initiation à la Programmation Fonctionnelle

publicité
Faculté des Sciences
de Nice Sophia-Antipolis
L2 Maths 2007-08
Semestre 3
Initiation à la
Programmation Fonctionnelle
(Séances 1 à 7)
Jean-Paul Roy
Département Informatique
http://deptinfo.unice.fr/~roy
Faculté des Sciences de Nice
L2 Maths 2007-08
Initiation à la programmation fonctionnelle
Feuille n° 0 : Préambule...
0.1 SCHEME se prononce « skime »
L’an dernier, à travers le langage C, vous avez étudié la programmation impérative. Cette dénomination
provient de la conception d’un programme comme une suite ordonnée {I1; ... In;} d’instructions exigeant
impérativement de l’ordinateur qu’il procède à telle ou telle action, par exemple modifier la valeur d’une
variable par l’instruction d’affectation x = x+y, ou bien afficher la valeur de la variable x à l’écran, ou bien
« itérer » une séquence d’instructions via une boucle, etc. Cette conception de la programmation comme
« pilotage » de l’ordinateur auquel on donne des instructions est présente dès le début de l’informatique, et peut
paraître au premier abord assez naturelle : on pilote bien des avions ou des machines à laver ! Mais ce n’est pas
la seule conception, et il existe en programmation comme dans la plupart des autres disciplines scientifiques
différentes manières de voir le monde et d’agir sur lui. La programmation fonctionnelle propose un autre style,
plus proche de la pensée mathématique pure donc à priori bien adaptée à la section de DEUG que vous avez
choisie. Pour le matheux, le concept d’instruction n’existe pas, celui d’affectation encore moins : avez-vous déjà
vu une démonstration où des objets mathématiques changeraient de valeur entre le début de la preuve et le
CQFD ? Et que dire d’une boucle dans une démonstration ?…
Que va-t-on donc lui emprunter, à ce mathématicien, pour fabriquer des programmes ? Essentiellement deux
concepts fondamentaux :
• les fonctions E1 × L × En → F et la possibilité de les composer, la suite d’instructions étant remplacée par
la composition de fonctions.
• le principe de récurrence, dont l’itération [while ou for en C] n’est en effet qu’un cas particulier !
Vous voyez que l’idée €
consiste à revenir aux racines des mathématiques et d’épurer au maximum la
programmation de ce qui n’est pas nécessaire. On vous demandera dans ce cours exactement la même rigueur
[le même purisme] de pensée qu’en algèbre, ni plus ni moins. Ce qui ne veut pas dire qu’en SCHEME o n
manipule uniquement des objets mathématiques, mais que les schémas de pensée y sont au fond les mêmes
qu’en maths. En particulier, on peut y raisonner sur un programme, comme on raisonne par exemple sur une
suite définie par récurrence ou sur un système d’équations... Ce cours ne suppose aucune connaissance préalable
de la programmation, C ou autre. Ne raisonnez pas avec des schémas C dans la tête, ce sera une toute autre
méthodologie. Ayez un esprit de débutant, d’ailleurs l’esprit de débutant c’est très zen...
Le propos de la formation à l’informatique en section MP est qu’à travers les langages C et SCHEME, vous
aurez en fait eu une introduction [ne soyons pas trop ambitieux quand même] aux deux paradigmes majeurs de
la programmation [impératif et fonctionnel1], et qu’indépendamment des langages eux-mêmes [qui n’ont que
peu d’importance au final] vous serez à même d’avoir une vision un peu large de cette activité scientifique que
les anglo-saxons qualifient de « computation », de calcul au sens large du terme. Programmer, c’est calculer
sur des objets que les mathématiciens et les physiciens, en attendant les transistors, n’ont pu qu’ignorer pendant
des siècles. Ils ont là une belle revanche à prendre, et ils le savent2. Leurs systèmes de calcul formel
[MATHEMATICA, MAPLE] utilisés à la fois pour l’enseignement et la recherche, forment un savant mélange de
programmation impérative et fonctionnelle.
0.2 Sommaire des feuilles de TP
1. Expressions préfixées et λ-expressions
2. Ordre applicatif et formes spéciales
3. Fonctions récursives dans N
4. Qu’est-ce qu’une boucle ?
5. Fonctions d’ordre supérieur
6. Doublets et données hiérarchiques
1
7. La structure récursive de liste
8. Des algorithmes sur les listes
9. Comment trier une liste
10+… Thème d’approfondissement
autour de la structure d’arbre.
Le troisième paradigme, vital de nos jours, est la » programmation par objets », exemplifiée par Java, qui est à peu près « C + objets ».
L’option Scheme du semestre 4 contiendra une introduction à ces fameux « objets », entre autres.
2
Il n’est que de considérer des ouvrages comme le « Cours d’Algèbre » de M. Demazure, professé à l’Ecole Polytechnique [Cassini éd,
1997], ou les travaux actuels sur le calcul quantique [« quantum computing »] que le physicien R. Feynman avait déjà pressenti dès 1981…
0.3 Bibliographie à la B.U.
« Structure et Interprétation des Programmes Informatiques » [SICP], Abelson & Sussman, InterEditions.
Superbe rédaction d’un cours professé annuellement au MIT. Lire absolument l’avant-propos et la préface.
La 1ère édition en français [à la BU] est actuellement épuisée en librairie ; la 2ème édition en anglais [Structure
and Interpretation of Computer Programs] est consultable gratuitement sur www-mitpress.mit.edu/sicp.
« Recueil de Petits Problèmes en Scheme » [RPPS], Moreau & al ., Springer-Verlag. 150 pages d’énoncés et
150 pages de solutions, pour tous les goûts et tous les niveaux : LICENCE, classes prépas…
« Programmer avec SCHEME » [PS], Chazarain, Thomson Publishing. Un cours professé durant de nombreuses
années en L3 d’Informatique à la fac. des sciences de Nice.
etc. Une bibliographie plus complète sur les deux sites Web ci-dessous.
0.4 Et sur le « Web » ?
On pourra consulter le site local associé à ce cours http://deptinfo.unice.fr/~roy qui contient lui-même de
nombreux liens. Plusieurs universités ou Grandes Ecoles françaises et étrangères proposent des formations à
SCHEME et offrent leurs TP corrigés et autres sujets d’examens aux surfeurs gourmands et motivés …
0.5 Installation du logiciel DRSCHEME
Ce logiciel universitaire, qui en est à sa version 371, est produit par l'équipe PLT [« Programming Language
Team », http://www.plt-scheme.org]. Il est disponible gratuitement pour WINDOWS, MACOS-X, et LINUX
avec 512 Mo de mémoire. Les ordinateurs compatibles PC du MIPS fonctionnent sous Windows-2000.
Attention, quelques aménagements ont été faits sur la version que vous utiliserez ici, notamment des réglages et
des fichiers spéciaux, dont l’aide en ligne en français. Vous trouverez sur le site
http://deptinfo.unice.fr/~roy une rubrique Install DrScheme dans la colonne de gauche, avec des
téléchargements à la clef. Suivez pas à pas les indications, il y a quelques modifications à effectuer par-rapport à
la version standard !
0.6 Méthode de travail
SCHEME est un langage simple et spartiate, c’est un atout et un danger. Dépourvu de fioritures inutiles, faisant
très peu appel à la mémoire, il exige en contrepartie une parfaite compréhension des mécanismes de base et une
parfaite rigueur intellectuelle au niveau des stratégies de résolution de problèmes. Vous êtes [supposés] être des
matheux, alors prouvez-le et ça devrait aller de ce côté-ci, il vous suffira de remplacer la « démonstration par
récurrence » par la « programmation par récurrence »…
Enfin, ça ira si en plus vous travaillez ! Car ne vous bercez pas d’illusions, on n’apprend pas à programmer
en SCHEME 15 jours avant l’examen, le résultat est en général assez ca-tas-tro-phi-que. En revanche, nous vous
assurons qu’un travail régulier, dans les livres et sur machine, et une préparation des TP ne devrait vous causer
aucune suprise lors du dit examen.
Vous ne devez pas arriver en TP sans avoir préparé au moins une partie du TP. Ces feuilles étant des feuilles
d’auto-formation, et en l’absence de cours magistral, l’enseignant est là pour apprécier ou critiquer vos solutions,
ou vous donner des explications sur vos erreurs, et en aucun cas vous débiter des solutions toutes faites. Tous les
exercices non optionnels auront été supposés compris et programmés le jour de l’examen de janvier [2h en
amphi, sans documents]. Bon travail et
(bienvenue (dans le monde des parenthèses))
La présence en TP est obligatoire et contrôlée, et comptera dans la note finale [–0.5 point pour chaque
absence non justifiée].
VENEZ TOUJOURS EN TP AVEC UNE « CLE USB » !
Faculté des Sciences de Nice
L2 Maths 2007-08
Initiation à la programmation fonctionnelle
Feuille n° 1 : Consultation avec DRSCHEME...
1.1 Une syntaxe étrange venue d’ailleurs
Et de loin, des premières années de l’informatique, avec le langage LISP : 1958, rien de bien récent donc, SCHEME
étant lui né vers 1980. Cet ancêtre LISP fut conçu à l’origine pour les besoins des théoriciens de la programmation
[le « λ-calcul »], mais aussi et surtout pour des programmeurs qui souhaitaient un langage suffisamment malléable
pour les besoins symboliques de l’Intelligence Artificielle naissante [1956]. La syntaxe des deux langages est donc
très proche, basée sur l’idée [qui se justifiera peu à peu] de représenter une expression mathématique sous une
forme préfixée totalement parenthésée.
En C, on écrit par exemple cos(x+2) pour exprimer le cosinus de x+2 . En SCHEME, nous écrirons de manière
« préfixée », ce qui signifie que l’opérateur sera toujours en tête de l’expression, et pour éviter toute
ambigüité, nous placerons des parenthèses comme ceci :
(cos (+ x 2))
Donc au lieu d’écrire f(x,y,z) , vous écrirez (f x y z) sans virgules, le même processus de conversion
s’opérant à tous les niveaux, c’est-à-dire par récurrence dans les sous-expressions x, y et z. Dans une expression
bien formée, il y aura donc autant de parenthèses ouvrantes que de fermantes. Quelques exemples :
en MATHS
x + y+ z
x − y+ z
sin(ωt + φ )
x + 2y = 0
€
€
2,71828
13
5
3 − 2i
x a x2 + 3
f ogoh
si x=2 alors 3 sinon y+1
en SC H E M E
(+ x y z)
(+ x (- y) z)
ou bien
(+ (- x y) z)
etc. [-y serait faux]
(sin (+ (* omega t) phi))
(= (+ x (* 2 y)) 0)
ou
(zero? (+ x (* 2 y)))
2.71828
la notation d’un nombre inexact [réel approché]
3
13/5 lorsqu’on le tape et 2
lors de l’affichage [avec la partie entière]
5
3-2i ce n’est pas une opération, mais la notation d’un nombre complexe !
(lambda (x) (+ (sqr x) 3)) la fonction qui à x associe x2+3
(compose f g h)
€ la composition de fonctions
(if (= x 2) 3 (+ y 1)) pas de mots then et else superflus...
€
Voilà. Bravo, vous savez déjà tout de la syntaxe de SC H E M E : l’art du bon balancement des
€ Art subtil, qui vous posera [peut-être] des problèmes au début mais qui ne devrait pas gêner des
parenthèses !
matheux qui font des notations leur pain quotidien... Désolé si vous trouvez cette notation compliquée pour des
problèmes simples [comme calculer x2+3], mais elle a été inventée pour simplifier la vie des programmeurs lors des
problèmes compliqués. Wait and see…
Exercice 1.1 : à faire sur papier a) Traduire en notation SCHEME l’expression sin(x+y/3) ≥ -a.
b) Traduire en notation mathématique usuelle l’expression SCHEME suivante, dans laquelle log désigne la fonction
« logarithme népérien » et abs ... vous devinez quoi :
(/ (log (- x 2)) (+ 1 (abs (- x 4))))
c) Rectifiez l’écriture de l’expression SCHEME suivante qui est bien pauvrement parenthésée :
(3 * sqrt(y + 2))
1.2 Le lancement de DRSCHEME
Allez faire votre vie dans le menu Démarrer, cherchez PLT, et lancez DRSCHEME !…
Bienvenue dans DrScheme, version 370[3m]
Langage: Assez gros Scheme.
D R S CHEME est un gros logiciel, dont vous n'exploiterez pas toutes les possibilités, et de loin. Il a été
spécialement conçu pour l'enseignement, la recherche et de le développement de gros programmes en SCHEME. Il est
lui-même [y-compris l'interface graphique] entièrement écrit en S CHEME . Il dispose d'un compilateur vers C, de
bibliothèques d’utilitaires pour le graphisme, pour Internet, etc. mais vous n'avez pas besoin de le savoir, puisque ce
cours MP2 n'est qu'introductif, donc oubliez tout cela. La fenêtre de DRSCHEME, par exemple celle que vous voyez
en arrivant, est composé de deux cadres :
• Celui du haut contiendra les définitions de votre programme [un programme S CHEME est donc un
ensemble de définitions]. Il contient un éditeur de programmes SCHEME.
• Celui du bas contiendra vos interactions avec DRSCHEME. Vous pourrez faire des calculs et tester vos
programmes « au toplevel » comme on dit...
En haut d'une fenêtre se trouvent, de gauche à droite :
• un bouton contenant le nom du fichier en cours d'édition [Sans Nom s'il est encore anonyme] ainsi qu’un
bouton (define …) qui permet de se positionner sur une définition.
• un bouton Sauvegarder pour sauver le fichier. Ce bouton est invisible si le fichier vient d’être sauvegardé...
• un bouton Déboguer qui permet d’exécuter en mode pas à pas, pour comprendre une erreur par exemple.
• un bouton Vérifier pour vérifier la syntaxe sans compiler et colorier le texte.
• un bouton Exécuter pour compiler l'ensemble des définitions et se retrouver au toplevel.
• un bouton Stopper pour forcer l'interruption d'un programme, lorsqu’il ne veut plus s’arrêter.
En bas d'une fenêtre se trouvent la position du curseur p:q [en ligne p et colonne q ], Lecture/écriture qui dit si
vous pouvez écrire ou pas, un petit dessin vert représentant l’icône des poubelles américaines, qui s'allume lorsque
D RSCHEME nettoie sa mémoire. Seule la position du curseur peut être importante s’il faut aller en ligne 57 par
exemple…
1.3 Les interactions au toplevel
Arrangez-vous pour que la fenêtre prenne tout l'écran [cliquez dans la case de zoom]. Le t o p l e v e l [cadre inférieur
de la fenêtre] peut être vu comme une super-calculette. Un « prompt » matérialisé par le signe > attend une
expression à évaluer. Calculons par exemple la moyenne des entiers de [1,10]:
> (/ (+ 1 2 3 4 5 6 7 8 9 10) 10)
; vous pouvez aller à la ligne en plein milieu
11
2
Un clic droit sur le résultat vous permet d’en changer le format, essayez !
Vous souhaitez faire le même calcul jusqu’à 12 ? Inutile de recopier la première ligne à la main. Tapez sur Esc-P
€
[une fois sur la touche Esc puis une fois sur la touche p, pas les deux en même temps !], les lignes précédentes sont
recopiées au prompt. Modifiez la ligne, placez le curseur à la fin de la ligne et relancez l’évaluation [celle-ci survient
sur une pression de la touche « Entrée » lorsque toutes les parenthèses sont refermées, même si l’expression tient
sur plusieurs lignes].
Exercice 1.2 : Calculez la factorielle 20! en multipliant les entiers de 1 à 20...
S CHEME travaille comme MAPLE « en précision infinie » sur les nombres exacts et contrairement à C qui se
limite à des nombres d’une quinzaine de chiffres, ce qui est insuffisant pour fairede l’arithmétique… Les codes
secrets utilisent couramment de grands nombres premiers d’une centaine de chiffres !
N.B. 11/2 est un nombre rationnel exact, à ne pas confondre avec 5.5 qui est un nombre réel inexact. Les primitives
exact->inexact et inexact->exact peuvent être utiles : (exact->inexact 11/2)  5.5 par exemple.
Le langage SCHEME distingue en effet deux types de nombres : exacts et inexacts. Les nombres inexacts [ou
approchés, comme les double de C] sont exprimés avec un point décimal comme 3.1416 ou en notation scientifique
comme 2.34e-2 qui signifie 2.34 10-2. Par exemple, 35 est un entier exact tandis que 35.0 est un entier inexact,
avec peut-être une erreur quelque part vers la 15ème décimale. Outre cette notion d’exactitude, SCHEME traite comme
MAPLE les catégories numériques usuelles : entiers [Z], rationnels [Q], réels [R], et complexes [C]. En réalité, le
type réel a été conservé par habitude, mais il ne se distingue pas des rationnels, à cause du nombre limité de
chiffres1 ! Vous trouverez la liste des primitives numériques dans la portion d’aide en ligne traduite en français
[fouillez dans le Menu Démarrer, rubrique PLT], cherchez le « Mémento du Schemeur ». Ouvrez une fenêtre
d’aide et tâchez d’avoir les deux fenêtres SCHEME et EXPLORER accessibles.
Exercice 1.3 a) Prévoyez le résultat de (/ 5 2) au toplevel. Vérifiez. Comparez avec (/ 5.0 2).
b) Demandez au toplevel la valeur de la constante prédéfinie pi [Rép : 3.14 ….]. Il n’y a pas de distinction
majuscules/minuscules, il est usuel de tout écrire en minuscules…
c) Calculez l’aire d’une cercle de rayon 3/2 [Rép: 7.06...]. On écrit bien 3/2 en Scheme et non un lourd (/ 3 2).
d) Calculez l’aire d’un triangle équilatéral de côté 5 [Rép : 10.82...]
e) Calculez la racine cubique de 10 [Rép : 2.15...]
f) Calculez le logarithme népérien de 10, puis son logarithme en base 2 [Rép : 2.30..., 3.32...]
g) Calculez une valeur approchée dans [0, π/2] de l’angle en radians dont le cosinus vaut 0.746. Quelle en est la
mesure en degrés ? [Rép : 0.72...rd = 41.75...degrés]
h) Les fractions 1789/1917 et 1917/1914 sont-elles irréductibles ? [Rép : oui, non]. Deux solutions : l’une
purement visuelle, l’autre en travaillant avec des entiers [calculez un pgcd : how do you say pgcd in English ?]
1.4 L’éditeur de programmes [la fenêtre du haut]
Programme = ensemble de définitions. Une définition a la forme :
(define symb expr) où symb est un « symbole », c’est-à-dire un mot qui ne soit pas une primitive2, et expr une expression, par
exemple numérique. Tapez dans l’éditeur [le cadre du haut] :
(define e (exp 1))
; la constante e base des logarithmes [tapez aussi ce commentaire]
pour définir le symbole e et lui associer la valeur 2.718... Cliquez sur Execute et vérifiez au toplevel que e est
bien défini en demandant sa valeur.
En programmation fonctionnelle, elle ne sert qu’à définir une constante : on ne modifiera plus jamais la valeur
de e. C’est une définition, pas une affectation ! Notez qu’en SCHEME et MAPLE [à l’opposé de C] on ne type pas les
variables : inutile de préciser que e est un réel…
Pour exprimer en SCHEME la fonction x a x + 2 par exemple, on écrira (lambda (x) (+ x 2)).
Pour la fonction ( x, y ) a x + 2y , on écrira (lambda (x y) (+ x (* 2 y))), etc. De manière générale :
(lambda (x1 ... xn) expr)
€
où x1,...,xn sont les paramètres [n ≥ 0] et expr l’expression formant le « corps » de la fonction.
€
Tapez dans l'éditeur les trois définitions de fonctions suivantes. Regardez bien ce qui se passe lorsque vous allez à
la ligne [indentation [= distance à la marge] automatique], et lorsque vous tapez une parenthèse fermante :
(define add4
(lambda (x)
(add2 (add2 x))))
; la fonction
(define add2
(lambda (x)
(+ x 2)))
; la fonction
(define add4
(compose add2 add2))
x a x+4
; en utilisant une fonction auxilliaire add2
x a x + 2€
; une autre définition €
plus élégante
; (compose f g) retourne la fonction composée
1
Sautez toujours
une ligne entre
deux définitions !
f og
Le « calcul réel exact » relève encore du domaine de la recherche. Par exemple les chiffres de 2 peuvent tenir sur un nombre fini d’octets
si au lieu du développement décimal usuel, on utilise un développement en fraction continue qui s’avère périodique…
€
2
Pour savoir si le symbole « truc » est une primitive, essayez de voir si ce mot tout seul a une valeur au toplevel… Essayez avec sqrt puis truc.
€
Si vous voulez que l’éditeur prenne toute la fenêtre, tapez Ctrl-E [-E sur Mac]. Si vous voulez réindenter
[mettre à la bonne distance de la marge] proprement tout votre texte, utilisez le menu Scheme/Réindenter Tout.
Passons maintenant à l'éxecution du programme. Cliquez sur Exécuter à la souris [ou Ctrl-T]. Le programme est
alors compilé. Les interactions antérieures au toplevel sont effacées et remplacées par un toplevel neuf, toutes les
définitions précédentes sont oubliées. Demandez par exemple (add4 (add2 5)) , il répond 11 . Puis (add4 x), qui
déclenche une erreur « reference to undefined identifier : x ». Récupérez la ligne fautive avec Esc-p. La
ligne entière est recopiée, vous pouvez la modifier en remplaçant par exemple x par 2.58 . Vous pouvez ainsi
recopier directement toute demande antérieure de calcul...
Si vous avez modifié ne serait-ce qu'un seul caractère dans le fichier et que vous n'avez pas redemandé Execute avant
de calculer dans la fenêtre d’interactions [le toplevel], DRSCHEME s'en apercevra et vous préviendra d'un défaut de
synchronisation par un message en jaune. Essayez tout de suite...
Exercice 1.4 : Devinez ce qui se passera si l’on évalue (3
error messages ?…
4 5 6)
au toplevel. Just do it ! Can you understand
Notez bien que dans un programme, l'ordre des fonctions n'a pas d'importance. Si une fonction f utilise
une fonction g, il est même considéré comme méthodologiquement préférable d'énoncer f avant g. Un peu comme le
mathématicien qui énonce d'abord le théorème [c'est lui qui est important] et ensuite seulement descend détailler les
lemmes techniques nécessaires à sa démonstration. Lorsqu'on lit un programme, il est bon d'avoir vite sous les yeux
son architecture essentielle, et seulement plus tard les détails de construction. Mais la méthode inverse de conception,
qui énonce d’abord les détails puis ensuite le programme principal, est parfois intéressante3. Quoiqu'il en soit,
DRSCHEME vous laisse libre, il se débrouillera pour ré-organiser vos fonctions. Ce que nous venons de dire ne vaut
que pour les fonctions. L’ordre serait par contre important pour des variables globales :
(define pi (acos -1))
(define pi-sur-2 (/ pi 2))
; pi n’est pas une fonction, mais un nombre...
; idem, et on suppose que pi existe déjà !
Enfin, la fonction add4 est définie deux fois dans le programme. Il est souvent intéressant d'avoir dans un
fichier plusieurs versions d'une même fonction avec le même n o m, mais c'est la dernière
compilée qui gagne, celle qui est située le plus bas dans le fichier. Si vous voulez mettre une version en
commentaire, sélectionnez à la souris sa définition [cf. paragraphe suivant] et utilisez le menu
Scheme/Commenter avec des points-virgules. Ou avec des boîtes. Essayez...
Pour enregistrer votre fichier, utilisez toujours le bouton Sauvegarder, sauf si vous voulez changer son nom, auquel
cas vous utiliserez Sauvegarder les définitions... dans le menu Fichier.
Ne venez jamais en TP sans votre clé USB !!!
N.B. i) Une dernière remarque : si vous ouvrez deux fenêtres D R S CHEME , les fonctions compilées dans l’une ne
seront pas connues dans l’autre, ce qui permet de travailler sur deux programmes indépendants. Il reste bien entendu
possible de faire du « Copier-Coller » d’une fenêtre à l’autre. Mieux vaut en fait utiliser des « onglets ».
ii) Ne lancez pas DRSCHEME en cliquant sur un de ses fichiers, cela ouvrirait un autre DrScheme chaque fois !
Exercice 1.5 : Définissez les fonctions suivantes dans l’éditeur, puis testez-les au toplevel :
a) la fonction constante k2 : x a 2 .
b) la fonction identité id : x a x [en réalité, la fonction id est une primitive, mais vous pouvez la redéfinir].
c) la fonction proj2 : ( x, y ) a y [la seconde projection].
d) la fonction distance prenant 4 paramètres x1, y1, x2, y2 et retournant la distance euclidienne du point
M1(x1,y1) au point M2(x2,y2) .
€
e) un prédicat [fonction à valeurs booléennes] mul4? testant si un entier n est divisible par 4 [utilisez les fonctions
€
quotient et modulo dans les entiers]. Les booléens de SCHEME se nomment #t pour « vrai » et #f pour
« faux ». Par convention, le nom d’un prédicat se termine par un ? comme zero? ou integer? ou number? , etc.
Vous n’avez pas besoin d’utiliser un quelconque « if » dont nous n’avons pas encore parlé…
3
On parle de programmation descendante [top-down programming] ou ascendante [bottom-up programming].
f) une fonction (volume R) retournant le volume d’une sphère de rayon R. Une sphère de rayon 2 m a un volume
d’environ 33.5 m3, vérifiez-le.
g) une fonction (loto) sans paramètre retournant un entier aléatoire de [1,49] . Utilisez (srandom n ) qui
retourne un entier aléatoire de [0,n-1], ou un inexact aléatoire de [0,n[ si n est inexact : (srandom 1.0)…
h) une fonction (alea a b) prenant deux entiers a et b [a ≤ b] et retournant un entier aléatoire de [a,b] . Testez
plusieurs fois sur (alea 2 4), vous devez obtenir 2, 3 ou 4.
i) la fonction (einstein u v) : loi d'addition des vitesses en relativité restreinte, avec la vitesse de la lumière
c ≈ 300000 km s-1 :
u+v
( u,v) a uv
1+ 2
c
A.N. Rien ne vaut un problème concret : un voyageur court à 250000 km.s-1 dans le couloir d'un train, dans le
sens de la marche. Et le train roule à 270000 km.s-1. Quelle est la vitesse du voyageur par-rapport à la voie ?...
j) Ecrire une fonction (stirling n) retournant un équivalent
en + ∞ de n ! en utilisant la « formule de
€
Stirling ». Testez avec n=20 [calculez le quotient avec la vraie factorielle] :
n! ≈
n
2nπ  
e
n
Exercice 1.6 : Définissez les fonctions suivantes :
a) La fonction (delta a b c) retournant le discriminant Δ = b2-4ac du trinôme ax2+bx+c.
A.N. Quel est le discriminant du trinôme 3x2-10x+9 ?
b) Le prédicat (racines-reelles? a b c) retournant #t si et seulement si les racines sont réelles. Non, vous
n’avez toujours pas besoin de savoir quoi que ce soit sur le mot « if »…
3
A.N. Le trinôme x 2 − x 2 + admet-il des racines réelles ?
5
c) La fonction (une-racine a b c) retournant l’une des racines, celle que vous voulez. Si Δ < 0, vous laisserez
SCHEME retourner tranquillement une racine… imaginaire puisqu’il connaît les nombres complexes !
Le signe ¶€indiquera dorénavant une question un peu plus délicate...
Exercice 1.7 a) Définir la fonction D : ( f , x ) a f ′ (x) : la dérivation numérique approchée [utilisez une
approximation par une corde (f(x+h)-f(x))/h en prenant h=10-4]. Vérifiez sur la dérivée du logarithme en 1/2
qui devrait donner une valeur proche de 2 [ce calcul est numériquement assez instable].
b) ¶ Pouvez-vous calculer la dérivée seconde du logarithme au point 1/2 ?…
€
c) ¶ En fait, nous pouvons faire bien mieux que a) et b) en exprimant comme en maths le fait que D( f ) = f ′
sans référence à un point x. Programmez une seconde version de la fonction D pour qu’elle prenne une fonction f et
retourne la fonction f’ . Le mathématicien comme le physicien travaillent couramment dans des espaces de
fonctions [l’espace des fonctions continues sur R, etc]. Or en 2007 bien des langages de programmation [C par
€
exemple] ne considèrent toujours pas les fonctions comme des variables à part entière !
A.N. Avec cette nouvelle version, recalculez la dérivée du log en 1/2, puis sa dérivée seconde en exprimant tout
simplement que la dérivée seconde n’est autre que la dérivée première de la dérivée première...
N.B. Les nombres complexes en SCHEME ne sont pas au programme de l’examen. Le nombre i tout seul se note +i
pour le distinguer d’une variable i. Les primitives complexes sont dans le Mémento du Schemeur.
Exercice
1.8 o p t i o n n e l ¶ Documentez-vous sur les primitives complexes. Ecrire une fonction
(rotation z0 α) prenant un nombre complexe z0 et un réel α, et retournant la rotation complexe de centre z0 et
d’angle α [en radians]. Oui, le résultat est bien une f o n c t i o n et pas un nombre complexe !
A.N. Calculez l’image de z = 3+2i par la rotation de centre z0 = 1 + i et d’angle α = - π /4 . Vérifiez
graphiquement sur une feuille de papier.
Vous avez vraiment tout fait ? Mmmmmm, bien !
Faculté des Sciences de Nice
L2 Maths 2007-08
Initiation à la programmation fonctionnelle
Feuille n° 2 : Evaluation et « formes spéciales »...
2.1 Sur le mécanisme d’évaluation d’un appel de fonction
En d’autres termes, comment ça marche ? Supposez que l’on demande l’évaluation d’un appel de fonction de la forme
(f a b c) où a, b et c sont des expressions quelconques. Attention, f doit être une fonction1 :
-
soit une fonction primitive comme sqrt, + ou sin…
soit une fonction écrite en SCHEME via une lambda : (define
add4 (lambda (x) (+ x 4)))
par ex.
Pour vous en assurer, demandez au toplevel s’il s’agit bien d’un objet « de type procédure » :
> sin
#<primitive:sin>
> add4
#<procedure:add4>
> (procedure? sqr)
#t
Si vous demandez si define est une procédure, vous vous ferez insulter, essayez ! En réalité, define n’est pas un
nom de fonction, mais le « mot-clé [keyword] d’une forme spéciale ». Pour l’instant, parlons des fonctions,
avec le calcul de (f a b c). Voici ce qui va se passer :
i)
ii)
les quatre expressions f, a, b et c vont être évaluées [dans un ordre inconnu], donnant des valeurs
B et C. La valeur F doit être un objet de type procédure, sinon erreur.
La procédure F est « appliquée » aux valeurs A, B et C pour fournir la valeur finale recherchée.
F, A,
Vous noterez que pour décrire la valeur de (f a b c), nous demandons par récurrence la valeur des quatre sousexpressions ! Comment enfin la procédure F est-elle appliquée ? Deux possibilités :
• si F est une primitive comme max, elle fonctionne comme on le pense, elle calcule le maximum.
• si F provient de l’évaluation d’une lambda-expression, comme (lambda (x y z) (+ x (* 2 y z))),
on peut imaginer que les paramètres x, y et z vont être remplacés par les valeurs A, B et C , puis que
l’expression (+ A (* 2 B C)) va être évaluée pour fournir la valeur recherchée.
Il s’agit là du « modèle de substitution » [SICP § 1.1.5]. Ce n’est pas réellement ce qui se passe dans la
machine, mais comme en physique un bon modèle, même faux, peut suffire tant qu’il n’est pas contredit ! Cet ordre
de calcul se nomme « l’ordre applicatif » d’évaluation. Il correspond à peu près à ce qui se passe en C, sauf
qu’en SCHEME la fonction aussi peut être calculée ! Il est important de bien saisir que les arguments sont évalués
AVANT que le texte de la fonction n’agisse ! Vous voyez maintenant pourquoi define n’est pas une fonction : si
c’en était une, en écrivant (define x 1), on chercherait à évaluer x, ce qui est absurde puisque le but consiste
justement à lui donner une valeur ! Vous verrez en tout une dizaine de formes spéciales comme define [lambda
et if en sont deux autres]. Chaque forme spéciale a un fonctionnement… spécial : lire la doc [par exemple le
mémento du Schemeur] !
N.B. L’ordre applicatif n’est pas le seul possible en programmation.Les curieux regarderont dans le livre d’Abelson
& Sussman [SICP] page 14 la notion d’ordre « normal » [qui n'est pas utilisée en S CHEME ]. Notez que votre
cerveau [on l’espère] ne fonctionne PAS en ordre applicatif : comment calculez-vous 1000! x (2-2) ?
Retenez : une fonction travaille en
ORDRE
APPLICATIF ,
une forme spéciale non !
Assurez-vous d’avoir compris cela !
1
SCHEME emploie indistinctement les termes de procédure et de fonction. Une fonction retourne un résultat.
2.2 Booléens et prédicats
Les deux constantes booléennes se nomment #t [pour true] et #f [pour false]. On nomme prédicat une
fonction à valeur booléenne, par exemple integer? [être un entier], odd? [être impair] ou encore zero? [valoir 0] :
> (integer? pi)
; la valeur du symbole pi est-elle un entier ?
#f
> (zero? (modulo 20 5)) #t
> (not (odd? 4))
#t
; le reste de la division de 20 par 5 est-il égal à 0 ? [⇔ 20 est-il multiple de 5 ?]
; est-il vrai que 4 n’est pas impair ?
Exercice 2.1 Définir le prédicat (good? n) retournant #t ssi l’entier n ∈ Z est de la forme 5k+3 avec k ∈ Z :
(good? 53) → #t
(good? 55) → #f
(good? -53) → #f
Il est usuel de terminer le nom d'un prédicat par un point d'interrogation. Un peu comme une question…
2.3 Les conditionnelles if et cond sont des formes spéciales
En SCHEME, nous utilisons des expressions, pas des instructions. Donc la forme « if » sera une expression
ayant une valeur, par exemple :
> (if (integer? pi) (+ 2 3) (* 2 3))
6
Le format général sera donc
(if p q r)
; si la valeur de pi est un entier, alors 5 sinon 6
où p, q et r sont des expressions,
p
étant un « test ».
Exercice 2.2 a) Comment définiriez-vous la fonction (abs x) retournant la valeur absolue d’un nombre réel x
si elle n’était pas prédéfinie en SCHEME : (abs –2) → 2 [lisez tout de suite le N.B. qui suit l’exercice].
b) Définir la fonction (khi12 x) retournant la fonction caractéristique de l’intervalle [1,2] dans R , la fonction
qui vaut 1 si x ∈ [1,2] et 0 sinon :
(khi12 2/3) → 0
(khi12 3/2) → 1
(khi12 -5.28) → 0
c) Définir une fonction (tirage) sans paramètre retournant aléatoirement l’un des nombres 2 ou 5 avec la même
probabilité d’apparition. Testez-la plusieurs fois au toplevel [utilisez Esc-p]…
N.B. A propos de la fonction abs, notez que SCHEME peut vous autoriser à redéfinir certaines fonctions prédéfinies
[ses « primitives »]. Ce n’est pas toujours malin, par exemple si votre définition est mauvaise ou moins efficace
que la sienne : toute liberté se paye ! Notez donc $abs plutôt que abs pour ne pas tuer la primitive… Pour savoir si
abs est bien une primitive, vérifiez si le symbole abs possède bien une valeur au toplevel…
Il est assez désagréable d’avoir des if emboîtés, à la fois pour la lisibilité et parce que l’indentation [distance à la
marge gauche] file très vite à droite. A cet effet, Scheme dispose de la forme c o n d plus générale, dont l’explication
en termes de if est la suivante, avec par exemple un cond à 3 clauses :
(cond (t1 e1)
(t2 e2)
(else e3))
<==>
(if t1
e1
(if t2 e2 e3))
Chaque ti est un test, par exemple (> x y) et chaque ei une expression, par exemple (+ x 1) . Il peut y en avoir
autant que l’on veut. Notez que le mot cond est toujours suivi de deux parenthèses ! Il est important de bien saisir
que les tests t1, t 2,… sont réalisés dans cet ordre. Donc si l’on considère t2, c’est que t1 a échoué. Il faudra s’en
servir pour raisonner et savoir où l’on en est !
Exercice 2.3 Comment définiriez-vous la fonction ($abs
x)
avec un cond ?
Exercice 2.4 Suite à l'exercice précédent, supposons que l’on veuille simuler le if de S CHEME en utilisant
Montrez que la définition ci-dessous est incorrecte en donnant un contre-exemple où (if p q r ) et
($if p q r) ne fonctionneraient pas de la même façon :
cond .
(define ($if p q r)
(cond (p q)
(else r)))
; essayez ($if (integer? pi) (+ 2 3) (* 2 3)), ça marche ?
Exercice 2.5 Définir avec un cond à 5 branches [les 5 sous-intervalles de l'axe Ot] la fonction s représentant un
signal dépendant du temps t et dont le graphique est donné ci-dessous. Faites varier t de -∞ à +∞ dans le cond :
s(t)
2
1
-3
-1
0
2
4
t
2.4 Les conditionnelles and et or sont aussi des formes spéciales !
Comme en C, ce ne sont pas des fonctions à valeurs booléennes ! En particulier, elles ne sont pas commutatives.
Par exemple, dans (and #f q), il est inutile d’aller perdre du temps à évaluer le test q puisque de toutes façons le
résultat « faux et q » sera égal à faux quelle que soit la valeur de q ! Donc sous le and se cache un if déguisé :
(and p q)
(and p q r)
<==>
<==>
(if p q #f)
(if p (and q r) #f)
etc. Comprenez-vous bien ?
Autrement dit, on cherche le premier test donnant #f . Dès que l’on en trouve un, on sort du
résultat #f. Sinon, le résultat est celui du dernier test. On peut donc écrire par exemple :
and
avec comme
(if (and (rational? obj) (= (numerator obj) 2)) ...)
et l’on ne considèrera le numérateur de l'objet inconnu obj que dans le cas où obj est bien un rationnel ! Ce qui est
intelligent, car si obj n’est pas un rationnel, la fonction numerator déclencherait une erreur !
Exercice 2.6 Avant d'essayer au toplevel, prévoyez les valeurs des expressions suivantes, sachant que
déclenche l’erreur « number expected » :
(and (< pi 3) (cos #f))
Exercice 2.7 Définir la fonction (khi12
x)
(cos #f)
(and (cos #f) (< pi 3))
de l’exercice 2.2 avec un if et un and.
• La forme or est duale : un or est vrai si au moins l’un des tests est vrai. Donc on cherche le premier qui donne
une valeur vraie [<==> distincte de #f] et on rend cette valeur. Sinon on rend la valeur du dernier :
(or p q)
(or p q r)
<==>
<==>
(if p p q)
(if p p (or q r))
sauf qu’en réalité on ne calcule p qu’une seule fois.
etc.
2.5 Les variables locales et la forme spéciale
let En maths, vous êtes habitués à faire des changements de variables. Par exemple, considérons la fonction :
f(x,y) = x2 + y3 + sin(x2 - y3)
On ne souhaite calculer deux fois ni x2 ni y 3. Le mathématicien va raisonner en termes de fonctions composées,
remarquer qu’en réalité f(x,y) = g(x2,y3) avec g(u,v) = u + v + sin(u – v). En SCHEME, cela revient à
et
introduire une fonction auxilliaire :
(define (f x y)
(g (* x x) (* y y y)))
avec :
(define (g u v)
(+ u v (sin (- u v))))
Ce que nous venons de voir montre que les « variables locales » sont parfaitement inutiles en théorie
puisqu’équivalentes au concept mathématique et propre de « fonction composée ». Ou de changement de variable, ce
qui revient au même : posons u = x2 et v = y3…
Le changement de variables u = x2 , v = y3 était possible en une seule fonction par la forme let qui permet de
déclarer et d’initialiser plusieurs variables locales à un calcul. Voici par exemple la fonction f(x,y) ci-dessus :
(define (f x y)
(let ((u (* x x)) (v (* y y y)))
(+ u v (sin (- u v)))))
; par pitié, attention aux parenthèses dans le let !…
; on utilise souvent des crochets pour des couples !
Le format général de la construction let [qui se lit « sachant que… »] est la suivante :
(let ((x1 e1) ... (xn en))
expr)
; ⇔ (let ([x1 e1] ... [xn en])
;
expr)
où x1,...,xn sont des symboles et e1,...,en des expressions. Les expressions e1,...,en sont toutes calculées en
même temps dans le contexte courant, puis les valeurs obtenues deviennent temporairement les valeurs des variables
x1,...,xn , juste le temps de calculer la valeur de l’expression expr qui forme le « corps » du let . Une fois cette
valeur calculée, les variables x1,...,xn reprennent leurs anciennes valeurs [si elles en avaient].
Exercice 2.8 L’aire A d’un triangle quelconque de côtés a, b, c est donnée par la formule :
A=
p( p − a)( p − b)( p − c)
où p représente le demi-périmètre. Ecrire la fonction (aire a b c) retournant l’aire d’un triangle de côtés a , b , c ,
en ne calculant le demi-périmètre p qu’une seule fois. Quelle est l’aire d’un triangle de côtés 1, 1, 1 ? Et de côtés
1, 2, 3 ? Et si les côtés sont de longueur 1, 2, 5 ?…
Exercice 2.9 a) Evaluez (define x 2) au toplevel. Prévoyez à l'avance sur papier ce que valent chacune des
expressions ci-dessous, puis faites-vous corriger par le toplevel :
(let ([y (+ x 1)] [x (* x 2)])
(+ x y))
b) Montrez que la version suivante de la fonction f(x)
(define (f x)
(let ([u (* x x)] [v (* u x)])
(+ u v)))
(let ([x (* x 2)] [y (+ x 1)])
(+ x y))
= x 2 + x3
est erronnée :
; testez-la avec x = 2
c) La définition précédente fonctionnerait par contre en remplaçant let par let* qui force les liaisons des variables
locales de la gauche vers la droite, permettant donc pour v d’utiliser la valeur que vient de prendre u , ce que ne
permet pas let. Vérifiez-le ! Mais c'est quand même let qui reste le plus courant [et le plus rapide]…
d) Comment pourriez-vous quand même poser « v = u * x » si let* n’existait pas ? [Montrez qu’en réalité sous
let* se cache let…]
Exercice 2.10 Travaillez sur papier d’abord, puis vérifiez ensuite votre réponse au toplevel. Que vaut
l’expression suivante, en supposant que x vaille 2 au toplevel ?
(let* ([y (* x 2)] [* +])
; * est un symbole comme un autre…
(let ([cube (lambda (y) (+ y 10))] [x (+ x 1)] [y (* x y)])
(cube (* x y))))
N.B. Vous avez remarqué la possibilité d'avoir des fonctions en variables locales. C'est en réalité assez rare
d'utilisation. Nous verrons plus tard les véritables « fonctions locales »…
Exercice 2.11 Une autre manière de voir « l’inutilité théorique » du concept de variable locale. Montrez que
l’expression suivante peut s’écrire sans let ni let* et sans utiliser aucun define ! Et bien entendu en faisant
strictement les mêmes calculs ! Indication : utilisez une fonction anonyme…
(let ([x (sin 1)] [y (sqrt 2)])
(+ (/ x y) (/ y x)))
⇔
???
Exercice 2.12 a) Ecrire une fonction (monte-carlo) retournant aléatoirement l’un des nombres 2, 5 ou 9 avec
la même probabilité 1/3 d’apparition. On n'utilisera qu'une seule fois la primitive (srandom n).
b) Ecrivez de même (las-vegas) retournant 2 avec la probabilité 2/7, ou bien 5 avec la probabilité 1/7, ou bien
9 avec la probabilité 4/7. Autrement dit, un générateur aléatoire biaisé ! Testez-le sur une dizaine de tirages.
2.6 Les deux manières de définir une fonction
Jusqu’à présent, vous avez défini une fonction comme une variable ordinaire, dont la valeur était fournie par
l’évaluation d’une λ-expression :
soit mul2
la fonction qui à x
associe 2x
(define mul2
(lambda (x)
(* x 2)))
Maple
mul2:=
proc(x)
RETURN(2 * x);
end:
Il existe une autre manière, plus proche de celle de C par exemple, et parfaitement équivalente, c’est le « style
MIT », du nom de la célèbre université américaine2 qui inventa SCHEME [et bien d’autres choses] :
(define (mul2 x)
(* x 2))
double mul2 (double x) {
return 2 * x;
}
C
C’est effectivement plus simple, mais la forme « puriste » avec une lambda explicite s’avère utile dans les cas
compliqués. Quant aux livres, ils ont chacun leur style préféré… En examen, vous êtes parfaitement libre de votre
choix, que ce soit clair. Notez que cela ne signifie pas que vous échapperez toujours aux lambda , loin de là, vous
verrez leur puissance réelle un peu plus tard...
Exercice 2.13 Ecrire en « style MIT » les fonctions suivantes :
a) Un prédicat (fraction-egyptienne? obj) prenant un objet obj quelconque et testant s’il s’agit d’un rationnel
dont le numérateur est égal à 1 [une fraction égyptienne]. Testez sur 1/4, sur pi et sur #t. Vous devez obtenir
respectivement #t, #f et #f sans provoquer d’erreur.
b) La fonction (loto) sans paramètre de l’exercice 1.5 g), retournant un nombre entier aléatoire de [1,49].
c) Une fonction (somme-carrés a b c) prenant trois réels a, b, c et retournant la somme des carrés des deux
plus grands. Exemple : (somme-carrés 4 –8 1) → 17
Exercice 2.14 optionnel style partiel Supposons que l’impôt [fictif] sur le revenu annuel soit calculé par
tranches de la manière suivante. Un salarié ne paye rien pour les 8000 premiers € qu’il gagne. Il paye 10% sur
chaque euro gagné entre 8000 € et 25000 €, et enfin 20% sur chaque euro gagné au-dessus de 25000 €..
a) Ecrire une fonction abstraite (tranche s b h p) retournant l’impôt dû pour un salaire annuel s dans la tranche
[b,h] dont le pourcentage est p %. Exemple :
(tranche 2500 2000 3000 10) → 50
(tranche 4000 2000 3000 10) → 100
b) A.N. Utiliser la fonction abstraite tranche pour en déduire la fonction (impôt s) retournant l’impôt total
calculé par tranches pour un salaire annuel s avec les données numériques données au début de l’exercice.
Exemple :
(impôt 40000) → 4700
Exercice 2.15 o p t i o n n e l Programmez par récurrence la fonction factorielle fac : n → n! en SCHEME.
Oui, vous pouvez le faire en n’utilisant que ce que vous connaissez jusqu’à présent. Pensez simplement, soyez
rigoureux, n’ayez pas froid aux yeux, et ça marchera :
(fac 5) → 120
(fac 50) → 30414093201713378043612608166064768844377641568960512000000000000
Jusqu’ici nous avons étudié les constructions linguistiques de base,
mais peu l’aspect algorithmique, le plus intéressant.
Rassurez-vous, cela va changer 
2
Le « Massachusetts Institute of Technology », situé à Boston [USA].
Faculté des Sciences de Nice
L2 Maths 2007-08
Initiation à la programmation fonctionnelle
Feuille n° 3 : Premiers pas en terrain récursif...
3.1 Programmer par récurrence : les « fonctions récursives »
La récurrence est un outil d'une puissance inégalée pour :
a) construire des objets [un entier naturel n'est pas autre chose que 0 ou le successeur d'un entier naturel].
n(n +1)(2n + 1)
b) démontrer des théorèmes : 12 + 2 2 + 32 + ...+ n 2 =
par récurrence sur n entier ≥ 0.
6
c) concevoir des algorithmes et écrire des programmes ! C'est bien entendu ce qui va nous intéresser ici.
Notre objectif est donc de programmer des fonctions récursives. Ce mot savant désigne simplement une
fonction définie par récurrence, i.e. s’utilisant elle-même. Typiquement, la factorielle d'un entier naturel, la définition
du ! utilisant ce même ! :
 1 si n = 0
n!= 
n × (n − 1)! sinon
que nous traduisons naturellement en SCHEME par une conditionnelle :
(define (fac n)
; n entier naturel, calcule n!
(if (zero? n)
1
; le cas de base
(* n (fac (- n 1)))))
; le cas général
Calculez 1000! au toplevel. Vous pouvez chronométrer vos algorithmes avec time [en millisecondes] :
(time (fac 1000))
Cette fonction fac est une description en quelque sorte axiomatique de la factorielle. Il faut bien se mettre dans la
tête que l'on n'a pas à savoir ce qui se passe dans la machine lorsque ce calcul s'exécute ! Ce qui va se passer ne
dépend que de l'état de l'art des compilateurs. Rien n'empêchera par exemple un [très bon] compilateur, en 2018, de
modifier le texte de votre fonction pour le rendre plus efficace. Il faut donc s'efforcer de raisonner de manière
statique : supposons que le programme marche bien pour n-1 et faisons en sorte qu'il marche bien pour n…
Ne cherchez pas à penser dynamiquement : comment va-t-il calculer, va-t-il faire une "boucle", etc ? Peu
importe, c'est son problème. Notre seul problème à nous, c'est la rigueur de la récurrence. En d'autres termes, oubliez
les boucles, les stratégies et les actions, et raisonnez comme en maths, n'hésitez pas à poser une hypothèse de
récurrence si vous êtes bloqué. Vous êtes en MP, cela ne devrait pas vous poser de problèmes.
Nous mettrons pleins de bémols plus tard, ceci est un discours provisoire, simplement pour « casser » les
réflexes issus de C. La méthodologie de programmation n'est en effet pas du tout la même ! En C, il fallait dire pas
à pas tout ce que l’on faisait, et modifier la mémoire à la main avec l’affectation. Ici, nous laissons le compilateur
traduire nos descriptions fonctionnelles en actions sur les variables.
Retenez : une fonction récursive se construit comme une démonstration par récurrence :
- on commence par traiter le cas général : n! = n * (n - 1)! en trouvant une relation de récurrence.
- on s'aperçoit qu'il y a une quantité strictement décroissante qui va converger, ici n vers 0 : le cas de base.
N.B. Une fois le raisonnement effectué, la programmation commencera bien entendu par le cas de base. On va
toujours du cas le plus simple vers le cas le plus général. L'oubli du cas de base a des conséquences terribles…
Exercice 3.1 Essayez de faire fonctionner la factorielle en oubliant le cas de base. Utilisez le bouton « Stopper»
en haut à droite lorsque vous sentez qu’une exécution ne termine pas. N’attendez pas trop longtemps…
Exercice 3.2 Au lieu de faire une récurrence de n-1 vers n, Ray Cursif propose « d’aller de n vers n+1, comme
en utilisant la relation correcte n! = (n+1)!/(n+1). Il programme ainsi :
en maths »,
(define (ray-fac n)
(if (zero? n)
1
(quotient (ray-fac (+ n 1)) (+ n 1))))
Pourquoi cette définition est-elle incorrecte ?...
Ne confondez pas quotient et / . La primitive (quotient a b) calcule le quotient entier, alors que
(/ a b) produit un rationnel si a et b sont entiers exacts. Si l’un des deux est inexact, le résultat sera inexact par
contagion de l’inexactitude. Comparez au toplevel (quotient 5 2) et (/ 5 2), puis (/ 5.0 2).
N.B.
• Dans les cas de petits malheurs, il existe des outils assez verbeux [dont le bon programmeur se sert peu puisqu'il ne
fait pas d'erreur bien entendu ], par exemple la « trace » qui permet d'espionner les appels à une fonction donnée.
Ecrivez en première ligne de votre fichier la ligne suivante, qui va charger un module spécialisé :
(require (lib "trace.ss"))
et vous pourrez alors « tracer » la fonction ray-fac pour voir ce qui se passe :
> (trace ray-fac)
> (ray-fac 5)
…………………
> (untrace ray-fac)
; Cliquez sur « Stopper » très vite et analysez la trace…
; pour supprimer la trace sur ray-fac
Comparez avec (fac 5) en traçant fac . Une fois que vous n'en avez plus besoin, mettez un point-virgule de
commentaire au début de la ligne du (require (lib "trace.ss")) pour éviter de perdre du temps en chargeant la
trace à chaque exécution .
Exercice 3.3 a) Ecrire une fonction récursive (somme-carrés n) prenant un entier naturel n ∈ N et retournant
la somme 12 + 22 + … + n2. Exemple : (somme-carres 100) → 338350.
N.B. Lorsqu’on vous dit « prenant un entier naturel n ∈ N », ceci est garanti, et vous n’avez donc pas à le tester.
C’est à celui qui utilisera cette fonction de vérifier qu’il est bien dans le domaine de définition de la fonction.
b) Ici, vous avez la chance de pouvoir « résoudre la récurrence ». Reprogrammez cette fonction avec une formule
polynômiale directe, en une ligne [le « if » devient alors inutile…].
c) Généralisez en une fonction récursive (somme-carrés a b) prenant deux entiers a ≤ b dans Z et retournant la
somme a2 + (a+1) 2 + ... + b2. Exemple : (somme-carrés 100 200) → 2358350.
N.B. Lorsqu’on vous dit « écrire une fonction qui … », cela ne veut pas dire que l’on doive [ou puisse] le faire en
une seule fonction. Rien ne vous empêche de recourir à des fonctions auxilliaires [parfois vous y serez obligé].
Et c’est souvent plus clair d’écrire plusieurs petites fonctions qu’un énorme dinosaure !
Exercice 3.4 On dit que n droites du plan sont « en position générale » si leur ensemble ne contient aucun
couple de droites parallèles ni aucun triplet de droites concourrantes , comme les 3 droites de la figure ci-dessous :
3
4
2
7
1
6
5
a) Ecrire une fonction récursive (nb-regions n) retournant le nombre de régions du plan [bornées ou pas]
délimitées par n droites en position générale. Raisonnez par récurrence : supposons que l’on connaisse le nombre
de régions Rn-1 pour n-1 droites, comment en déduire Rn ? Exemple : (nb-régions 3) → 7
b) Ecrire la même fonction avec une formule directe [on « résoud » ici aussi la récurrence, ce qui est rarement
faisable, ne vous illusionnez pas…]. Vous devez trouver un polynôme en n de degré 2…
Exercice 3.5 a) Ecrire la fonction (int f a b δ) retournant une valeur approchée de l’intégrale de la fonction
continue sur [a,b], en utilisant la méthode de Riemann qui consiste à découper [a,b] en sous-intervalles de
largeur δ. Intégrons par exemple x a x 3 sur [0,1] avec un pas de 0.001 :
(int (lambda (x) (* x x x)) 0 1 0.001) → 0.249…
b) Par intégration d’une fonction bien choisie, retrouvez une valeur approchée du nombre π.
c) Quelle est la valeur moyenne du sinus sur [0,π] ? Réponse = 0.636…
f
N.B. Vous verrez de meilleures méthodes d’intégration numérique au second semestre en Analyse Numérique…
Exercice 3.6 a) Ecrire une fonction (interfac a b) prenant deux entiers a et b quelconques dans Z et
retournant le produit des entiers de l'intervalle [a,b]. Exemples :
(interfac 3 5) → 60
(interface 5 3) → 1 [pourquoi ?]
b) En déduire une nouvelle définition de la factorielle.
Programmation modulaire : Lorsque vous sentez qu’une fonction [ou un ensemble de fonctions] sera sans
doute utile ultérieurement, il est souhaitable que vous en fassiez un fichier séparé. Par exemple, interfac et fac
de l’exercice ci-dessus. Ouvrez une nouvelle fenêtre [menu Fichiers] et placez-y par copier-coller les textes
d’interfac et de fac. Sauvez ensuite cette nouvelle fenêtre sur votre clé USB sous le nom fac.scm . Dans un TP
ultérieur, vous avez besoin d’une factorielle ? Rien de plus facile, il suffit de placer dans votre fichier une ligne [en
supposant que le fichier est dans le répertoire scheme de votre clé USB sur le lecteur d:] :
(load ”d:/scheme/fac.scm”)
Le fichier sera alors automatiquement chargé et compilé à la volée, et les deux fonctions qu’il contient aussitôt
disponibles. Vous construisez ainsi de petits composants logiciels ré-utilisables : c’est propre et cela vous
facilitera la vie en vous évitant d’écrire 36 fois le même programme… A vous de gérer vos propres
« bibliothèques logicielles » comme le matheux gère ses « théorèmes » et le physicien ses « lois »…
Notez au passage qu’en Scheme , le séparateur dans un chemin menant à un fichier est le / comme sous LINUX et
Mac, et non le \ de Windows, attention…
p
Exercice 3.7 a) Ecrire une fonction récursive (binomial n p) retournant comme en MAPLE le nombre Cn de
parties à p éléments d'un ensemble à n éléments [on supposera 0 ≤ p ≤ n] . Utiliser la relation de récurrence bien
p
p−1
connue C np = C n−1
+ Cn−1
[pensez au triangle de Pascal], en étudiant soigneusement le cas de base. Exemple :
(binomial 10 6) → 210.
b) Combien de mains de 13 cartes dans un jeu de 52 ? Plus ou moins de 1 million à votre avis ? Vérifiez…
c) Ecrire la fonction (binomial n p) mais en utilisant cette fois un quotient de factorielles bien connu.
Comparez les temps de calcul de (binomial 20 10) avec les deux versions.
d) Combien votre solution à c) fait-elle de multiplications ? Si elle en fait environ 2n, réfléchissez encore, vous
devriez parvenir à faire baisser ce nombre… Soyez attentif à l’efficacité de vos programmes !
Exercice 3.8 La fonction primitive (expt a b) calcule ab. Intéressons-nous à sa programmation récursive
lorsque a ∈ R et b ∈ N. Exemple typique : 210 = 1024 [le « kilo » informatique].
a) Ecrivez une première version naïve ($expt a b) exprimant que ab = a * ab-1. Quel est l’ordre de grandeur du
nombre de multiplications effectuées lors du calcul de ab ? N’oubliez pas le $ quand vous simulez une primitive !
b) Il semble clair que pour calculer 250, vous n’allez pas faire une cinquantaine de multiplications [enfin, je l’espère
pour vous], mais vous allez remarquer que 250 = (225)2, puis que 225 = 2 * (212)2, etc. En utilisant cette stratégie
algorithmique [qui se nomme la « dichotomie » : couper en deux], écrivez une nouvelle version récursive
($expt—dicho a b). Quel est devenu l’ordre de grandeur du nombre de multiplications effectuées lors du calcul de
ab ? Est-ce plus ou moins efficace que la solution de a).
Exercice 3.9 optionnel ¶ [d’après un exo de CAPES Maths]. Eliminer la récursivité [trouver une formule
directe] pour la fonction : (define (foo n) (if (= n 1) 1 (+ 1 (foo (quotient n 2))))), avec n ∈ N.
Faculté des Sciences de Nice
L2 Maths 2007-08
Initiation à la programmation fonctionnelle
Feuille n° 4 : Qu’est-ce qu’une BOUCLE ?
L’itération comme cas particulier de récursivité
Une fonction est récursive si elle est programmée par récurrence, i.e. si elle s'utilise elle-même dans son propre texte. Il
s'agit donc d'une définition purement grammaticale, textuelle. Cette définition ne préjuge pas de ce qui va se passer lors de
l'application de la fonction, au niveau du processus de calcul enclenché dans l'ordinateur. Reprenons l'exemple de la
factorielle, et déroulons le calcul de 4! :
(define (fac n)
(if (zero ? n)
1
(* n (fac (-
n 1)))))
(fac 4) ==
==
==
==
==
==
==
==
==
(*
(*
(*
(*
(*
(*
(*
(*
24
4
4
4
4
4
4
4
4
(fac
(* 3
(* 3
(* 3
(* 3
(* 3
(* 3
6)
3))
(fac
(* 2
(* 2
(* 2
(* 2
2))
2)))
(fac 1))))
(* 1 (fac 0)))))
(* 1 1))))
1)))
(*)
Que s'est-il passé ? Lorsque SCHEME voit (* 4 (fac 3)) , il ne peut pas effectuer la multiplication par 4 avant d'avoir
calculé (fac 3), il va donc mettre en attente cette multiplication par 4 dans une pile [comme une pile d'assiettes, sauf qu'on
empile des opérations]. On peut visualiser cette pile qui monte et redescend sur le schéma de droite, sous la forme d'un
calcul qui s'expanse puis se contracte. Sur la ligne (*) du milieu, il y a 4 multiplications en attente sur la pile, et c'est la
dernière empilée [la multiplication par 1] qui sera la première à être effectuée et dépilée [retirée de la pile]. Les
informaticiens parlent de structure LIFO [Last In, First Out]. Cette nécessité d'une pile pour effectuer le calcul peut se voir
à la simple lecture du texte de la fonction fac ci-dessus. Regardez l'appel récursif [l'endroit où la fonction s'invoque ellemême] :
(* n (fac (- n 1)))
Après le calcul de (fac (- n 1)), il reste à effectuer quelque chose, ici la multiplication par n. On dit que l'appel récursif à
fac est enveloppé, l'enveloppe étant cette fameuse multiplication par n . L'existence d'une enveloppe est équivalente à la
nécessité d'une pile puisque l’enveloppe ne fait qu’exprimer un calcul en attente. On dit qu'il s'agit d'une « récurrence
enveloppée ». D'autres exemples sont fournis par interfac, ou binomial, ou somme-carres du TP3 telles que vous les
aviez sans doute programmées [localisez l’enveloppe]…
Et alors ? Quel rapport avec l’itération ?
Patience, ça vient. Il existe en effet un autre type de récurrence, pour laquelle l’appel récursif n’est pas enveloppé ! Nous
allons faire un peu d’algèbre de programme en raisonnant sur la factorielle récursive enveloppée ci-dessus. Généralisons
l’expression (* n (fac (- n 1)), qui calcule quelque chose multiplié par une factorielle, et introduisons la fonction
auxiliaire (aux p q) == (* p (fac q)).
• Remarquons tout d’abord que fac se définit à partir de aux : (fac n) == (aux 1 n)
• On se propose de trouver une définition intrinsèque de aux, donc ne faisant pas appel à fac. Pour cela, supposons n
« portons » le texte de fac dans la définition de aux :
(aux p q) ==
==
==
==
==
(* p (fac q))
(* p (if (zero? q) 1 (* q (fac (- q 1)))))
(if (zero? q) (* p 1) (* p (* q (fac (- q 1)))))
(if (zero? q) p (* (* p q) (fac (- q 1))))
(if (zero? q) p (aux (* p q) (- q 1)))
par distributivité de if
par associativité de *
et fac a été éliminée !
≠ 0
et
D’où finalement une autre programmation possible de la factorielle :
(define (fac n)
(aux 1 n))
avec :
(define (aux p q)
(if (zero? q)
p
(aux (* p q) (- q 1))))
Faire de l’algèbre, c’est bien et intellectuel, mais comprendre ce qui se passe est encore mieux. Remarquons d’ores et déjà
que l’appel récursif de aux n’est plus enveloppé [i f n’est pas une fonction mais une forme spéciale, donc pas une
enveloppe !], ce qui semble indiquer l’absence de calculs à mettre en attente, donc l’inutilité d’une pile pour effectuer le
calcul de 4! par exemple. Vérifions-le à la main :
(fac 4) ==
==
==
==
==
==
(aux
(aux
(aux
(aux
(aux
24
1 4)
4 3)
12 2)
24 1)
24 0)
Pas d’expansion/contraction !
Pas besoin de pile !
C’est une ITERATION !
Lorsque l’appel récursif n’est pas enveloppé, il est dit terminal. On dit
que la fonction est « récursive terminale », ou mieux « itérative ».
Si l’on emploie ce langage, c’est par référence à la notion d’itération en C, qui utiliserait pour cela une boucle while. Dans
la fonction (aux p q) précédente, p représente un « accumulateur » [il accumule les produits successifs pour aboutir au
résultat] et q un « compteur » [il s’agit en réalité de n ]. Il est usuel de nommer iter une itération et acc un accumulateur.
Enfin, on localise [on cache] la fonction iter à l’intérieur de fac, car personne d’autre que fac n’aura besoin de l’utiliser.
D’où la version propre finale avec un « define interne » :
(define (fac n)
; la factorielle de n, version itérative
(define (iter acc n)
; la « boucle » avec ses deux « variables de boucle
(if (zero? n)
; fini ?
acc
; oui : on rend l’accumulateur qui est plein
(iter (* acc n) (- n 1))))
; non : on itère, en mettant à jour acc et n
(iter 1 n))
; on lance la boucle en initialisant les variables de boucle acc et n
»
N.B. Vous voudrez peut-être démontrer par récurrence sur n que (iter acc n) calcule acc * n! et ceci fait , d’en déduire
que (fac n) calcule bien n!. Ceci se nomme une « preuve de programme ».
Exercice 4.1 Ecrire (interfac a b) et (nb-régions n) du TP3 sous la forme d’algorithmes itératifs.
Un exemple d'algorithme itératif : la méthode de Newton
Il s'agit d'une méthode de calcul d'une racine approchée r
de l'équation
f(x) = 0 . Elle consiste à partir d'une
approximation a de la racine et de converger vers cette
dernière. La convergence est assez rapide.
Elle procède de la manière suivante [cf. le schéma cicontre]. On trace la tangente au point d'abscisse a sur la
courbe [ce qui suppose que la dérivée ne s'annulle pas en
x=a ], qui recoupe l'axe Ox en une abscisse b qui est plus
près de la racine que a . On itère cette amélioration jusqu'à
ce qu'elle soit « assez bonne ». Pour cela, on se fixe une
précision ε [par exemple 10-4] et on considèrera que
l'approximation courante a est « assez bonne » si
f (a) < ε donc si f(a) est « presque » égal à 0…
€
Exercice 4.2 a) Rappel [cf exo 1.7] : on définit ainsi la fonction (D f) retournant la fonction dérivée approchée f’ :
(define (D f)
; ((D log) 2.5)
(let ((h 0.0001))
(lambda (x)
(/ (- (f (+ x h)) (f x)) h))))
pour calculer log’(2.5)
Le but est d’écrire la fonction (une-racine f a) retournant une valeur approchée d’une racine de f(x)=0 en partant de
l'approximation a. Plutôt que d’écrire plusieurs fonctions « à plat », vous allez directement localiser les sous-fonctions à
l’intérieur de la fonction principale une-racine, comme dans le squelette encadré ci-dessous. Vous noterez que f et eps
sont « visibles » dans les sous-fonctions, qui ne les ont donc pas en paramètres, tout en pouvant s’en servir librement !
(define (une-racine f a eps)
(define (assez-bonne? a)
...)
(define (améliore a)
...)
(define (iter a)
...)
(iter ...))
b) Ecrire la fonction (améliore a) retournant b comme amélioration de a . Il vous faudra calculer l’équation de la
tangente en x=a mais vous êtes en MP2 et savez donc faire ça les yeux fermés et en apnée… Utilisez a).
c) Ecrire le prédicat (assez-bonne? a) retournant #t si et seulement si l'approximation a est « assez bonne » pour la
précision choisie, c’est-à-dire vérifie ici |f(a)| < 10-4.
d) Ecrire la boucle (iter a) itérant l’amélioration jusqu’à ce que l’approximation a soit assez bonne.
e) Ecrire enfin le corps de la fonction une-racine, qui va passer la main à la boucle iter en initialisant a.
3
f) A.N. Calculez une valeur approchée de π, de 2 , puis de 2 , en résolvant des équations. Calculez le point fixe du
cosinus [i.e. la solution approchée de cos x = x], réponse = 0.739…
N.B. Vous trouverez un corrigé ainsi que des variations sur la méthode de Newton dans le livre d’Abelson & Sussman…
Entraînement à l’écriture de€boucles €
Exercice 4.3 Voyons si vous avez pris l’habitude de vous constituer des bibliothèques de fonctions ré-utilisables. Vous
avez programmé dans l’exercice 3.5 la fonction (int f a b δ). Vous vous êtes dit que cette fonction avait un intérêt en
soi, et vous l’avez bien placée toute seule dans un fichier int.scm sur votre clé USB ? Parfait, vous avez compris la
manoeuvre. Sinon, vous fonctionnez encore à l’ancienne et vous savez ce qui vous reste à faire…
Editez ce fichier int.scm et ajoutez à la fin une version itérative de (int f a b d). Adoptez un schéma localisé :
(define (int f a b delta)
(define (iter ...)
...)
(iter ...))
; le tout est de bien trouver les « variables de boucle »
Exercice 4.4 Calculer itérativement la somme des cubes des entiers multiples de 7 dans l’intervalle [1234, 5678].
Réponse : 37103506034140
Exercice 4.5 Calculer itérativement une valeur approchée du minimum m de la fonction
l’intervalle [0,2]. Réponse : -5.21...
f (x) = x 5 − x 2 − 6x + 1 sur
Exercice 4.6 Modifiez la fonction précédente pour que son résultat soit non pas le minimum m mais une valeur x telle
que f (x) = m . Autrement dit, un point sur lequel le minimum est atteint ! Réponse
: 1.13... La solution est ici unique,
€
mais pas en général. Vérifiez en traçant la fonction avec Maple [vous lancez Maple qui se trouve dans « Menu
Démarrer » et vous demandez :
plot(x^5-x^2-6*x+1,x=0..2) ;
€
Exercices complémentaires
On rappelle que ces exercices complémentaires ne seront pas supposés résolus lors des examens, contrairement aux
exercices obligatoires, mais leur résolution est plus que vivement conseillée pour une préparation optimale aux dits
examens ! N’hésitez pas à les travailler en groupes.
Exercice 4.7 Un matheux tient dans un bus le discours suivant : « Si on tire deux entiers naturels au hasard, la
6
probabilité qu’ils soient premiers entre eux est égale à 2 ».Programmez une expérience avec par exemple 10000
π
tirages aléatoires de deux entiers p et q permettant de vérifier si cette proposition est raisonnable. On rappelle que deux
entiers sont premiers entre eux s'ils n'ont pas de diviseurs communs, autrement dit si leur pgcd est égal à 1.
€
Exercice 4.8 Un diviseur d’un entier n ≥ 1 est un entier k ≥ 1 tel que (zero? (modulo n k)). Par exemple, les
diviseurs de 12 sont 1, 2, 3, 4, 6, 12.
a) Combien le nombre 123456 admet-il de diviseurs ? Réponse : 28, calcul itératif.
b) Un entier n est premier s’il est ≥ 2 et si son seul diviseur ≥ 2 est n lui-même. Déduire de a) le prédicat (premier?
n). L’entier 2003 est-il un nombre premier ? Réponse : oui. Et l’entier 2003317 ? Réponse : non.
c) L’inconvénient de la programmation de b) est de chercher le nombre total de diviseurs. Or, dès que l’on trouve un
diviseur, on devrait stopper et dire « le nombre n’est pas premier » ! Implémentez cette idée…
d) On peut facilement montrer [cours d’arithmétique, raisonnement par l’absurde] que si n n’admet aucun diviseur > 1
jusqu’à
€
n , alors il est premier. Profitez-en pour accélérer encore un peu la recherche du nombre de diviseurs.
Exercice 4.9 Une autre expérience de Monte-Carlo du même type que 4.7. Ecrivez une fonction (fléchette n)
chargée de calculer une approximation du nombre π avec l'expérience suivante. On considére un carré dans lequel est
inscrit un cercle. On jette n fois et de manière aléatoire une fléchette dans le carré et on compte le nombre de fois k
qu'elle tombe dans le cercle. La fonction (fléchette n) retourne comme résultat de l'expérience l'approximation de
π calculée à partir de n et de k. Utilisez un rapport d’aires…
Faculté des Sciences de Nice
L2 Maths 2007-08
Initiation à la programmation fonctionnelle
Feuille n° 5 : GE-NE-RA-LI-SEZ !
5.1 Généralisez vos fonctions en « passant à l'ordre supérieur » !…
Les mathématiciens ont commencé à démontrer des théorèmes de géométrie en dimension 2, puis en dimension 3,
puis… en dimension N. La généralisation étant une activité importante de la démarche scientifique, pourquoi ne pas s'en
servir en programmation ? Souvent, il vous arrivera d'écrire un algorithme adapté à un problème particulier et de vous
apercevoir qu'à peu de frais, il vous est possible d'en déduire un algorithme plus général qui pourra être ré-utilisé par la
suite dans d'autres situations. Le mot-clé est bien celui de ré-utilisation…
Autre intérêt de la généralisation : résoudre des problèmes particuliers. C'est peut-être paradoxal, mais il est souvent plus
facile [en programmation comme en maths] de résoudre des problèmes généraux que des problèmes particuliers !
Prenons l'exemple du calcul de la factorielle (fac n). Nous l'avions généralisé en (interfac a b ) . Et bien,
continuons cette généralisation. Pourquoi se borner à ne faire que des multiplications ? Remplaçons celle-ci par une
opération quelconque binaire associative op de « neutre » e :
(define (accumulation op e a b)
(if (> a b)
e
(op a (accumulation op e (+ a 1) b))))
> (accumulation * 1 10 20)
6704425728000
> (accumulation + 0 10 20)
165
; avec * de neutre 1 : le produit 10 × 11 × ... × 20
; avec + de neutre 0 : la somme 10 + 11 + ... + 20
Exercice 5.1 Donnez une définition de la factorielle (fac
n)
en utilisant cette fonction accumulation.
Exercice 5.2 a) Cette généralisation est insuffisante pour calculer une somme de carrés par exemple. Introduisez
une fonction f en paramètre qui sera appliquée à chaque élément de l’intervalle [a,b], et programmez la fonction plus
générale (accumulation op e f a b) :
> (accumulation + 0 sqr 10 20)
2585
; la somme 102 + 112 + ... + 202
b) Redéfinissez la factorielle (fac n) avec cette nouvelle version de la fonction accumulation.
c) Calculez en une ligne une valeur approchée de la constante e = 1.718… en exprimant le développement limité
1+1/1!+1/2!+1/3!+…+1/15!. Comparez avec les « vraies » décimales de e obtenues avec (exp 1).
d) Calculez le produit de tous les nombres impairs de [1,50]. Rép: 58435841445947272053455474390625.
• La question d) ci-dessus demandait une astuce. Or, pourquoi se limiter à aller de 1 en 1 ? Nous pourrions parcourir
l'intervalle [a,b] de 2 en 2, ou de 5 en 5, etc. Et d'ailleurs, pourquoi avec un pas constant ? Prenons plutôt une fonction
de pas (suiv a) qui calculera le suivant de a dans l'intervalle. En imaginant que l'on ait déjà programmé une version
(accumulation op e f a suiv b), et pour en revenir à la question d), la solution serait alors simplement :
(accumulation * 1 id 1 (lambda (x) (+ x 2)) 50)
N.B. i) Il existe bien une primitive add1 qui représente la fonction x  x+1 , mais pour passer une fonction sans nom
en argument, il faut bien entendu une lambda. Beaucoup d'étudiants ont tendance à oublier les lambda.
ii) Oui, bon, d'accord, la fonction x  x + 2 n'est autre en maths que la fonction composée add1 o add1 et donc en
Scheme (compose add1 add1), vous avez de la chance…
Exercice 5.3 a) Ecrire la nouvelle version plus générale (accumulation op e f a suiv b) . Bien voir que suiv
est une fonction, pas un nombre !
b) Redéfinir la factorielle (fac n) avec cette dernière version d'accumulation.
c) Calculez en une ligne une valeur approchée de la somme S = 1 - 1/2 + 1/3 - 1/4 + 1/5 - 1/6 + ... Oui,
vous savez sans doute démontrer en MP2 que cette série est bien convergente… Prenez 100 , 500 , 1000 , 5000 termes.
Devinez-vous la valeur exacte de la limite ?…
• Pas mal, n’est-ce pas ? Juste un dernier coup de généralisation. On sait d’où l’on part [de la borne gauche a] mais on
ne sait pas toujours en combien de coups on arrive au bout. Remplaçons donc la borne droite b par une fonction de test
qui dira si a est en train de sortir du domaine du calcul :
(define (accumulation op e f a suiv testfin)
(if (testfin a)
e
(op (f a) (accumulation op e f (suiv a) suiv testfin))))
Exercice 5.4 a) Reprogrammez une dernière fois la factorielle (fac n) avec cette version…
b) Programmez avec cette dernière version, en une seule ligne, la fonction (int f a b δ ) chargée de calculer une
valeur approchée de l’intégrale de f sur [a,b] à la Riemann, avec une subdivision de [a,b] en sous-intervalles de
largeur δ.
c) Recalculez de même la somme S de l'exercice précédent en exprimant cette fois que la sommation se poursuit
jusqu'au premier terme a vérifiant a < 0.001.
Définition : Une fonction est dite d'ordre supérieur si son domaine de départ ou d'arrivée contient des fonctions.
Exemples : les fonctions f  f(0), f  f’, x  [f  f(x)], accumulation.
Contre-exemple : la factorielle.
• La programmation à l’ordre supérieur, assez abstraite, fait donc abondamment usage de telles fonctions d’ordre
supérieur. Le fait de pouvoir passer des fonctions en paramètre produit des programmes très généraux et souvent très
courts1. Peu de langages de programmation [parmi eux Scheme et Maple] permettent une telle programmation. Il est
donc intéressant pour le programmeur, lorsqu’il sent qu’il peut généraliser un algorithme, d’en produire une version à
l’ordre supérieur et le mettre en bibliothèque pour l'exploiter plus tard. A condition qu’il l’ait suffisamment optimisé…
Par exemple ici, il serait intéressant de transformer accumulation, qui est récursive profonde, en itération, de manière à
économiser l’espace de pile lorsque le nombre d’éléments à traiter dépasse plusieurs centaines, ce qui est le cas de
l'intégrale par exemple.
Exercice 5.5 Ecrivez une version itérative de la fonction (accumulation op e f a suiv testfin). Il faut être
conscient que les calculs se font de la droite vers la gauche avec la version récursive profonde, et qu’ils se feront de la
gauche vers la droite avec la version itérative, ce qui peut poser des problèmes si la fonction n’est pas associative.
(define (accumulation op e f a suiv testfin)
(define (iter a acc)
; inutile de passer
...)
(iter a ...))
1
On parle même de one-liners : les programmes qui tiennent en une ligne !
op, e, f, suiv et testfin qui ne varient pas !
5.2 Sur les opérations de lecture et d’écriture…
Certains étudiants demandent régulièrement comment afficher des informations au cours du déroulement d'un algorithme.
En principe, on ne fait jamais ce genre de sport dans une fonction récursive2. Mais il peut être intéressant d'avoir des
informations sur certaines variables à un certain moment, ou de visualiser la convergence d'une itération. Ou bien pour
pallier au fait que l'on ne peut pas tracer une fonction interne ! Bref, voici l'outil adéquat printf [une version simplifiée
de celui de C], que vous êtes priés de ne pas utiliser à tort et à travers : une fonction est censée retourner un résultat, pas
effectuer des affichages [le toplevel fait ça très bien tout seul]… L’aide sur printf se trouve dans le Mémento du
Schemeur, section « Entrées-sorties à la console ».
> (printf "pi=~a et son log vaut ~a~n" pi (log pi))
pi=3.1415926535 et son log vaut 1.1447298858208
• Exemple d’un programme interactif [avec (read) comme cerise sur le gâteau] :
(define (programme)
(printf "Entrez un nombre : ")
(let ((x (read)))
(cond ((not (number? x)) (printf "~a n'est pas un nombre !~n" x) (programme))
((zero? x) (printf "Ce nombre est nul !~n"))
(else (printf "L'inverse de ~a est : ~a~n" x (/ 1.0 x))))))
N.B. i) L'appel récursif (programme) dans la première clause du cond est en position terminale : c'est une itération !
ii) Vous noterez la possibilité, dans un define, dans une lambda , dans un let , dans un cond , d’évaluer plusieurs
expressions en séquence [les unes à la suite des autres], ce que l’on fait en Pascal avec un begin...end et en Java avec
des {...}. En Scheme, il existe aussi la forme (begin e1 e2 … en) même si elle est dans la plupart des cas implicite ;
son résultat est celui de la dernière expression en , les expressions e1, e2, ..., en-1 n’étant évaluées que pour leurs
effets, par exemple d’affichage.
• La fonction (read), sans paramètre, lit au clavier un seul objet Scheme : un nombre, un symbole, une lambdaexpression, etc. Il est parfois utile de forcer l’évaluation de l’objet lu car (read) se contente de lire sans évaluer. Par
exemple [ce qui est lu au clavier est en gras et encadré, comme au toplevel d’ailleurs] :
> (read)
(+ 2 3)
;
c’est
moi
qui
rentre
l’expression
au
clavier
(+ 2 3)
et le résultat de la fonction (read) est bien (+
2 3)
et non pas 5. Par contre :
je
fournis
> ((eval (read)) 3)
(lambda
(x)
(+
x
10))
;
une
lambda
qui
va
être
évaluée
13
• Munis de ces précieux renseignements, vous êtes en état d’écrire des programmes comportant des dialogues avec
l’utilisateur. Soyez conscient que ceci n’est qu’une facilité et que la programmation fonctionnelle par elle-même se
concentre sur les algorithmes et n’a que faire des read et autre printf. Elle se focalise sur le fond, pas sur la forme…
Exercice 5.6 Ecrire un programme interactif qui demande à l’utilisateur une fonction à intégrer, ainsi que les
bornes a et b et le pas d’intégration δ et qui affiche une valeur numérique approchée de l’intégrale…
> (intégrale)
Quelle fonction voulez-vous intégrer : (lambda (x) (sin (* x x)))
Donnez la borne a : 0
Donnez la borne b : pi
Donnez le pas d’intégration : 0.01
L’intégrale approchée vaut 0.7711761181639067
2
Les outils de trace sont là pour ça…
Faculté des Sciences de Nice
L2 Maths 2007-08
Initiation à la programmation fonctionnelle
Feuille n° 6 : Doublets et données structurées
Jusqu’à présent nous ne nous sommes intéressés qu’à des données simples : les nombres. Il est temps d’aborder les
données structurées. Un matheux dirait : après les scalaires, le vectoriel !… Sauf qu’en Scheme [comme en
Maple], la donnée structurée de base n’est pas le vecteur ou le tableau de Java, mais la liste. L’avantage sur le tableau
est qu’elle est plus élastique et peut contenir n’importe quoi : des nombres, des symboles ou bien… d’autres listes !
6.1 Les symboles et la citation [la « quote »]
Jusqu'à présent, vous avez utilisé des nombres, des booléens, et des procédures :
> (number? (sqrt 2))
#t
> (boolean? (integer? pi))
#t
> (procedure? sqrt)
#t
Les mots de Scheme se nomment les symboles, par exemple le mot pi est un symbole dont la valeur est le nombre
inexact 3.1415926535. De même + est un symbole dont la valeur est la procédure1 d'addition. Un symbole peut donc
avoir une valeur, ou ne pas en avoir :
> pi
3.141592653589793
> sqrt
#<primitive:sqrt>
> bonjour
undefined identifier : bonjour
Il a cru que vous demandiez la valeur du symbole bonjour. L'accent aigu [on dit la "quote"] permet d'éviter une
évaluation intempestive, que ce soit d'un symbole ou d'une expression quelconque, pour la garder « telle quelle » :
> ’bonjour
bonjour
> ’(+ 1 2 3)
(+ 1 2 3)
> ’(if p q r)
(if p q r)
> (define prénom ’justin)
> prénom
justin
> (symbol? prénom)
; est-ce que la valeur de la variable prénom est un symbole ?
#t
> (symbol? pi)
; mais la valeur de pi n'est pas un symbole !
#f
> (symbol? ’pi)
; même si le mot pi est lui-même un symbole...
#t
N.B. Un symbole n'est pas une chaîne de caractères : il est insécable. Les ”chaînes de caractères” sont d'autres
objets Scheme, de type string, que nous n’utiliserons pas beaucoup, vous les avez déjà pratiquées en Java...
6.2 La structure de « doublet »
En Scheme, un couple mathématique (a,b) se construit avec la fonction cons, et les deux projections se nomment c a r
et cdr [pour des raisons historiques2]. Un couple [on dit un doublet] se reconnaît grâce au prédicat pair? [en anglais,
« un couple » se dit « a pair »...] :
> (define d (cons 1 ’one))
> (car d)
1
1
; notez la quote, essayez de ne pas la mettre pour voir !...
> (cdr d)
one
> (pair? d)
#t
d -->
1
one
car
cdr
Scheme utilise indistinctement les mots « procédure » et « fonction ». Un peu comme C pour lequel une procédure n’est qu’une fonction dont
le domaine d’arrivée est void…
2
Liées aux noms des registres de l’ordinateur IBM-704 qui en 1960 contenaient ces deux composantes. Le folklore en a conservé les noms…
6.3 Emboîtez les doublets !
boris ==
a
b
c
d
e
• Rien n'empêche en effet d'emboîter les doublets comme des poupées russes pour construire des données structurées.
Exercice 6.1 a) Définissez le doublet ci-dessus et nommez-le boris :
(define
boris
(cons
...))
b) Quel est le cdr du cdr du car de boris ? Vérifiez votre réponse au toplevel. Puis demandez-le plus simplement
sous la forme contractée cddar. Vous avez droit à l’abréviation c x x x x r avec x = a ou d [quatre x maximum, et à
condition que la composante existe !].
c) Comment accèderiez-vous à la composante d du doublet boris ? Et à c ou a ? Vérifiez au toplevel…
d) Qui est le cddar de boris ? Et son cdaar ?...
• Il est vite pénible d'emboîter trop de doublets. La convention consiste alors à sortir les doublets internes en les tirant
par une flèche, qui sera verticale pour les car et horizontale pour les cdr. Le point d’entrée lui, est noté verticalement :
boris
a
d
e
b
c
Exercice 6.2 Dessinez dans ce style « boîtes et pointeurs » le doublet défini ainsi :
(define natacha (cons (cons 1 (cons (cons 2 3) 4)) 5))
6.4 La convention d'affichage d’un doublet : le « point-parenthèse »...
Le doublet
boris
devrait normalement s'afficher avec des points comme ceci :
((a . (b . c)) . (d . e))
mais s'affichera en réalité :
((a b . c) d . e)
On efface chaque point immédiatement suivi d'une parenthèse
ouvrante, et l'on efface aussi la parenthèse fermante associée.
Exercice 6.3 Simplification que fait automatiquement le toplevel, vérifiez-le en demandant la valeur de boris et de
Vous noterez que tous les points ne partent pas, seulement ceux suivis d’une parenthèse ouvrante !
natacha…
Exercice 6.4 a) Dessinez le schéma de boîtes et pointeurs du doublet dont la représentation externe simplifiée est:
((a . b) c d (e ( f . g) . h) . i)
b) Réciproquement, quelle est la représentation externe
simplifiée du doublet mp2 représenté par l'architecture
ci-contre ?
j
g
a
d
b
c
e
f
h
i
6.5 Notion de Type Abstrait de Donnée : les « vecteurs 2D »
Idée : Scheme fournit certains « types de base » , comme les nombres, les symboles, les booléens, les doublets, les
procédures, etc. Mais il ne peut pas tout prévoir ! Vous pouvez avoir envie de nouveau types plus « abstraits » comme
des matrices, des polygones, ou des cartes à jouer... L’idée consiste à définir les opérations de base sur ces nouveaux
objets, en isolant dans ces opérations les détails de l’architecture choisie, puis en « oubliant » la manière dont ils sont
construits en se forçant à n’utiliser que des opérations abstraites. Un peu comme le mathématicien qui construit les
nombres réels puis s’empresse bien vite d’oublier la construction pour se focaliser sur les seules propriétés de ces
nombres... Vu ? Cette idé de « type abstrait » est TRES IMPORTANTE !
Exemple : pour écrire un jeu graphique, un programmeur a besoin d'une structure abstraite de vecteur à 2 dimensions. Il
choisit de représenter un vecteur par une structure à deux doublets dont le car contient un symbole comme *vec2D*
dénotant le type géométrique de l'objet dans le logiciel [un point, un vecteur, un segment, un cercle, un polygone, etc]
et dont le cdr contient [dans un autre doublet] les composantes orthonormées en x et en y de ce vecteur.
Schématiquement :
V
*vec2D*
champ de type
x
y
composante composante
en y
en x
1
3
Exercice 6.5 a) Définissez les vecteurs v1   et v 2   . Comment vont-ils s’afficher au toplevel ?
2
−4 
b) Définir le constructeur général de vecteurs (vec2D x y) retournant un vecteur de coordonnées (x,y) c’est-à-dire une
structure à 2 doublets comme ci-dessus.
c) Définir les fonctions (xcor v) et (ycor v) retournant les composantes en x ou y d'un vecteur v. On suppose dans
ce genre de fonctions que v est bien un vecteur et que la vérification a été faite en amont...
d) Définir un prédicat (vecteur? obj) retournant #t si l'objet obj est un vecteur au sens de la structure ci-dessus. Il
y a deux choix possibles : le choix « dur » où l’on teste toute la structure, et le choix plus « laxiste » où l’on se
contente de tester si le champ de type contient bien le symbole *vec2D* . Testez votre solution en prenant pour obj
d’abord un vrai vecteur [par exemple v1 construit ci-dessus], puis le nombre 2 : vous devez obtenir #t puis #f.
Dès lors, vous disposez d'un « type abstrait vecteur 2D » qui vous permet d'oublier cons , car , etc.
e) Définir l’addition vectorielle (vec+ v1 v2).
f) Définir la loi externe (ext* k v).
g) Définir le produit scalaire (prodscal v1 v2).
h) Définir la longueur d’un vecteur (norme v).
i) Définir le prédicat d’orthogonalité de deux vecteurs (ortho? v1 v2).
j) Définir la fonction (projection v1 v2) retournant le vecteur projeté orthogonal de v1 sur la droite portée par
Trouvez une formule de projection. Vérifiez en projetant le vecteur (2,0) sur la diagonale y=x.
k) Définir la fonction (rotation a) retournant la rotation vectorielle d'angle a.
Oui, le résultat est une fonction [cf. exercices 1.7c et 1.8]…
Vérifiez en appliquant sur le vecteur (1,0) la rotation d’angle π/2. Quel vecteur attendez-vous ?
v2 .
N.B. Lorsque l’on construit un type abstrait comme « vecteur 2D », il est usuel d’isoler dans un fichier séparé [par ex.
ici vec2D.scm] l’implémentation du type abstrait, c’est-à-dire la boîte à outils, toutes les fonctions qui sont au courant
de la représentation concrète d’un objet de ce type, ici celles qui utilisent cons, car, cdr. Dans un autre fichier, si l’on a
besoin d’utiliser le type abstrait en question, on demandera comme d’habitude son chargement par un load :
(load ”d:/scheme/vec2D.scm”)
; si la clé USB est sur le lecteur d:
et l’on s’interdira d’utiliser directement la représentation interne même si on la connaît ! Ce serait une faute (gasp…).
Faculté des Sciences de Nice
L2 Maths 2007-08
Initiation à la programmation fonctionnelle
Feuille n° 7 : La structure de « liste »
7.1 Définition d’une liste
Dans la feuille 6, vous avez travaillé avec des architectures [ou chaînages] de doublets. Il se trouve qu’il existe un
objet SCHEME particulier, nommé liste vide1 qui n'est pas un doublet, qui se représente par une croix et qui
s'affiche () . Il sert à « terminer » un chaînage de manière à éviter le point en avant-dernière position. Parmi les
deux doublets bob et bill ci-dessous, seul bill est une liste :
bob
bill
d
a
b
e
a
d
c
b c
> (pair? bob)
> (pair? bill)
#t
#t
> (list? bob)
> (list? bill)
#f
#t
> bob
> bill
(a (b . c) d . e)
(a (b . c) d)
Définition Dans tout ce qui suit, nous appellerons liste :
• ou bien la liste vide () qui est reconnue par le prédicat null?
• ou bien un chaînage de doublets dont le dernier cdr est vide. Le dernier cdr d'un chaînage de doublets est celui qui
se trouve complètement à droite et au plus haut lorsque l'on dessine le diagramme de « boîtes et pointeurs ».
Remarques : i) Le prédicat (pair? x) permettait de savoir si x est un doublet. Il existe de même un prédicat
(list? x) retournant #t si et seulement si x est une liste [cf. exercice 7.4.g].
ii) La liste vide symbolisée par une croix peut en fait se trouver en n’importe quel point d’un chaînage, comme le
montre l’exercice suivant.
Exercice 7.1 a) Dessinez la liste L = ((a b) c ((d) . e) () f). Définissez-la au toplevel. Quel en est le car
? le cdr ? le caddr ? le cadar ?
b) Entourez en rouge sur votre dessin l’endroit précis qui prouve que L est bien une liste.
c) Que pensez-vous de (a b . c (d e)) ? Est-ce une liste ?
d) Comment pourrait-on définir en SCHEME le prédicat null? s’il n’existait pas ?
☞
Le car d'une liste est son premier élément. Le cdr d'une liste est la liste privée de son car .
Une liste vide ne possède ni car ni cdr !
• En C, vous avez surtout programmé avec des tableaux : agrégats d'objets de même type, et dont le nombre est fixé
une fois pour toutes. En SCHEME, nous allons plutôt utiliser des listes : agrégats d'objets de types quelconques et
dont le nombre n'est pas fixé. Voici quelques exemples de listes :
1
On le prononce souvent nil par référence au latin nihil qui signifie rien. Il est analogue au pointeur nil de Pascal ou à la référence null de C.
Il a été introduit par le langage Lisp en 1958.
()
; la liste vide
(sol la si do)
; une liste à 4 éléments, tous des symboles
(sol la sol si)
; il peut y avoir des répétitions,
(sol (re 5) #t fa)
; et des objets de type quelconque, y-compris des listes !
(define (foo x) (+ x 1))
; un texte écrit en SCHEME est une liste !!!
> (sol la si re)
; Oups, j'ai oublié la quote…
ERROR : undefined identifier : sol
; et du coup, il croit que sol est une fonction !…
> ’(sol la si do)
> (list? ’(sol la si do))
(sol la si do)
> (car ’(sol la si do))
#t
; le 1er él.
> (cdr ’(sol la si do))
sol
; et le reste
(la si do)
> (null? ’(sol la si do))
; vide ?
> (null? ’())
#f
#t
Exercice 7.2 a) Quel est le cadr de la liste (sol la si do) ? Et le cddr ? Vérifiez au toplevel…
b) Quel est le cdr de la liste vide ? Vérifiez au toplevel…
c) Quel est le cdr de la liste (a) ? Vérifiez au toplevel…
7.2 Le principe de récurrence sur les listes
Dans les entiers naturels, on fait une hypothèse de récurrence HR sur n-1 et on prouve [ou on programme] sur n, le
cas de base étant n=0. Dans les listes, on fait une hypothèse de récurrence HR sur (cdr L) et on programme
sur L, le cas de base étant la liste vide (). Exemple : soit à programmer une fonction (longueur L) prenant une
liste L et retournant le nombre d'éléments de L [un élément comptant pour 1, même s'il s'agit lui-même d'une liste]:
(longueur ()) → 0
(longueur ’(sol (la 8) re do)) → 4
-
HR : Supposons connue (longueur
-
Cas de base : si L est vide, elle ne contient aucun élément, sa longueur est égale à 0.
(cdr L)).
Quelle est la longueur de L ? Facile, c'est 1 de plus…
(define (longueur L)
(if (null? L)
0
(+ 1 (longueur (cdr L)))))
Exercice 7.3 Programmez (longueur L) avec un algorithme itératif.
• En réalité, cette fonction est une primitive, qui se nomme length. Il existe une dizaine de primitives importantes
sur les listes à bien connaître pour programmer. Afin de les apprendre, vous allez les programmer vous-même en
SCHEME ! Ceci dit, afin de ne pas « tuer » les primitives, vous mettrez un dollar % devant leur nom. Par exemple,
vous auriez défini %length au lieu de length…
Exercice 7.4 Programmez les fonctions suivantes [qui sont des primitives, n'oubliez pas le $]. Vous avez le
choix entre une récurrence enveloppée et une itération. Tâchez quand même d'être sûr de savoir faire les deux un
jour de partiel ou d’examen [sachant que l’itération est en général un peu plus efficace et un peu plus
compliquée]…
a) La très utilisée fonction (iota n x) prenant un entier n ≥ 0 et un nombre x quelconque, et retournant la
liste de longueur n commençant par x et dont chaque élément vaut 1 de plus que le précédent :
(iota 7 -2) → (-2 –1 0 1 2 3 4)
(iota 0 5) → ()
(iota 3 4+5i) → (4+5i 5+5i 6+5i)
b) La fonction (append L1 L2) qui concatène deux listes L1 et L2 :
(append ’(do re mi) ’(fa sol si la re)) → (do re mi fa sol si la re)
c) Montrez que le nombre de doublets consommés durant l'exécution de (append L1 L2) avec l’algorithme
ci-dessus [i.e. le nombre total d'appels à cons] est égal à la longueur de L1 et indépendant de L2. On exprime ce
résultat en disant que le « coût » [ou la « complexité »] de (append L1 L2) est d’ordre length (L1).
d) La fonction (reverse L) retournant une copie inversée de la liste L . Attention, l’inversion se fait « au
premier niveau » : on ne plonge pas dans les éléments qui seraient eux-mêmes des listes :
(reverse ’(do re (mi fa) sol)) → (sol (mi fa) re do)
e) Si votre algorithme reverse ci-dessus était récursif enveloppé, produisez une version itérative, et
réciproquement. Dans chaque cas, quelle est l’ordre de grandeur du nombre total d’appels à cons de l’algorithme
obtenu ? Si n est la longueur de L, vous devez en trouver un d’ordre n et l’autre d’ordre n2.
f) La fonction (list-ref
L k) retournant l'élément numéro k de la liste L. Attention, comme en JAVA, le
premier élément [c’est-à-dire le car] a pour numéro 0 :
(list-ref (reverse (iota 10 5)) 3) → 11
Dans une liste de 1000 éléments, « ça coûte 500 » d’accéder au 500ème élément !
Une liste n’est pas un tableau, on la traite plutôt en séquence de gauche à droite !…
en conséquence de quoi on s’afforcera de ne PAS utiliser list-ref si possible [pas plus qu’append d’ailleurs]…
g) Et enfin le prédicat (list? obj), retournant #t si et seulement si l'objet SCHEME obj est bien une liste
[regardez à nouveau la définition d'une liste au §1 et tâchez de respecter exactement cette définition !].
7.3 Résumé sur la construction d’une liste
Dans tout ce qui suit, le type Elément est générique, il signifie : n’importe quel objet SCHEME.
• La manière la plus fréquente de construire une liste consiste à utiliser la fonction (cons x L) qui retourne un
nouveau doublet dont le car est x et le dont le cdr est L :
cons : Elément × Liste  Liste
N.B. Beaucoup d’étudiants croient pouvoir inverser les deux arguments et écrire (cons L x) pour rajouter un
élément x en queue de la liste L. Ils ont bien tort, vérifiez-le sur un exemple au toplevel !…
• On peut aussi construire une liste comme concaténation de plusieurs listes existantes avec (append L1 … Ln) :
append : Liste × … × Liste  Liste
• On peut enfin, comme les matheux et leurs fameux ensembles {x1,…xn} , définir une liste « en extension » en
donnant explicitement tous ses éléments, sauf que contrairement aux maths, l’ordre a de l’importance [une liste n’est
pas un ensemble, d’ailleurs il peut y avoir des répétitions]. On utilise pour cela la primitive (list x1 ... xn)
qui prend autant d’arguments que l’on veut :
list : Elément × Elément × … × Elément  Liste
En fait, elle se contente de faire une suite d’appels à cons. Par exemple :
(list x y z) == (cons x (cons y (cons z ’())))
Essayez-la au toplevel pour construire par exemple la liste (do (re mi) fa sol).
• Inversement, les fonctions car et c d r sur les listes [elles sont plus généralement définies sur les doublets
quelconques] permettent de « déconstruire » une liste, elles jouent le rôle d’accesseurs aux composantes :
car : Doublet  Elément
et ListeNonVide  Elément
cdr : Doublet  Elément
et ListeNonVide  Liste
Ce sont des projections, au sens où par exemple : (car (cons a b)) == a
Bibliographie :
• L'aide en ligne de DrScheme : Mémento du Schemeur  Fonctions primitives sur les listes…
• Livres : Chazarain pages 27+, Abelson & Sussman, pages 79-85, Arditi & Ducasse, chap. 2
Exercices complémentaires
Exercice 7.5 Ecrire une fonction (Lrandom long n) retournant une liste de longueur long, contenant des entiers
aléatoires de [0,n-1]. Exemple :
(Lrandom 5 10) → (8 5 8 2 0)
Exercice 7.6 En vous appuyant uniquement sur les textes des fonctions length et append que vous avez
programmées dans l’exercice 7.4, PROUVEZ PAR RECURRENCE que :
- la liste vide () est élément neutre à droite pour append, donc que : (append L ()) == L. Notez que votre
programme dit simplement que () est élément neutre à gauche !
- l’on a le théorème suivant : (length (append L1 L2)) == (+ (length L1) (length L2))
Vous voyez donc que la programmation fonctionnelle considère que les programmes eux-mêmes sont des objets sur
lesquels on peut raisonner. Quoi de plus naturel que de vouloir prouver mathématiquement qu’un programme
possède telle ou telle propriété, par exemple avant d’appuyer sur le bouton d’un réacteur nucléaire  …
Téléchargement