Introduction `a la programmation fonctionnelle par Scheme

publicité
Introduction à
la programmation fonctionnelle
par Scheme
Année 1993-1994
Jean-Jacques Girardot
[email protected]
Octobre 1993
Ecole Nationale Supérieure des Mines de Saint-Etienne
158 Cours Fauriel
42023 Saint-Etienne Cédex
2
Document de travail. Release 0.34 (c) J.J.Girardot, 1993.
Date d’impression 20 décembre 1993
Preface
0.1
Objectifs
Ce cours a pour but de présenter une autre facette de l’activité de programmation, celle des langages applicatifs. Pour celà, certains des concepts de base de la programmation, éventuellement déja vus par l’étudiant dans
le cadre d’un langage impératif, sont repris sous un angle nouveau. Cependant, la programmation fonctionnelle
couvre un champ d’application relativement distinct de celui de la programmation impérative, et nous verrons
que, même sur des problèmes similaires, les approches sont relativement différentes.
0.2
Plan de l’ouvrage
Le chapitre 1 effectue un tour sommaire de la programmation fonctionnelle, du lambda calcul jusqu’au
langage Haskell.
Le chapitre 2 est le chapitre introductif à Scheme. Y sont abordés la syntaxe et la sémantique du langage,
ainsi que les aspects numériques.
Le chapitre 3 introduit les aspects de manipulations symboliques, ainsi que la structure de base, la liste.
Le chapitre 4 présente les aspects fonctionnels de Scheme : qu’est-ce qu’une fonction, que peut-on faire avec
les fonctions.
Le chapitre 5 propose une analyse, en terme de complexité, des caractéristiques du langage.
Le chapitre 6 introduit la notion de macro-définition, ainsi que certains aspects non fonctionnels du langage :
manipulation de listes, etc.
Le chapitre 7 aborde un aspect particulier de la programmation fonctionnelle : l’évaluation retardée, grâce
aux promesses et aux flots.
Le chapitre 8 revient sur les structures de contrôles, en introduisant la notion de continuation : création de
continuation, passage explicite de continuation, primitives correspondante du langage.
Le chapitre 9 propose une vision d’une extension classique de Scheme : l’introduction d’objets.
Bibliographie, index, table des matières.
0.3
Matériaux
0.3.1
Le langage
Le langage de programmation particulier utilisé commme support de la pensée dans un cours d’informatique
ne devrait jouer qu’un rôle très secondaire vis à vis des concepts enseignés. Ce n’est malheureusement presque
jamais le cas, et, en particulier dans le cas des “initiations” à l’informatique, ce choix du langage joue un
rôle prépondérant. Une mode actuelle consiste à débuter les enseignements par un cours de programmation
dit “fonctionnelle”. Plusieurs langages répondent à ce critère : les langages de la famille ML comme CAML
([Mau91], [HV92]), Haskell ([HJe92], [HF92]), Miranda ([Tur86]), Gofer, etc.
Nous avons choisi Scheme, qui est plus ancien mais plus répandu, pour lequel la littérature est beaucoup
plus importante, et surtout qui a fait depuis longtemps ses preuves dans le domaine de la pédagogie, avec en
particulier le très réputé Structure and Interpretation of Computer Programs, d’Abelson et Sussman ([ASS85],
et dans sa traduction française, [ASS89]). Citons encore, entre autres ouvrages consacrés à Scheme : [Dyb87],
[SF92], ou [pF86].
A l’heure actuelle, un très grand nombre d’universités américaines, dont les plus prestigieuses, une centaine
d’universités de par le monde, dont plus d’une vingtaine en France, proposent un ou plusieurs enseignements
reposant sur l’utilisation de Scheme. Cet engouement n’est pas sans raisons. Scheme a en effet peu à peu acquis,
au sein des établissements d’enseignement, la réputation d’un bon langage pour la présentation de la plupart
3
4
des concepts de l’informatique (cf. par exemple [FWH89]). Le curriculum de l’Université d’Indiana est éloquent
à ce sujet. Scheme y est utilisé, comme unique langage de programmation, dans les cours suivants :
– Sémantique et implantation des langages de programmation.
– Concepts avancés des langages de programmation (langages data-flow et acteurs en particulier).
– Operating Systems (cours de base et cours avancé).
– Compilation (cours de base et cours avancé).
– Sémantique dénotationnelle.
– Intelligence artificielle.
(et il peut être choisi par les étudiants pour la remise de projets informatiques dans la plupart des autres
matières).
0.3.2
L’implantation
Le système Scheme utilisé comme support des exemples est MIT-Scheme. Ce système est disponible en
freeware, et peut être obtenu par ftp sur la machine altdorf.ai.mit.edu, dans le répertoire pub/scheme-7.1 .
Différents portages ont été réalisés pour plusieurs modèles de machines.
Les exemples ont été exécutés sur Sun SPARC. Cependant, Scheme est un dialecte relativement bien spécifié,
et il existe de nombreux systèmes du domaine publique pour quasiment tous les types de machines.
Le lecteur disposant d’un compatible PC peut utiliser PCScheme. Il s’agit d’une ancienne implantation
commerciale du langage, due à Texas Instruments, aujourd’hui maintenue et enrichie par Laurent Bartholdi et
Marc Vuilleumier, de l’université de Genève. On les applaudit très fort.
Sur Macintosh, on utilisera par exemple MacGambit , réalisée par Marc Feeley et Doug Currie, à l’université
de Montréal.
0.3.3
Le cours
Le support de cours (ce document) contient des chapitres correspondant, en général, aux objectifs énoncés
ci-dessus. Certains aspects resteront non dits - le lecteur intéressé se reportera à ce document, ou aux documents
cités en référence.
Ces notes de cours ne sont pas destinées à publication. Elles ne se veulent ni exhaustives ni originales, au
contraire, de nombreuses parties ont été snarfées 1 ici et là. Elles ne couvrent (et c’est heureux) ni l’ensemble des
aspects de la programmation fonctionnelle, ni l’ensemble des caractéristiques du langage Scheme. De multiples
références sont données çà et là, auxquelles le lecteur curieux pourra se reporter.
0.3.4
Autres documents
D’autres documents sont également rendus accessibles aux élèves :
– Manuel de Référence de MIT-Scheme [Han91b].
– Manuel de l’Utilisateur de MIT-Scheme [Han91c].
– Norme du langage, dite R4RS [R4R90].
– Textes des TPS et documents complémentaires distribués à l’occasion.
– Quelques ouvrages consacrés à Scheme et à Lisp sont disponibles (ou ne le sont plus) en bibliothèque.
0.3.5
Où?
Ou se procurer des informations supplémentaires sur Scheme et Lisp?
Une source inépuisable (et chaque mois renouvelée) d’informations est constituée par les FAQ2 LISP et
Scheme, accessibles par ftp sur ftp.think.com, dans le répertoire /public/think/lisp.
Enfin, certains documents, et certaines des implantations, sont accessibles par ftp à l’Ecole des Mines de
Saint-Etienne, sur la machine ftp.emse.fr , dans le répertoire /pub/scheme.
1 snarfer : de to snarf , s’emparer d’un texte, d’un fichier, d’une citation, etc, pour la réutiliser, avec ou sans l’accord de son
légitime propriétaire ; expression snarfée de [ASS85].
2 Frequently
Asked Questions.
Chapitre 1
La programmation Fonctionnelle
Ce chapitre présente la programmation fonctionnelle : en quoi celle-ci se distingue-t-elle de la programmation
traditionnelle, ou impérative, quelles sont ses origines, ses possibilités et ses limites, bref toutes ces sortes de
choses. Un tour complet de la question est fait dans [Hud89] dont ce résumé est très largement inspiré.
1.1
Introduction
La famille des langages de programmation fonctionnelle, ou applicative, a suscité beaucoup d’intérêt depuis
une quinzaine d’années.
Que nous apporte la programmation fonctionnelle ? Les programmes fonctionnels, nous dit-on, sont plus
concis, plus rapidement écrits, de plus haut niveau. Ils se prêtent bien à l’analyse de leurs propriétés, sont,
comparativement, plus facile à prouver que les programmes impératifs équivalents, et présentent plus de potentialités que ceux-ci lorsqu’il s’agit de les compiler efficacement pour certaines classes d’architectures, telles les
machines parallèles.
1.2
Qu’est-ce que la programmation fonctionnelle ?
La programmation fonctionnelle est un style de programmation, dans lequel l’évaluation s’effectue uniquement (ou essentiellement) par des applications de fonctions. Le but de la programmation fonctionnelle est en
fait de proposer un paradigme de programmation, qui soit aussi proche que possible du modèle mathématique.
Une description rapide des principaux paradigmes de programmation permet de mieux cerner ce qu’est
effectivement la programmation fonctionnelle.
1.2.1
Analyse des paradigmes de programmation
Emmanuel Saint-James [SJ91] classe les langages de programmation en trois familles principales :
programmation impérative : le calcul s’exprime par des modification de l’état de la machine. Parmi les
langages de programmation supportant essentiellement ce modèle, citons Ada, Basic, C, Fortran, Pascal
et bien d’autres.
programmation applicative : le calcul se réalise par la définition et l’application de fonctions. Les langages
de cette famille sont relativement récents (années 80) et assez peu connus. Citons ML, Haskell, Miranda.
programmation déclarative : le calcul se définit par l’expression des propriétés du résultat attendu. On peut
placer dans cette catégorie aussi bien Prolog que SQL, bien qu’ils aient très peu de traits en commun.
Dans la pratique, les limites peuvent être floues ; certains langages vont emprunter des traits à plusieurs de ces
catégories. Il existe des langages fonctionnels “purs”, tels Miranda ou Haskell, alors que d’autres vont conserver
des aspects impératifs (ML).
1.2.1.1
Un bref rappel historique
Les premiers langages de programmation ont été conçus dans l’optique d’une utilisation effective de l’ordinateur, dont le coût de fonctionnement était alors excessivement élevé. Ces langages reflètent donc, plus ou
moins fidèlement, la machine sous-jacente. Ils se composent essentiellement d’ordres destinés à cette machine :
mettre telle donnée à tel endroit, continuer l’exécution à tel autre endroit, etc. Cependant, ce qui semblait
5
6
CHAPITRE 1. LA PROGRAMMATION FONCTIONNELLE
aller de soi dans les années 50 ne semble plus aussi évident aujourd’hui. Le coût du matériel n’ayant cessé de
baisser, et celui du logiciel d’augmenter, on en est progressivement arrivé à la conclusion qu’il importait de
proposer des langages de programmation simples pour l’homme, plutôt que pour la machine. Les langages de
programmation sont devenus “de haut niveau”. Après le style impératif des années 50, on a vu apparaı̂tre de
nouveaux paradigmes de programmation : programmation logique, fonctionnelle, par objets, par contraintes...
1.2.1.2
Programmation impérative
Ce paradigme de programmation s’appuie sur le modèle conceptuel d’un ordinateur composé d’une mémoire
et d’une unité de traitement. La mémoire contient la représentation codifiée d’un algorithme, et l’unité de
traitement exécute une séquence de commandes qui modifient l’état de la mémoire. Cette architecture correspond
à la machine de von Neumann. La programmation impérative est donc caractérisée par la présence, dans le
langage, de constructions destinées à modifier l’état du calculateur : les variables, le pointeur d’instructions, la
pile, etc. Ces langages reposent sur la notion de séquencement. Voici un exemple d’un tel programme :
n := x;
a := 1;
while n>0 do
begin a := a*n;
n := n-1;
end;
Cet exemple fait appel au séquencement d’instructions, à l’affectation, notée ici :=, et, implicitement, au
branchement par l’instruction while. La variable x est une entrée, et le programme opère par modifications
successives des variables n et a. Après exécution, la variable a contient le résultat du calcul, ici la factorielle du
nombre naturel x.
La programmation impérative impose donc, à chaque étape de l’écriture d’un algorithme, de déterminer
quelles valeurs sont nécessaires pour le calcul, d’associer un emplacement dans la mémoire à ces données, et de
décrire, étape par étape, les transformations à appliquer à la mémoire, afin que l’état final de celle-ci contienne
les résultats (corrects!) de l’algorithme.
1.2.1.3
Autres paradigmes de programmation
Par opposition à ce modèle de programmation impérative, d’autres styles de programmation utilisent des
modèles conceptuels beaucoup plus éloignés de la machine de von Neumann. Le langages déclaratifs n’ont pas
d’état implicite. La programmation s’effectue par expressions ou par termes. L’état du calculateur est indiqué
explicitement, les itérations sont réalisées par des applications récursives. L’exemple du factoriel s’écrit ainsi
dans le langage fonctionnel Haskell [HJe92] :
fact x 1
where fact n a =
if n>0 then fact (n-1) (a*n)
else a
Le problème est résolu de manière effective par l’application de la fonction fact sur x et 1, dans un contexte
(“where”) où la fonction fact possède une certaine définition. On pourrait le paraphraser sous la forme “calculer
f act(x, 1), en sachant que f act(n, a) vaut f act(n − 1, a × n) si n est positif, et a si n est nul.” Le programme
exprime par la récursion 1 (dans laquelle l’état du calculateur est entièrement représenté par les valeurs des
paramètres) la structure itérative du premier algorithme.
La programmation fonctionnelle est donc un modèle de programmation s’appuyant sur les notions de fonction
et d’application de fonction. (Voir par exemple [Hud89], [Rea89] ou [Mic89].) C’est ce modèle que nous allons
présenter par l’intermédiaire du langage Scheme.
1.3
Historique de la Programmation Fonctionnelle
Le document relatif à la programmation fonctionnelle le plus volontiers cité est certainement Can Programming be liberated from the von Neumann style? A functional style and its algebra of programs, de John Backus
[Bac78], transcription du discours prononcé par celui-ci lorsqu’il reçu son Turing Award 2 en 1978.
1 c’est
2 Sorte
à dire l’utilisation, dans la définition d’une fonction, de cette fonction elle-même.
de prix Nobel qui n’intéresserait que les informaticiens.
1.3. HISTORIQUE DE LA PROGRAMMATION FONCTIONNELLE
7
Backus présentait un langage fonctionnel, FP , proposant un petit nombre de combinateurs offrant une
puissance d’expression suffisante pour la plupart des applications.
Même si le langage FP n’a pas considérablement influencé l’histoire de la programmation fonctionnelle,
l’article de Backus a eu un impact énorme sur le monde de la recherche.
Dans ce texte, Backus exposait en effet pourquoi la programmation impérative était “mauvaise”, et pourquoi
la programmation fonctionnelle était “bonne”,3 l’une des difficultés de la programmation traditionnelle étant
le recours constant aux effets de bord : un programme impératif ne peut se comprendre que par une analyse
séquentielle de son déroulement, la présence d’affectations et de branchements interdisant pratiquement toute
analyse formelle du texte d’un programme.
L’un des concepts importants mis en valeur par Backus est celui de transparence référencielle. Dans un
langage purement fonctionnel, il y a équivalence formelle entre expressions du type :
x + x where x = f a
⇔
(f a) + (f a)
Ce n’est naturellement pas le cas dans un langage impératif, où les deux expressions peuvent avoir des valeurs
différentes si la fonction f a des effets de bord. C’est cette transparence référencielle qui va permettre le
raisonnement équationnel sur les programmes, la preuve de ceux-ci, leur optimisation, leur fonctionnement sur
des machines parallèles, etc.
1.3.1
Le Lambda Calcul
Il est difficile de parler de la programmation fonctionnelle sans dire quelques mots du lambda calcul , travail
fondamental d’Alonzo Church [Chu41], qui fut une source d’inspiration pour la plupart des langages fonctionnels.
1.3.1.1
Syntaxe
Le lambda calcul fut conçu dans les années 30, non comme un langage de programmation, mais comme un
modèle pour analyser la calculabilité dans un système mathématique basé sur l’application fonctionnelle.4
La syntaxe des expressions du lambda calcul est très simple :
Identificateurs : x ∈ Id
Lambda expressions : e ∈ Exp
où e ::= x | e1 e2 | λx.e
Les expressions de la forme λx.e sont dites abstractions. Les expressions de la forme e1 e2 , ou encore, en utilisant
des parenthèses pour grouper les termes, (e1 e2 ) sont dites applications. Intuitivement, une expression telle que :
λx. · · · x · · · x · ··
représente une fonction de la variable x. Par exemple, λx.x est la fonction identité. λp.q est une fonction qui
fournit q, quel que soit le paramètre p. De même, λu.(λv.v) est une fonction qui, quel que soit le paramètre,
fournit la fonction identité.
Remarque
Les parenthèses, qui servent à regroupper les termes, sont parfois omises dans l’écriture des expressions. On
adopte alors la convention suivante :
(((( · · · ((e1 e2 ) e3 ) · ·· ) en )
e1 e2 e3 · · · en ≡
λp.λq.λr.λs.e ≡
λp.(λq.(λr.(λs.e)))
1.3.1.2
Variable liée
Le choix de x dans l’expression d’une fonction telle que λx.x est arbitraire, et une fonction équivalente peut
se noter λz.z ou λtoto.toto. La variable x (ou z, ou toto) est dite variable d’abstraction. Elle est dite liée dans
l’expression λx.x.
3 Assez ironiquement, ce Turing Award était précisément décerné à Backus pour son travail de développement du langage
FORTRAN.
4 Tout
comme d’autres systèmes sont basés sur la notion d’ensemble.
8
CHAPITRE 1. LA PROGRAMMATION FONCTIONNELLE
1.3.1.3
Variable libre
Une variable qui n’est pas liée dans une lambda expression est dite libre. Les règles suivantes permettent de
déterminer les variables libres (f v pour free variables) d’une lambda expression.
f v(x) = {x}
f v(e1 e2 ) = f v(e1 ) ∪ f v(e2 )
f v(λx.e) = f v(e) − {x}
1.3.1.4
L’opération de substitution
Les règles de réécriture du lambda calcul font appel à la notion de substitution :
la notation :
[e1 /x]e2
(qui se lit e1 substitué à x dans e2 ), indique que toutes les occurences libres de l’identificateur x sont remplacées
par l’expression e1 dans e2 . Cette opération de substitution, bien qu’intuitivement simple, nécessite quelques
précautions lorsqu’il s’agit de déterminer quelles variables sont libres dans une expression ; il peut par exemple
être nécessaire de renommer certaines variables liées de l’expression.
Ainsi :
[y/x](((λy.x)(λx.x))x) ≡ ((λz.y)(λx.x))y
Substituer y à x dans λy.x (qui représente une fonction qui, quel que soit son paramètre, fournit x) produirait la
fonction λx.x, fonction identité, dont la sémantique est bien différente. Pour pouvoir procéder à la substitution,
on a d’abord transformé λy.x en λz.x, fonction équivalente, puis seulement remplacé x par y. Les règles formelles
de l’opération sont les suivantes :
1. L’expression est une variable5 :
[e/xi ]xj =
!
e,
si i = j
xj , si i '= j
2. L’expression est une application :
[e1 /x](e2 e3 ) = ([e1 /x]e2 )([e1 /x]e3 )
3. L’expression est une abstraction :
[e1 /xi ](λxj .e2 ) =
1.3.1.5
Règles de réécriture

λxj .e2 ,








 λxj .[e1 /xi ]e2 ,


λxk .[e1 /xi ]([xk /xj ]e2 ),







si i = j
si i '= j et xj ∈
/ f v(e1 )
sinon ,
avec k '= i, k '= j,
et xk ∈
/ f v(e1 ) ∪ f v(e2 )
Il est possible de définir diverses règles de réécritures, dites conversions, qui préservent la sémantique les
expressions. Les trois suivantes fournissent un système mathématique cohérent :
1. α-conversion, ou renommage :
λx.e ⇔ λy.[y/x]e si y ∈
/ f v(e).
Cette règle implique donc (ce que l’on avait compris intuitivement) l’équivalence de fonctions telles que
λx.(x x) et λz.(z z).
2. β-conversion, ou application :
(λx.e1 )e2 ⇔ [e2 /x]e1
3. η-conversion :
λx.(e x) ⇔ e si x ∈
/ f v(e).
5 On imagine que les identificateurs sont dénotables par un indice : x , x , etc., et l’on dira que x et x représentent le même
i
j
i
j
identificateur si i = j, et sont différents sinon.
1.3. HISTORIQUE DE LA PROGRAMMATION FONCTIONNELLE
9
(On utilise ici une autre propriété que l’on ne démontrera pas :
si, V p, (f1 p) ⇔ (f2 p) alors f1 ⇔ f2
Dans notre cas, on peut vérifier que les deux expressions e et λx.(e x) satisfont cette propriété.)
On peut définir également la notion de réduction, correspondant aux β et η-conversions utilisées à sens
unique :
1. β-réduction :
(λx.e1 )e2 ⇒ [e2 /x]e1
2. η-réduction :
λx.(e x) ⇒ e si x ∈
/ f v(e).
1.3.1.6
Ordre de réduction
Si plusieurs conversions (ou réductions) sont simultanément applicables à une lambda expression, on peut
définir des stratégies d’application de ces conversions :
– On parle d’ordre normal lorsque l’on applique une conversion à la sous expression qui débute le plus à
gauche.
– On parle d’ordre applicatif lorsque l’on applique une conversion à la sous expression la plus interne à
gauche.6
Voici un exemple de réduction d’une lambda expression selon ces deux modes :7
Ordre normal :
(λx.x + 2)(2 ∗ 3)
(2 ∗ 3) + 2
6+2
8
Ordre applicatif :
(λx.x + 2)(2 ∗ 3)
(λx.x + 2)6
6+2
8
1.3.1.7
Forme normale
Une lambda expression est sous une forme normale lorsqu’on ne peut plus lui appliquer de réductions8 .
Certaines lambda expressions ne peuvent être mises sous forme normale :
(λx.(x x))(λx.(x x))
1.3.1.8
Théorèmes de Church-Rosser
Th1: si on peut passer d’une expression e1 à une expression e2 par une suite quelconque de conversions, alors
il existe une expression e3 telle que l’on puisse passer de e1 à e3 , et de e2 à e3 par des réductions. Un corrolaire
de ce théorème est que, si la forme normale d’une expression existe, elle est unique.
Th2: si la forme normale d’une expression existe, on peut l’obtenir par des réductions en ordre normal.
6 Les langages de programmation traditionnels pratiquent l’évaluation des expressions en ordre applicatif . Concrètement, ceci
signifie que lorsque le paramètre d’une fonction est une expression, celle-ci est d’abord évaluée, et c’est son résultat qui est transmis
à la fonction : c’est l’appel par valeur . Une évaluation en ordre normal impliquerait que l’expression ne soit pas évaluée, mais
remplace textuellement dans le corps de la fonction toute occurence du paramètre formel correspondant : c’est l’appel par nom. Le
langage Algol 60 fut [parmi les langages à grande audience et à vocation universelle] le seul à proposer les deux modes d’évaluations
pour l’appel des sous-programmes : le mode par défaut est l’ordre normal ; le mode applicatif s’obtient en associant le mot-clé value
à la variable formelle correspondante.
7 On s’éloigne ici quelque peu du lambda calcul en utilisant des constantes, des fonctions arithmétiques et des expressions infixes,
afin de simplifier l’exemple.
8 Réduction signifiant ici β-réduction ou η-réduction ; des α-conversions peuvent naturellement être appliquées aux variables liées
d’une forme normale sans changer la sémantique de celle-ci.
10
CHAPITRE 1. LA PROGRAMMATION FONCTIONNELLE
Ainsi, il est possible d’obtenir la forme normale, 1, de l’expression suivante par des réductions en ordre
normal, mais non par des réductions en ordre applicatif :
(λu.1) [(λx.(x x))(λx.(x x))]
1.3.1.9
Théorème du point fixe
Toute lambda expression e a un point fixe e! tel que (e e! ) ⇔ e! .
La démonstration consiste à considérer le combinateur Y ainsi défini :
Y
≡ λf.(λx.f (x x))(λx.f (x x))
et à vérifier que (Y e) ⇔ e(Y e), montrant que (Y e) construit effectivement le point fixe de e.
Ce résultat est important. Il permet d’associer à toute fonction récursive f une formulation non récursive.
Considérons en effet la fonction f formulée récursivement sous la forme :
f
≡ · · · f · · · f · ··
Cette écriture est équivalente à :
f
≡ (λf. · · · f · · · f · ··)f
dans laquelle nous avons abstrait f dans l’expression parenthésée (β-conversion). Cette équation, sous cette
forme, indique que f est le point fixe de (λf. · · · f · · · f · ··). Ce point fixe peut donc, d’après le résultat
ci-dessus, s’exprimer sous la forme :
f
≡ Y (λf. · · · f · · · f · ··)
qui est la formulation non récursive de f . Pour prendre un exemple concret, la fonction factorielle, exprimée
sous la forme traditionnelle :
f act ≡ λn. if (n = 0) then 1 else n ∗ f act(n − 1)
peut se réécrire :
f act ≡ Y (λf.λn. if (n = 0) then 1 else n ∗ f (n − 1))
1.3.1.10
Conclusion
L’hypothèse de Church était que le lambda calcul permettait de définir l’ensemble des fonctions calculables
d’entiers positifs vers entiers positifs. On a montré depuis lors que les fonctions calculables au sens de Turing
étaient précisément celles que l’on pouvait définir dans le lambda calcul.
Notons qu’il existe plusieurs formes étendues du lambda calcul, tel le lambda calcul avec constantes, le
lambda calcul typé, etc. Voir [Bar84] pour un ouvrage récent sur la question.
1.3.2
Lisp
Lisp est certainement le plus ancien membre de la famille des langages fonctionnels. Conçu dans les années
cinquante par John McCarthy [McC60], le langage Lisp présente quelques analogies avec le lambda calcul, telle
la représentation des abstractions par des “lambda expressions” : λx.e se note ainsi (lambda (x) e). Lisp diffère
du modèle du lambda calcul par le choix des expressions conditionnelles permettant d’exprimer la récursion9.
La syntaxe du langage est assez peu conventionnelle. Les expressions du langage sont entièrement parenthésées,
avec une notation préfixée. Un programme de calcul de la factorielle d’un nombre s’écrit ainsi :
(de fact (n)
(cond
((= 0 n) 1)
(t (* n (fact (- n 1))))))
9 Plutôt
que le recours au combinateur Y du lambda calcul.
1.3. HISTORIQUE DE LA PROGRAMMATION FONCTIONNELLE
11
(pour paraphraser cette écriture, on définit (de) la fonction fact à un argument n, comme étant, si (cond) n
est égal à 0, 1, sinon (t pour true) le produit de n par le résultat de l’appel de fact de n − 1.)
Les travaux d’implantation du premier système Lisp se sont déroulés entre 1958 et 1962. Lisp a vu naı̂tre
beaucoup de dialectes, dont les plus connus sont certainement Maclisp, InterLisp, et, plus récemment, Scheme
[SF92] et Common-Lisp [Ste90], [Kun88].
Parmi les contributions majeures de Lisp aux langages fonctionnels, citons l’expression conditionnelle utilisée
pour exprimer les récursions, l’utilisation de cellules de mémoire dynamiquement allouées pour la constitution
des listes, la gestion automatique de ces cellules par un mécanisme spécialisé, le glaneur de cellules (ou, en
anglais et moins poétiquement, garbage collector ), et l’utilisation de fonctionnelles permettant l’application de
fonctions à des listes élément par élément.
De très nombreux ouvrages sont disponibles sur la question [ASS85], [Gir85], etc.
1.3.3
Iswim
Peter Landin introduisit en 1966 une famille de langages nommée Iswim (pour If you See What I Mean),
qui peut être considérée comme du lambda calcul saupoudré de sucre syntaxique. Iswim était caractérisé par
l’utilisation d’une notation infixe10 , l’introduction des clauses let et where (permettant la définition simultanée de fonctions mutuellement récursives, et l’utilisation de l’indentation comme outil syntaxique permettant
d’alléger l’écriture des programmes. Landin insistait en particulier sur la généralité obtenue par un noyau petit,
mais expressif, et surtout sur l’idée qu’un langage de programmation devait permettre de décrire ce que l’on
voulait obtenir, plutôt que comment l’obtenir.
Les idées exprimées alors n’ont pas eu un impact immédiat, peut-être parce qu’elles restaient théoriques,
sans implantation susceptible d’en démontrer le bien fondé, mais se retrouvent aujourd’hui, 25 ans plus tard,
dans tous les langages de programmation fonctionnelle modernes11 .
1.3.4
APL
APL, acronyme de “A Programming Language”, a été conçu par Kenneth Iverson en 1962 [Ive62] comme
un outil mathématique de description d’algorithmes. Langage presque purement fonctionnel dans ses concepts
originels, de nombreuses caractéristiques des langages impératifs (branchement) ont été introduits lors des
implantations effectives sur systèmes IBM 360 (1965, 1968). Les fonctions du langage (c’est là leur caractéristique
essentielle) opèrent globalement sur des tableaux. Les fonctions primitives sont représentées par des caractères
spéciaux, dérivés de la notation mathématique. Les opérateurs sont des fonctionnelles, modifiant l’algorithme
d’application d’une fonction à ses opérandes. Le langage utilise une notation infixe (fonction à deux opérandes)
ou préfixe (fonction à un seul opérande). Quelques exemples :
2 3 5+4 5 12
6 8 17
+/2 3 5
10
×/2 3 5
30
ι 11
0 1 2 3 4 5 6 7 8 9 10
2 3 ρ 3 5 7 8
3 5 7
8 3 5
φ’Hello, World’
dlroW ,olleH
2 3 7 5 +.× 9 6 0 2
46
Ces exemples nous montrent une addition de deux vecteurs, la réduction d’un vecteur (opérateur /) par + et
×, le générateur d’entiers, ι, le constructeur de tableaux, ρ, la fonction miroir φ, le produit scalaire de deux
vecteurs (opérateur . de “produit interne” appliqué aux fonctions + et ×).
10 En lambda calcul, les fonctions n’acceptent qu’un argument, une notation telle que (f x y) représentant en fait les applications
successives ((f x) y). On parle alors de currification, en hommage au mathématicien Haskell Curry [HC58].
11 Il est intéressant de noter que Landin indiquait dans son article que Iswim pouvait servir de base aux “700 prochains langages
de programmation”.
12
CHAPITRE 1. LA PROGRAMMATION FONCTIONNELLE
Le langage dispose d’une centaine de fonctions et opérateurs primitifs permettant l’écriture sans boucles des
algorithmes simples. Une approximation du nombre e par la somme des inverses des factoriels des n premiers
entiers12 s’écrit ainsi :
+/÷!ι20
La fonction suivante, due à Eugene McDonnell [McD88] est une solution du célèbre jeu de la vie de Conway.
Appliquée à une matrice booléenne de taille arbitraire, représentant la configuration des organismes vivants ou
morts à un instant donné, elle fournit la génération suivante :
lif e : (3 3(− 3¨
◦ <)ω))O
Cette définition apparaı̂t pratiquement comme une spécification du problème, puisqu’elle peut se lire : une
cellule du résultat est vivante si la sous-matrice de taille 3 3 correspondante, obtenue par partitionnement avec
recouvrement (fonction dérivée − 3¨
◦ <) de la matrice initiale (ω désigne le paramètre de la fonction) existe dans
le vecteur O (qui est une variable calculée antérieurement, contenant la liste de toutes les matrices solutions).
Des développements récents, au cours de ces dernières années (opérateurs de compositions, d’application,
nouvelles fonctions primitives, nouveaux types de données ; voir par exemple [GM88]), ont apporté un renouveau
au langage qui conserve d’ardents défenseurs, malgré les critiques formulées à son égard par les informaticiens
traditionnalistes. Certains langages dérivés d’APL, tels Nial (acronyme de Nial is APL and Lisp), CTalk [Gir92],
ou J [HIMW90] insistent sur les aspects fonctionnels d’APL, en renonçant à son jeu de caractères spécifique.
APL démontrait dans les années 60 ce que Backus allait prôner dans les années 78, à savoir qu’il était
possible de construire un langage fonctionnel qui ne s’appuyait pas sur la notion de lambda expression, mais
sur celle de combinateurs (c.f. la logique combinatoire de Curry et Schönfinkel).
1.3.5
FP
Le langage FP, que nous avons déja introduit sommairement ci-dessus, est un langage fonctionnel sans
variables, reposant sur l’utilisation d’un nombre réduit de fonctions primitives, et d’un nombre encore plus
réduit de fonctionnelles, permettant de conposer ces fonctions. FP est très proche d’APL, dont il reprend
nombre de fonctions et de combinateurs, en supprimant les aspects impératifs (variables, branchements).
1.3.5.1
Exemples
Voici un exemple de session réalisé avec un petit interprète FP écrit par l’auteur :
---Quelques exemples en FP
+ : <2,3>
-- = 5
length : <1,3,4,6>
-- = 4
3 : <a,b,c,d>
-- = c
id : <a,b,c>
-- = <a,b,c>
|33| : <a,b,c,d>
-- = 33
[id,length] : <a,b,c>
-- = <<a,b,c>,3>
Cette première série d’exemples introduit les notions de base. La séquence -- introduit un commentaire ; de
même, les réponses du système sont précédées de -- =. Le langage travaille sur des données qui peuvent être
des éléments simples (nombres, symboles) ou des séquences d’éléments. Le symbole : représente l’application
fonctionnelle. Ainsi, la fonction + est appliquée à une séquence de deux nombres, fournissant la somme de ces
deux nombres. De même, length retourne la longueur de son paramètre. Un nombre représente une fonction
réalisant une sélection d’élément. Une notation telle que |33| désigne une fonction constante, dont la valeur est
toujours 33. La notation [f,g,h] est une composition fonctionnelle ; appliquée à un objet x, elle fournit comme
résultat <f:x,g:x,hx>.
def square = { * & [id,id] }
-- = square
12 avec
n = 20 !
1.3. HISTORIQUE DE LA PROGRAMMATION FONCTIONNELLE
13
square : 3
-- = 9
@ square : <2,3,4>
-- = <4,9,16>
(\+) & @ square : <2,3,4>
-- = 29
sup -> 1; 2 : <2,3>
-- = 3
Def max = { sup -> 1; 2}
-- = max
max : <2,3>
-- = 3
\ max : <2,3,7,5,6>
-- = 7
Le mot clef Def introduit une définition fonctionnelle. L’environnement global se trouve enrichi d’une nouvelle
fonction. Le caractère & désigne la classique composition de fonctions f ◦ g (notée ◦ chez Backus) : f&g:x
représente f:(g:x). De même, @ est la fonctionnelle d’application élément par élément (notée α chez Backus, ¨
en APL), \ est l’insertion (équivalente à la réduction en APL), et la forme conditionnelle f->g;h : x signifie
si f : x alors g : x sinon h : x.
Terminons par quelques exemples typiques :
-- La moyenne
def moyenne = { / & [ \ + , length] }
-- = moyenne
moyenne : <2,5,8,5,7>
-- = 5
def moyenne = { / & [ float & \ + , length] }
-- = moyenne
moyenne : <2,5,8,5,7>
-- = 5.4
-- Produit scalaire de deux vecteurs
def IP = { (\+) & (@*) & trans }
-- = IP
IP : <<2,5,3,6>,<3,0,4,2>>
-- = 30
-- decomposition du fonctionnement :
trans: <<2,5,3,6>,<3,0,4,2>>
-- = <<2,3>,<5,0>,<3,4>,<6,2>>
(@*) & trans: <<2,5,3,6>,<3,0,4,2>>
-- = <6,0,12,12>
(\+) & (@*) & trans: <<2,5,3,6>,<3,0,4,2>>
-- = 30
-- Le factoriel
def sub1 = { - & [id, |1|] }
-- = sub1
def eq0 = { eq & [id, |0|] }
-- = eq0
def fact = {
eq0 -> |1| ; * & [id, fact & sub1] }
-- = fact
fact : 6
-- = 720
fact : 12
-- = 479001600
-- La fonction de Fibonacci
def sub2 = { - & [id, |2|]}
-- = sub2
def lt2 = { inf & [id, |2|]}
-- = lt2
def fib = {
14
CHAPITRE 1. LA PROGRAMMATION FONCTIONNELLE
lt2 -> |1| ; + & [fib & sub1, fib & sub2] }
-- = fib
fib : 5
-- = 8
fib : 20
-- = 10946
-- Mettre bout a bout deux listes
def apnd = { null & 1 -> 2 ;
apndl & [ 1 & 1, apnd & [ t1 & 1, 2 ]] }
-- = apnd
apnd : <<1,2,3,4>,<a,b,c>>
-- = <1,2,3,4,a,b,c>
-- Toute une liste de listes
\ apnd : <<1,2,3,4>,<a,b,c>,<>,<6,7>,<z>>
-- = <1,2,3,4,a,b,c,6,7,z>
-- Ne garder que les nombres d’une liste
def numbers = { null -> id;
numberp & 1 -> apndl & [1, numbers & t1];
numbers & t1}
-- = numbers
numbers : <1,2,3,4,a,b,c,6,7,z>
-- = <1,2,3,4,6,7>
-- Mettre a plat une structure quelconque
def aplat = {
null -> nil;
atom -> list;
(\ apnd) & (@ aplat) }
-- = aplat
aplat : <>
-- = <>
aplat : 3
-- = <3>
aplat : <1,2,3,<4,5,6,<7,<<<<<<<<<<<8>>>>,9>>>,<10,11,12,
<>,<>,<>,<>,<<>>,<<<<<13>>>>>>>>,14,15>,16,17>>>,18>>
-- = <1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18>
1.3.5.2
Impact de FP
Plusieurs extensions du langage FP ont été proposées, en particulier dans le domaine des architectures
multi-processeurs. Bien que la mouvance des langages fonctionnels soit plutôt dans le style Iswim (comme en
témoignent ML, Miranda ou Haskell), les langages sans variables de type FP restent un sujet d’étude actuel.
Backus travaille lui-même aujourd’hui (en 1986, en tout cas) sur un descendant de FP, le langage FL [BWW86],
qui est typé fortement et dynamiquement, et permet la définition de fonctions d’ordre supérieur et de types de
données.
1.3.6
ML
Le langage ML [GMM+ 78] a initialement été conçu comme langage de commande d’un système déductif
de raisonnement sur les fonctions récursives, LCF. Bien que ML ne soit pas un langage purement fonctionnel
(il comporte en particulier des variables pouvant recevoir des valeurs), il encourage un style de programmation
fonctionnel. Le langage a évolué depuis sa création, s’est standardisé (Standard ML, en 1984), et plusieurs
implantations en ont été faites (CAML par exemple [Mau91], [CH90], ou encore [HV92]). L’une des originalités
de ML est son typage fort (bien que dynamique).
ML opère par inférence de type, pour déterminer le type de chaque expression. Il autorise des fonctions et
des structures de données polymorphiques13 . Enfin, il permet la définition de types concrets ou abstraits. Ce
13 Une fonction peut être polymorphique si son comportement ne depend pas du type de ses paramètres : par exemple, une
fonction qui prend un objet et rend une liste d’un élément contenant cet objet. On parle alors de polymorphisme paramétrique.
Par constraste, le langage Basic fournit une fonction + dont le comportement est celui de l’addition pour des nombres, et de la
concaténation pour des chaı̂nes de caractères. Il s’agit là de surcharge, ou de polymorphisme ad hoc, la fonction (ou le mécanisme
de sélection d’algorithmes) devant prendre en compte le type de ses opérandes. En d’autres termes, le langage se contente d’associer
plusieurs significations à un même symbole, mais l’opération elle-même n’est en rien générique.
1.3. HISTORIQUE DE LA PROGRAMMATION FONCTIONNELLE
15
mécanisme d’inférence de type, dit de Hindley-Milner , a été repris par la plupart des autres langages fonctionnels
fortement typés, tels Miranda ou Haskell. Par ailleurs, ML a aussi introduit la notion de modules, qui sont en
fait des environnements rendus accessibles (ou de première classe).
1.3.7
Haskell
Haskell, enfin, l’un des derniers nés des langages fonctionnels, apparaı̂t comme un effort pour unifier, dans
une conception cohérente, la plupart des notions liées aux langages fonctionnels [HF92], [HJe92]. Il se caractérise
en particulier par l’importance accordée à la notion de filtrage (pattern matching) utilisée comme structure de
contrôle (instruction case, et dans les appels de fonctions), permettant une programmation par équations. Notre
exemple de la factorielle devient ainsi :
fact 0 = 1
fact n+1 = (n+1) * fact n
(le mécanisme est assez élaboré pour reconnaı̂tre que le schéma n+1 représente un entier strictement positif.)
Une fonction qui calcule la somme des éléments d’une liste peut s’écrire :
sum []
sum (x:l)
= 0
= add x (sum l)
Cette définition s’interprète de la manière suivante : lorsque sum est appliquée à la liste vide [], le résultat est 0 ;
lorsque sum est appliquée à une liste qui peut s’écrire x:l — c’est à dire qui est composée d’un premier élément
x concaténé (fonction infixe :) à une liste éventuellement vide l — le résultat est la somme de x et du cumul
des éléments de l.
On peut ne pas s’en tenir là, et abstraire la fonction utilisée pour ce cumul, ainsi que l’élément neutre de
cette fonction. On obtient alors une fonctionnelle, dont la sémantique est très voisine de celle de l’opérateur de
réduction APL, / :
(fold f init) []
= init
(fold f init) (x:l)= f x ((fold f init) l)
(fold est une fonction à trois arguments, définie par filtrage sur le 3ème). Des fonctions pour calculer la somme
et le produit des éléments d’une liste s’en déduisent immédiatement :
sum = fold add 0
prod = fold mul 1
(sum et prod sont des applications partielles de fold sur deux des trois arguments). Travailler au moyen de
telles abstractions permet de découvrir des schémas de programmation nouveaux et puissants. Une formulation
de la fonction append, qui place deux listes bout à bout, peut ainsi s’écrire :
append x y = fold (:) y x
(il est possible de désigner la valeur fonctionnelle associée à un opérateur infixe comme + ou :, en plaçant celui-ci
entre parenthèses).
1.3.7.1
Evaluation paresseuse
Un aspect du langage Haskell, qu’il partage avec d’autres langages fonctionnels, est l’utilisation d’une évaluation paresseuse (lazy evaluation, par opposition à eager evaluation, évaluation au plus tôt des langages
traditionnels).
(Re)situons les concepts de l’évaluation en ordre normal et applicatif dans le cadre des langages de programmation. L’évaluation en ordre applicatif consiste à évaluer les paramètres effectifs d’une fonction, avant
d’appliquer la fonction sur les valeurs obtenues. Les paramètres sont évalués une fois, et une seule, chacun.
L’évaluation en ordre normal consiste (conceptuellement) à remplacer, dans le corps de la fonction, toutes les
occurences des paramètres formels par les expressions qui apparaissent dans l’appel de la fonction. Une expression peut ainsi se retrouver exécutée de 0 à n fois dans la fonction. Ceci n’est pas un inconvénient théorique,
puisque les expressions n’ont pas d’effet de bord en programmation fonctionnelle14 . Malheureusement, alors que
14 Quelque chose qui ressemble à l’évaluation en ordre normal se produit avec les macro-remplacements du langage C. Une
définition comme :
#define max(u,v) (u>v?u:v)
utilisée dans max(f(0),g(1)) va entraı̂ner une double exécution, suivant le cas, de f(0) ou g(1), ce qui peut être très troublant
pour le programmeur si ces fonctions modifient leur environnement.
16
CHAPITRE 1. LA PROGRAMMATION FONCTIONNELLE
l’ordre normal fournit avec certitude la forme normale (si elle existe), c’est à dire le résultat, ce n’est pas le cas
avec l’ordre applicatif. D’autre part, l’évaluation en ordre normal peut nécessiter un grand nombre d’exécutions
d’une même expression, d’où une inefficacité certaine.
On peut imaginer un modèle d’exécution, voisin de l’ordre normal, dans lequel on associe à chaque paramètre
d’une fonction l’expression correspondante dans l’appel de la fonction. Cette expression n’est exécutée que si
la valeur du paramètre est nécessaire à la poursuite du calcul, mais la valeur obtenue est alors associée au
paramètre, et sera fournie lors de toute référence ultérieure à celui-ci. Un paramètre effectif est ainsi calculé une
fois au plus, éventuellement pas du tout. Ce modèle d’exécution est dit évaluation paresseuse, ou encore appel
par besoin. Il possède les propriétés de l’évaluation en ordre normal, associées à l’efficacité de l’évaluation en
ordre applicatif.
Ce modèle offre l’avantage supplémentaire, lorsqu’il est généralisé aux fonctions primitives, de permettre
de travailler avec des structures de données “infinies”. Nous approfondirons ultérieurement cet aspect lors de
l’étude des mécanismes permettant l’évaluation paresseuse en Scheme.
1.4
Concepts de la programmation fonctionnelle — Une rétrospective
Alors, qu’est-ce que la programmation fonctionnelle? C’est, on l’a vu, programmer avec des expressions. Mais
un langage fonctionnel n’est pas qu’un langage impératif dont on a supprimé l’affectation et le branchement !
Voici quelques unes des caractéristiques essentielles des langages fonctionnels modernes :
– les fonctions sont des objets de première classe. En particulier, elles peuvent être manipulées par d’autres
fonctions.
– certaines constructions du langage utilisent le filtrage. La définition de fonctions peut se faire sous forme
équationnelle.
– ils permettent de faire appel à l’évaluation paresseuse, soit explicitement, soit implicitement.
– ils proposent différents types d’abstractions de données. Les algorithmes d’inférence de types permettent de
garantir la correction d’un programme sans imposer au programmeur des déclarations de types explicites.
1.5
Conclusion
Nous avons fait un tour rapide des langages de programmation fonctionnels, d’une part en traçant leur
historique, d’autre part en présentant leurs traits essentiels.
Certains langages applicatifs s’industrialisent : autrefois curiosités de laboratoire, ils proposent aujourd’hui
de bonnes performances et des environnements de programmation élaborés. La rapidité de production qu’ils
permettent se révèle déterminante pour répondre à temps aux exigences d’un marché en perpétuelle évolution.
Ainsi, Miranda [Tur86], qui a obtenu le “BCS Awards 1990”15, est aujourd’hui effectivement utilisé dans l’industrie. Son concepteur revendique un facteur 10 à 20 sur la taille d’applications Miranda comparées à des
produits identiques écrits en C ou en Pascal. De même, les programmeurs APL prétendent (depuis plus de vingt
ans !) obtenir un rapport 10 sur les temps de programmation par rapport aux programmeurs traditionnels.
Nous verrons ultérieurement que Lisp, dont Scheme est un illustre représentant, dispose lui-aussi d’utilisateurs
enthousiastes et de références industrielles solides.
15 Quoi
que ce puisse être — information reproduite d’après une publicité trouvée dans ma boite aux lettres !
Chapitre 2
Une Introduction à Scheme
Ce chapitre, ainsi que les suivants, n’est en rien une description exhaustive du langage. Son but est d’introduire progressivement certains concepts de Scheme. On se réfèrera pour toute description précise d’une fonction
particulière aux manuels de référence du langage ([Han91b], [Han91c]), ou au document Revised 4 Report on
the Algorithmic Language Scheme, [R4R90], qui nous servira souvent de support d’information (que l’on citera
typiquement sous la forme “[R4R90], § 4.2.6”), et qu’il serait inutile de recopier ou paraphraser ici.
2.1
Pourquoi Scheme
Scheme est un petit dialecte de Lisp, particulièrement propre, et, qui plus est, particulièrement agréable
à utiliser. Le langage a été conçu pour proposer un petit nombre de constructions régulières, orthogonales1,
qui favorisent une grande variété de styles de programmation, en particulier les programmations fonctionnelle,
impérative et à objets.
Scheme fut conçu en 1975, par Gerald J. Sussman et Guy L. Steele Jr., au MIT (Massachussets Institute
of Technology), comme un prototype pour la validation de leur recherche autour des concepts d’acteurs (Cf.
par exemple les travaux de Gull Agha, Carl Hewitt et de Henry Lieberman [Agh86], [AH87] ou [Lie87]) et de
fonctions. Le langage, initialement outil de recherche et d’enseignement, a évolué au fil des ans, intégrant avec
parcimonie certains des concepts les plus évolués de l’informatique, tout en restant un outil simple à maı̂triser
et à manipuler. Citons à ce sujet cet extrait de l’introduction à la norme du langage [R4R90] :
Les langages de programmation devraient être conçus, non en empilant extension sur extension, mais en supprimant les faiblesses et les restrictions qui semblent rendre nécessaire l’addition de ces nouvelles extensions.
Scheme démontre qu’un petit nombre de règles de constitution d’expressions, sans restrictions sur la manière
dont ces expressions sont composées, suffisent à définir un langage de programmation pratique et efficace, qui
est suffisament flexible pour supporter la majorité des paradigmes de programmation en usage aujourd’hui.
Scheme fut l’un des premiers langages à considérer les procédures2 comme des citoyens de première classe 3 .
Scheme nous permettra également d’introduire simplement la plupart des concepts des langages fonctionnels,
qui sont soit immédiatement disponibles dans le langage, soit simples à implanter ; nous verrons ainsi les notions
d’environnement, d’évaluation paresseuse, de continuation, de filtrage.
2.1.1
Scheme en quelques mots
La description complète du langage tient en une cinquantaine de pages (le document4 Revised 4 Report
on the Algorithmic Language Scheme [R4R90] tient lieu de norme, et comporte une description formelle de la
syntaxe et de la sémantique du langage ; on trouvera également dans [Ram92] une sémantique opérationnelle
de Scheme).
1 Ce
terme indique ici que ces constructions peuvent être combinées entre elles (pratiquement) sans aucune restrictions.
2 On
utilisera ici indifféremment les termes fonction et procédure, tous les sous-programmes rendant un résultat.
3 First class citizen in the text, ce qui veut dire que ces procédures peuvent être fournies comme paramètres à des fonctions,
rendues comme résultat par une fonction, et conservées comme élément d’une structure.
4 Le chiffre “4” dans Revised 4 Report... ne fait pas référence à une note de bas de page, mais est une abbréviation de Revised
revised revised revised Report..., indiquant que ce fameux document en est maintenant à sa cinquième mouture !
17
18
CHAPITRE 2. UNE INTRODUCTION À SCHEME
Scheme est fondé sur un modèle formel (le lambda calcul – cf. [Chu41], ou encore [Bar84], [Kri90], etc.), si
bien que les programmes Scheme sont pourvus de plein de propriétés sympathiques, et que l’on peut construire
des outils fiables d’analyse et de transformation de code.
Le langage peut être qualifié de fonctionnel (et c’est essentiellement cet aspect que nous étudierons) en ce
sens qu’il offre une complète liberté dans la manipulation des fonctions, bien qu’il ne présente pas toutes les
caractéristiques des “véritables” langages fonctionnels. Scheme présente aussi de nombreux traits empruntés
aux langages impératifs (affectation, effets de bord), tout comme il offre des constructions permettant de mettre
en place des outils de programmation par objets.
2.2
Syntaxe et Sémantique
Comme tout dialecte de Lisp, Scheme repose sur des règles syntaxiques fort simples : notation préfixée et
entièrement parenthésée pour les programmes et les données.5 La fonction précède donc ses arguments, et le
tout est encadré par un couple de parenthèses6. Voici quelques exemples de cette syntaxe :
(+ 2 3)
(* 2 3 5 7 8)
(* 23 578)
17
Les blancs servent à séparer les éléments des listes. La seconde expression calcule le produit de cinq nombres
représentés chacun par un seul chiffre décimal. La troisième calcule le produit du nombre 23 par le nombre
578. Enfin, la dernière expression se réduit à un seul nombre. Sa valeur est 17. Les résultats des expressions
précédentes sont 5, 1680 et 13294 respectivement. On notera donc que les fonctions (au moins certaines d’entre
elles), admettent un nombre variable d’arguments.
La plupart des systèmes Scheme (et Lisp) opèrent en mode interactif , c’est à dire que la machine attend
une commande de l’utilisateur, l’exécute immédiatement, imprime le résultat de cette exécution, puis se remet
en attente. Les commandes attendues par Scheme sont tout simplement des expressions à calculer.
2.2.1
Expressions Symboliques
Comme les autres dialectes de Lisp, Scheme repose sur des règles syntaxiques fort simples : notation préfixée
et entièrement parenthésée pour les programmes et les données. Tout programme Scheme est représenté par
des expressions symboliques. Une expression symbolique est une structure arborescente de taille arbitraire,
composée d’atomes.
2.2.1.1
Les atomes
Les atomes sont les données élémentaires du langage. Les atomes se décomposent typiquement en un petit
nombre de catégories :
– les symboles, ou identificateurs ; les règles précises de formation des symboles diffèrent d’un système à un
autre. De manière informelle, un symbole est une suite de caractères ne représentant ni un nombre, ni
une chaı̂ne de caractères. Hormis une dizaine de caractères utilisés comme ponctuations, la plupart des
caractères du code ASCII sont admis à l’intérieur d’un identificateur. Voici quelques identificateurs :
truc + symbol? [email protected] let* set-car!
– les booléens, représentent les valeurs logiques vrai et faux . Ils sont dénotés par les noms #t et #f.
– les nombres ; un nombre est une suite de chiffres, débutant éventuellement par un signe + ou un signe -,
pouvant comporter un point décimal ou un exposant.
– les caractères ; un caractère s’écrit sous la forme d’un dièze, #, suivi d’un backslash, \, suivi du caractère
ou du nom du caractère. Voici quelques caractères :
#\A #\a #\space #\( #\.
5 Cette notation est dite “polonaise préfixée de Cambdridge”. La notation polonaise, due au logicien Polonais Jan Lukasiewicz,
consiste à écrire une expression sous forme linéaire, en faisant suivre les opérandes par la fonction. Ainsi, 3x + 5 s’écrit 3 x × 5 +.
Cette notation est utilisée dans le langage Forth. Sa variante préfixée consiste à placer la fonction en premier, ce qui donnerait ici :
+ × 3 x 5. Enfin, la notation dite de Cambdridge ajoute des parenthèses pour englober chaque sous-expression, ce qui permet aux
fonctions d’accepter un nombre variable de paramètres. L’écriture devient alors : (+ (× 3 x) 5).
6 On
parle de liste pour désigner une telle suite d’éléments placés entre parenthèses.
2.2. SYNTAXE ET SÉMANTIQUE
19
– les chaı̂nes de caractères ; une chaı̂ne de caractères est une suite arbitraire de caractères placée entre
double quote ". Une double quote doit être précédée du caractère d’échappement \. On en déduit qu’un
\ doit aussi être précédé d’un autre \ pour être digne de figurer dans une chaı̂ne.
2.2.1.2
Les expressions symboliques
Une expression symbolique est soit un atome, soit une suite d’expressions symboliques (séparées par des
blancs), placée entre parenthèses. On parle alors de liste. Quelques exemples :
toto
(une expression symbolique)
(en voici (encore !) une autre
(qui occupe plusieurs lignes (en effet)))
2.2.1.3
Les ponctuations
Voici une liste non exhaustive des caractères spéciaux du langage :
– L’espace : il sert de séparateur. Une suite quelconque d’espaces est équivalente à un seul. Le caractère de
fin de ligne, le caractère de tabulation et la plupart des caractères dits “de contrôle” sont assimilés à des
blancs.
– Le point : utilisé seul, le point sert de séparateur dans une paire pointée (nous y reviendrons ultérieurement). Le point sert également à séparer la partie entière de la partie décimale d’un nombre. Il peut
également être utilisé (il n’a alors pas de signification particulière) à l’intérieur d’un identificateur.
– Le point-virgule : il introduit un commentaire. Le restant de la ligne est ignoré, et le tout équivaut à un
blanc.
– La double quote : elle introduit une chaine de caractères.
– Les parenthèses servent à délimiter listes et paires pointées.
– Le dièze # est un macro-caractère. Il précède certaines constructions particulières du langage ; par exemple,
#x2fc est une notation hexadécimale du nombre entier 764 (c.f. [R4R90], § 6.5.4).
– L’apostrophe ’, ou quote, introduit des données symboliques.
– La contre apostrophe, ou back-quote, joue un rôle voisin de celui de la quote.
– Les séquences , et ,@ sont utilisées en conjonction avec la back-quote.
– Citons enfin les accolades { } et les crochets [ ], qui ne sont pas utilisés dans le langage, mais qui sont
“réservés” pour de futures extensions.
Nous reviendrons sur leurs rôles précis par la suite.
2.2.2
Programmes et évaluation
Tout programme Scheme est représenté par une expression symbolique. Les règles d’évaluation d’une expression symbolique sont les suivantes :
– Les constantes (nombres, caractères, booléens, chaines de caractères, etc.) représentent leur propre valeur.
Ainsi :
3
est un programme Scheme, dont l’effet est de calculer la valeur 3.
– Les atomes représentent les variables Scheme. Ces variables peuvent être prédéfinies, comme +, -, *, sin,
cos, remainder, etc., ou définies par l’utilisateur. L’évaluation d’un atome consiste à le remplacer par la
valeur qui lui est associée dans le contexte courant. L’évaluation d’un atome auquel aucune valeur n’est
associée dans le contexte courant provoque une erreur.
La plupart des valeurs associées aux variables prédéfinies sont des valeurs fonctionnelles.
20
CHAPITRE 2. UNE INTRODUCTION À SCHEME
– Une liste représente une application fonctionnelle, ou une forme spéciale.
– Une application fonctionnelle consiste en l’application d’une fonction sur ses opérandes. Les éléments
de la liste (constantes, atomes ou autres listes) sont d’abord évalués, dans un ordre non précisé.
Le premier élément de la liste, qui doit être une valeur fonctionnelle, est alors appliqué aux autres
éléments. Le résultat de cette application devient le résultat de l’expression symbolique.
– Une forme spéciale est un cas très particulier d’expression Scheme. Les formes spéciales ne sont pas
des applications fonctionnelles. Le premier élément d’une telle forme n’est pas une désignation de
fonction, mais un mot clef . Les autres éléments d’une forme spéciale ne sont pas nécessairement
évalués : l’utilisation qui en est faite dépend de la sémantique associée à la forme spéciale.
Alors que le mécanisme d’exécution d’une application fonctionnelle est toujours le même, celui d’une
forme spéciale est ainsi propre à chaque forme. Heureusement, il n’existe qu’un tout petit nombre
de formes spéciales, qui, pour la plupart, correspondent aux instructions de contrôle des langages
impératifs.
Dans ce chapitre, nous étudierons quelques unes des fonctions primitives du langage (en particulier les
fonctions arithmétiques), et trois formes spéciales : if, lambda et define. Ces règles sont illustrées dans les
exemples ci-dessous.
2.2.3
Une session avec MIT Scheme
Les lignes suivante retracent une “première” session avec le système Scheme du MIT.
kiwi.emse.fr% scheme
Scheme Microcode Version 11.59
MIT Scheme running under SunOS
Type ‘^C’ (control-C) followed by ‘H’ to
obtain information about interrupts.
Scheme saved on Tuesday December 11, 1990 at 7:04:22 PM
Release 7.1.0 (beta)
Microcode 11.59
Runtime 14.104
SF 4.15
1 ]=> 2
;Value: 2
1 ]=> (+ 3 4 5)
;Value: 12
1 ]=> "hello, world\n"
;Value: "hello, world\n"
1 ]=> toto
Unbound variable toto
;Package: (user)
2 Error-> (* 2.5 1e19)
;Value: 2.5e19
2 Error-> 2+3i
;Value: 2+3i
2 Error-> (* 2+3i 5-i)
;Value: 13+13i
2 Error-> (* 1 2 3 4 5 6 7 8 9 10 11 12
13 14 15 16 17 18 19 20 21 22 23 24 25)
;Value: 15511210043330985984000000
2 Error-> (/ 7 5)
;Value: 7/5
2 Error-> (* 3 (/ 2 3))
;Value: 2
2 Error-> (sqrt (+ (* 3 3) (* 4 4)))
;Value: 5
2 Error-> (quit)
Stopped
21
2.2. SYNTAXE ET SÉMANTIQUE
kiwi.emse.fr% jobs -l
[1] + 8337 Stopped
kiwi.emse.fr% fg 1
scheme
;No value
2 Error-> (exit)
Kill Scheme (y or n)? No
;No value
2 Error-> ^D
End of input stream reached
Moriturus te saluto.
kiwi.emse.fr%
scheme
Ces quelques lignes nous montrent diverses représentations des nombres : entiers, flottants, complexes. Scheme
sait faire des calculs sur des nombres entiers en précision illimitée et sur des fractions rationnelles. Elles illustrent
également les aspects syntaxiques du langage. Une expression peut être composée simplement d’une constante
(nombre, chaı̂ne de caractères), ou d’applications fonctionnelles, éventuellement imbriquées. Une telle application
s’écrit sous la forme d’une liste parenthésée, débutant par la fonction, désignée par son nom, comme +, *, sqrt,
suivie de son ou ses paramètres, qui peuvent eux-mêmes être des expressions simples ou des applications.
Quelques autres remarques préliminaires : il est utile de comprendre ce qui se passe lors d’un dialogue avec
la machine, de savoir que faire en cas d’erreur, et comment arrêter le jeu.
Le plus simple pour interrompre l’exécution est de taper un ^D (Contrôle-D). La fonction (exit) offre
une sécurité supplémentaire, en demandant une confirmation de l’arrêt. Enfin, la fonction (quit) permet de
suspendre l’exécution, qui peut être reprise ultérieurement par la commande unix fg.
Le prompt en mode normal est la suite de caractères 1 ]=>. En cas d’erreur, le contexte est empilé, l’exécution
se poursuit dans ce nouveau contexte (contexte global, incrémenté du contexte au moment de l’erreur). Le
prompt devient 2 Error->, puis 3 Error->, 4 Error->, etc, à chaque nouvelle erreur. La manière la plus
simple de s’en sortir est de taper ^G (Contrôle-G) :
3 Error-> ^G
Quit!
1 ]=>
qui nous ramène directement au contexte d’exécution normal.
Il est possible également à tout moment d’interrompre l’interprète, par ^C (Contrôle-C) :
1 ]=> ^C
Interrupt option (? for help): ?
^B: Enter a breakpoint loop.
^C: Goto to top level read-eval-print (REP) loop.
^L: Clear the screen.
^U: Up to previous (lower numbered) REP loop.
^X: Abort to current REP loop.
D: Debugging: change interpreter flags.
E: Examine memory location.
H: Print simple information on interrupts.
I: Ignore interrupt request.
Q: Quit instantly, killing Scheme.
R: Hard reset, possibly killing Scheme in the process.
T: Stack trace.
Z: Quit instantly, suspending Scheme.
Interrupt option (? for help):
Les deux options réellement utiles a notre niveau sont ^C (donc ^G est équivalent à ^C^C, mais sans impression
de message) et I, pour ignorer l’interruption. Remarquons enfin que dans cet exemple :
3 Error-> (+ 2 3
4 5 ^C
Interrupt option (? for help): Ignored.
6 7
)
;Value: 18
Resuming Scheme.
22
CHAPITRE 2. UNE INTRODUCTION À SCHEME
l’effet de l’interruption par ^C suivi de I a été d’ignorer complètement la ligne débutant par 4 5.
Signalons enfin une source traditionnelle d’erreurs en Scheme comme en Lisp : les parenthèses ou les doublequotes non refermées :
1 ]=> "Hello
))
ben quoi ?
ma console est morte!
^C
Interrupt option (? for help): Ignored.
et alors ?
Ah oui! la double-quote! la voici:"
Resuming Scheme.
;Value: "Hello\n\n))\nben quoi ?\nma console est morte!
\n\n\net alors ?\nAh oui! la double-quote! la voici:"
1 ]=>
On trouvera toutes les informations nécessaires à une utilisation effective de MIT Scheme dans le guide de
l’utilisateur [Han91c].
2.3
Type numérique
Les fonctions les plus simples à mettre en oeuvre lors d’un premier contact avec le langage sont naturellement
les fonctions numériques. Scheme fournit, sous forme préfixe, toutes les fonctions mathématiques usuelles : les
opérations traditionnelles +, -, *, /, quotient, remainder, les fonctions trigonométriques, hyperboliques, etc.
On se reportera pour plus de détails soit au manuel de référence de l’implantation du MIT [Han91b], soit encore
au fameux rapport servant de norme [R4R90] (§ 6.5, pages 18–23).
Un nombre est une suite de chiffres, débutant éventuellement par un signe + ou un signe -, pouvant comporter
un point décimal ou un exposant. Voici quelques exemples d’écritures représentant des nombres :
325
9.81
6.023e-23
-1
+125
2.3.1
Expressions arithmétiques
Le modèle de l’arithmétique utilisé par Scheme est conçu pour être aussi indépendant que possible des
représentations particulières utilisées pour les nombres dans le calculateur hôte. En Scheme, tout entier est un
rationnel, tout rationnel est un réel, et tout réel est un nombre complexe. Ainsi, la distinction entre arithmétiques
entière et réelle, qui joue un rôle si important dans de nombreux autres langages de programmation, n’apparaı̂t
pas en Scheme. A sa place, il existe une distinction entre l’arithmétique exacte, qui correspond à un idéal
mathématique, et l’arithmétique inexacte des approximations (distinction sur laquelle nous reviendons dans un
chapitre ultérieur).
Une expression arithmétique est une liste dont le premier élément désigne l’opération à réaliser, les autres
éléments étant les paramètres de cette opération. Certaines des opérations de Scheme, qui correspondent à des
fonctions mathématiques traditionnelles, sont représentées par des symboles particuliers :
+ représente l’addition. Cette fonction admet un nombre arbitraire de paramètres :
1 ]=> (+ 2 3 -7)
;Value: -2
1 ]=> (+ 4)
;Value: 4
1 ]=> (+ 5 4 3 2 1)
;Value: 15
1 ]=> (+)
;Value: 0
2.3. TYPE NUMÉRIQUE
23
Elle associe même une signification à la forme (+), c’est à dire à un appel de la fonction sans paramètres,
et fournit comme résultat l’élément neutre de l’opération.
* est la multiplication. Tout comme l’addition, la multiplication admet un nombre arbitraire de paramètres :
1 ]=> (* 2 3 5.5)
;Value: 33
1 ]=> (*)
;Value: 1
1 ]=> (* 7)
;Value: 7
- représente la soustraction, ou la fonction “opposé” lorsqu’il n’y a qu’un argument :
1 ]=> (- 5 8.2)
;Value: -3.2
1 ]=> (- 4)
;Value: -4
1 ]=> (- 9 2 5 6)
;Value: -4
/ représente la division, ou la fonction “inverse” lorsqu’il n’y a qu’un argument :
1 ]=> (/ 7 2)
;Value: 3.5
1 ]=> (/ 4)
;Value: 0.25
1 ]=> (/ 9 2 5 6)
;Value: 0.15
Ces fonctions suffisent à réaliser certaines manipulations avec MIT-Scheme :
– La moyenne des nombres 3 et 4.12 :
1 ]=> (/ (+ 3 4.12) 2)
;Value: 3.56
– Le prix TTC d’une boite de 10 disquettes haute densité, vendue 69 F ht :
1 ]=> (* 69 (+ 1 (/ 18.6 100)))
;Value: 81.834
(et ils osent afficher “82 F TTC” !)
– La température record du jour à Miami, avec 97.7 Fahrenheit au thermomètre, exprimée en degrés Celsius :
1 ]=> (/ (* 5 (- 97.7 32)) 9)
;Value: 36.5
2.3.2
Autres fonctions arithmétiques
Naturellement, Scheme nous fournit un ensemble assez complet de fonctions mathématiques et trigonométriques. Contrairement aux opérations que nous avons déja présentées (addition, soustraction, multiplication,
etc.), elles ne sont pas représentées par un caractère particulier, mais sont désignées par un nom, qui est le plus
souvent une abbréviation du nom de la fonction mathématique correspondante. Ceci ne change naturellement
rien à la manière dont elles peuvent être utilisées. Voici la liste des principales :
abs valeur absolue de l’argument.
acos arc cosinus de l’argument. Le résultat est la valeur principale de l’angle, exprimée en radians.
asin arc sinus de l’argument. Le résultat est la valeur principale de l’angle, exprimée en radians.
atan arc tangente de l’argument. Le résultat est la valeur principale de l’angle, exprimée en radians. La fonction
peut être utilisée avec deux arguments. Il y a alors quasi-équivalence :
(atan z1 z2 ) ⇐⇒ (atan (/ z1 z2 ))
24
CHAPITRE 2. UNE INTRODUCTION À SCHEME
à cette nuance près que la division n’est pas effectuée, ce qui permet d’écrire :
1 ]=> (atan 6 0)
;Value: 1.5707963267948966
ceiling plafond. Les quatre procédures ceiling, floor, round et truncate fournissent comme résultats des
entiers. Le résultat de floor est le plus grand entier non supérieur au paramètre. Le résultat de ceiling
est le plus petit entier non inférieur au paramètre. Le résultat de truncate est l’entier le plus voisin du
paramètre, dont la valeur absolue ne soit pas supérieure à celle du paramètre. Le résultat de round est
l’entier le plus voisin du paramètre ; lorsqu’il y a deux candidats possibles, c’est l’entier pair qui est choisi :
1 ]=> (floor -4.3)
;Value: -5.0
1 ]=> (ceiling -4.3)
;Value: -4.0
1 ]=> (truncate -4.3)
;Value: -4.0
1 ]=> (round -4.3)
;Value: -4.0
1 ]=> (floor 3.5)
;Value: 3.0
1 ]=> (ceiling 3.5)
;Value: 4.0
1 ]=> (truncate 3.5)
;Value: 3.0
1 ]=> (round 3.5)
;Value: 4.0
1 ]=> (round 4.5)
;Value: 4.0
1 ]=> (round 6)
;Value: 6
cos cosinus. Le paramètre est un angle exprimé en radians.
exp exponentielle.
expt élévation à la puissance.
(expt z1 z2 ) ⇐⇒ z1z2 ⇐⇒ ez2 log z1 ⇐⇒ (exp (* z2 (log z1 )))
floor plancher. Cf. ceiling.
gcd plus grand diviseur commun (aka7 pgcd ). Cette fonction admet un nombre arbitraire de paramètres.
log logarithme népérien.
lcm plus petit multiple commun (aka ppcm). Cette fonction admet un nombre arbitraire de paramètres.
max plus grand élément. Cette fonction fournit le plus grand de ses paramètres ; il doit y avoir au moins un
paramètre.
min plus petit élément. Cette fonction fournit le plus petit de ses paramètres ; il doit y avoir au moins un
paramètre.
modulo reste de la division entière. Les trois procédures modulo, quotient et remainder réalisent des opérations liées à la division entière. Pour des entiers positifs n1 et n2 , il existe des entiers n3 et n4 , uniques,
tels que n1 = n2 n3 + n4 , avec 0 ≤ n4 < n2 , et :
(quotient n1 n2 ) =⇒ n3
(remainder n1 n2 ) =⇒ n4
(modulo n1 n2 ) =⇒ n4
7 aka
= also known as.
2.4. LE TYPE BOOLÉEN
25
Si n2 n’est pas nul, l’identité suivante est respectée :
n1 ⇐⇒ (+ (* n2 (quotient n1 n2 )) (remainder n1 n2 ))
Le résultat de la procédure quotient est toujours du signe du produit des arguments, ou nul si n1 = 0.
Le résultat de la procédure remainder est soit nul, soit du signe du dividende. Le résultat de la procédure
modulo est toujours du signe du diviseur.
quotient division entière. Cf. modulo.
remainder reste de la division entière. Cf. modulo.
round arrondi. Cf. ceiling.
sin sinus. Le paramètre est un angle exprimé en radians.
sqrt racine carrée de son argument.
tan tangente. Le paramètre est un angle exprimé en radians.
truncate troncature. Cf. ceiling.
2.4
Le type booléen
Tout langage de programmation permet de comparer entre eux des objets (par exemple des nombres, des
suites de caractères), et d’utiliser le résultat de cette comparaison pour prendre une décision : faire ou ne pas faire
telle action, arrêter ou continuer l’exécution d’un programme, etc. En Scheme, le résultat d’une comparaison
est une valeur, dite logique ou booléenne. Ce paragraphe décrit les valeurs booléennes du langage Scheme, et
leur comportement.
2.4.1
Les valeurs booléennes
Les objets booléens standard pour “vrai” et “faux” s’écrivent #t et #f. Ce qui importe réellement, cependant,
ce sont les objets que les expressions conditionnelles de Scheme (if, cond, and, or, do) considèrent comme vrais
ou faux. La phrase “la valeur vrai” (ou quelquefois simplement “vrai”) signifie “n’importe quel objet considéré
comme vrai par les expressions conditionnelles”, et la phrase “la valeur faux ” (ou “faux”) signifie “n’importe
quel objet considéré comme faux par les expressions conditionnelles”.
La norme du langage précise que, parmi toutes les valeurs Scheme standard, seul #f est considérée comme
faux dans les expressions conditionnelles. A l’exception de #f, toutes les valeurs Scheme standard (ceci inclut
#t, les paires, la liste vide, les symboles, les nombres, les chaı̂nes, les vecteurs et les procédures) sont considérées
comme “vraies”. Dans l’implantation que nous utilisons, MIT-Scheme, la liste vide notée () est également
assimilée à la valeur “faux”.
Voici les éléments du langage liés aux objets booléens :
#t, #f Ces deux objets représentent les valeurs “vrai” et “faux” respectivement. Ces valeurs sont utilisées dans
les instructions conditionnelles. Une fonction qui fournit toujours un résultat booléen est dite prédicat.
Par convention, les noms des prédicats se terminent habituellement par “?”.
not La procédure not, négation logique, fournit #t si son paramètre est #f, #f sinon.
boolean? Cette procédure fournit #t si son paramètre est soit #t, soit #f, et #f dans le cas contraire.
Voici quelques exemples de manipulation des objets booléens :
1 ]=> #t
;Value: #t
1 ]=> #f
;Value: ()
1 ]=> (not
;Value: ()
1 ]=> (not
;Value: #t
1 ]=> (not
;Value: #t
1 ]=> (not
#t)
#f)
())
(not (not #t)))
26
CHAPITRE 2. UNE INTRODUCTION À SCHEME
;Value: ()
1 ]=> (boolean? #t)
;Value: #t
1 ]=> (boolean? 23)
;Value: ()
Notons que le système MIT-Scheme imprime la valeur “faux” sous la forme (), et non #f, contrairement à ce
que veut la norme du langage8, mais cette différence n’aura aucune incidence réelle sur les programmes que
nous écrirons. C.f. les opérations sur valeurs booléennes ([R4R90], § 6.1).
2.4.2
Prédicats
Les prédicats sont des fonctions fournissant un résultat booléen (c.f. [R4R90], § 6.2). Ainsi, la procédure
boolean? décrite ci-dessus peut être qualifiée de prédicat . La plupart des prédicats prédéfinis sont désignés par
un nom se terminant par un point d’interrogation. Les exceptions à cette règle sont peu nombreuses : fonctions
de comparaison numérique (<, <=, =, =>, >), etc.
2.4.2.1
Fonctions de comparaison
Voici la liste des opérations de comparaison numériques proposées par le système.
=, <, <=, >=, >
Ces fonctions réalisent des comparaisons de leurs opérandes, qui doivent être numériques.
Elles admettent deux paramètres ou plus, et leur résultat est une valeur booléenne (cf. 2.4).
La norme spécifie que les fonctions de comparaison doivent admettre un nombre arbitraire de paramètres,
une expression telle que (-op. x1 x2 ... xn ) étant vraie si x1 -op. x2 , x2 -op. x3 , ..., et si xn−1 -op. xn le
sont. Ceci permet d’écrire :
(< A B C D E)
pour vérifier que les nombres A, B, C, D et E forment bien une suite strictement croissante, ou encore :
(<= 2 X 3.5)
pour s’assurer que X appartient bien à l’intervalle fermé [2, 3.5].
2.4.3
Autres prédicats numériques
Tout comme boolean? peut être utilisé pour découvrir si son paramètre est une valeur booléenne, number?
indique si son paramètre est un nombre. Scheme nous propose d’autres prédicats s’appliquant aux valeurs
numériques, dont voici la liste :
even? Ce prédicat fournit #t si son paramètre est un nombre entier pair, #f sinon.
integer? Ce prédicat fournit #t si son paramètre est un nombre entier, #f sinon.
negative? Ce prédicat fournit #t si son paramètre est un nombre strictement négatif, #f sinon.
number? Ce prédicat fournit #t si son paramètre est un nombre, #f sinon.
odd? Ce prédicat fournit #t si son paramètre est un nombre entier impair, #f sinon.
positive? Ce prédicat fournit #t si son paramètre est un nombre strictement positif, #f sinon.
real? Ce prédicat fournit #t si son paramètre est un nombre réel, #f sinon. Attention, puisque le corps des
entiers est un sous-ensemble du corps des réels, tout entier est un nombre réel :
1 ]=> (real? 2)
;Value: #t
1 ]=> (real? 3.14)
;Value: #t
1 ]=> (real? #t)
;Value: ()
zero? Ce prédicat fournit #t si son paramètre est égal à zéro, #f sinon.
8 Dans un système Lisp traditionnel, on utilise t et nil pour représenter les valeurs booléennes ; en pratique, n’importe quelle
valeur différente de faux est considérée comme vraie dans un test. Pour compliquer encore les choses, la liste vide est assimilée au
symbole nil. En Scheme, ces trois objets (valeur logique vraie, symbole nil et liste vide) sont différents, sauf dans MIT-scheme,
où c’est la même chose.
27
2.5. STRUCTURES DE CONTRÔLE
2.5
Structures de contrôle
Scheme ne dispose que d’un nombre minimum de structures de contrôles. La forme conditionnelle essentielle
est la forme spéciale if :
(if -test . -consequence. -alternative.)
L’algorithme d’exécution est simple : si l’exécution de la partie -test . fournit la valeur vraie, l’expression
-consequence. est exécutée ; dans le cas contraire, l’expression -alternative. est exécutée. Un exemple simple, le
maximum des deux nombres x et y :
(if (> x y) x y)
2.6
Fonctions
Nous avons déjà signalé que les fonctions étaient citoyens de première classe en Scheme. Elles constituent
donc, à l’instar des nombres et des chaı̂nes de caractères, un type de données primitif du langage. Rappelons
que lorqu’une expression est une liste, tous les éléments de cette liste sont évalués, en particulier le premier, qui
représente la fonction à appliquer. Dans une expression telle que :
(* x y)
les variables x, y, mais aussi * sont remplacées par leurs valeurs, * désignant en l’occurence la fonction primitive
de multiplication. Le premier élément est habituellement un atome, qui va être le nom d’un objet prédéfini
du système, ou défini par l’utilisateur au moyen de define, mais ce premier élément peut être lui-même une
expression quelconque, dont la valeur est une fonction :
((if (> 3 5) + *) 5 7)
=⇒
35
Ce exemple9 montre que la forme if peut être utilisée dans n’importe quelle expression, en particulier en
position fonctionnelle.
Une fonction se définit, comme en Lisp10 , par une lambda-expression :
(lambda -paramètres-formels. -expression.)
-paramètres-formels. est une liste de symboles représentant les paramètres de la fonction, -expression. est le
corps de la fonction. Ainsi :
(lambda (x) (+ x 1))
est la fonction successeur.
(lambda (x) (* x x))
définit la fonction élévation au carré.
Une lambda expression n’est pas une fonction : c’est son exécution qui définit la fonction. Cette fonction
peut être directement appliquée à son, ou ses paramètres :
((lambda (x) (* x x)) 3)
La fonction peut être également affectée à un identificateur :
(define square (lambda (x) (* x x)))
Une sémantique identique est obtenu par la forme simplifiée suivante :
(define (square x) (* x x))
9 Nous utilisons ici le symbole =⇒ pour signifier que l’évaluation de l’expression Scheme située à gauche fournit ordinairement
le résultat placé à droite — des exceptions sont toujours à craindre !
10 La syntaxe est identique, mais certains aspects font que les fonctions ne sont pas first class citizen dans la plupart des autres
dialectes de Lisp.
28
CHAPITRE 2. UNE INTRODUCTION À SCHEME
Tout comme Pascal ou Algol 60, Scheme utilise la notion de bloc. Le corps d’une définition de fonction, par
exemple, est un bloc, à l’intérieur duquel il est possible de déclarer d’autres fonctions. Ainsi, on peut utiliser
la fonction ci-dessus à l’intérieur d’une autre fonction destinée à calculer la longueur de l’hypothénuse d’un
triangle rectangle :
(define (hypo a b)
(define (square x) (* x x))
(sqrt (+ (square a) (square b))))
=⇒
hypo
(hypo 3 4)
=⇒
5.0
(hypo 5 8)
=⇒
9.43398
2.7
Programmer en Scheme
L’exemple ci-dessus nous montre une première technique de programmation en Scheme : utiliser de petites
fonctions, simples à comprendre, prouver ou modifier. Nous avons utilisé la fonction primitive d’extraction de
racine carrée. Comment programmer une telle fonction ? En voici une première approche par la méthode de
Newton.
(define (square-root n)
(define (itere x y)
(/ (+ y (/ x y)) 2))
(itere n 1.0))
=⇒
(square-root 2)
=⇒
square-root
1.5
Une seule itération ne suffit manifestement pas à obtenir une précision correcte. Il est possible de réitérer un
certain nombre de fois le calcul d’approche de la solution :
(define (square-root n)
(define (itere x y)
(/ (+ y (/ x y)) 2))
(itere n (itere n
(itere n (itere n 1.0)))))
=⇒
square-root
(square-root 2)
=⇒
1.41421
Mais...
(square-root 10000)
=⇒
630.304
Il vaut mieux une fonction qui itère autant que nécessaire, jusqu’à l’obtention d’une certaine précision :
(define (square-root n)
(define (itere x y)
(/ (+ y (/ x y)) 2))
(define (close-to p q eps)
(< (/ (abs (- p q)) (max (abs p) (abs q))) eps))
(define (check n root eps)
(if (close-to root (/ n root) eps)
root
(check n (itere n root) eps)))
(check n 1.0 1E-6))
=⇒
square-root
(square-root 2)
=⇒
1.41421
(square-root 10000)
=⇒
100.0
Cette écriture introduit trois fonctions auxiliaires, dont l’une est récursive. La fonction itere réalise une itération. Le prédicat close-to vérifie la proximité relative de deux nombres à un epsilon près. Enfin, check vérifie
si l’on a atteint une précision suffisante, récurse sinon en appliquant une itération supplémentaire.
2.8. CONCLUSION
29
Grâce à l’utilisation d’une arithmétique entière et rationnelle exacte, Scheme permet l’écriture directe, sous
la forme habituelle11 d’algorithmes nécessitant une grande précision. Ainsi, le test suivant fournit une réponse
correcte (la valeur -54767/66192), alors que les autres langages de programmation (C, Pascal, Fortran, etc)
échouent s’ils ne font pas appel à une bibliothèque spécialisée :
1 ]=> (define (f x y)
(+ (* (/ 1335 4) (expt y 6))
(* (expt x 2)
(- (* 11 (expt x 2) (expt y 2))
(expt y 6)
(* 121 (expt y 4))
2))
(* (/ 11 2) (expt y 8))
(/ x (* 2 y))))
;Value: f
1 ]=> (f 77617 33096)
;Value: -54767/66192
Scheme peut même nous fournir une idée des premières décimales de la valeur approchée de ce nombre :
1 ]=> (floor (* (f 77617 33096) (expt 10 100)))
;Value:
-82739605994682136814116509547981629199903311578438
48199178148416727096930142615421803239062122310854
L’algorithme de racine carrée ci-dessus peut ainsi être modifié pour opérer en fractions rationnelles. Il suffit de
remplacer 1.0 par 1 dans l’appel initial de check à l’intérieur de la fonction. En ajoutant la précision demandée
comme paramètre, on obtient la fonction suivante, capable de calculer une racine carrée avec une précision
arbitraire :
1 ]=> (define (square-root n precision)
...
(check n 1 precision))
;Value: square-root
1 ]=> (square-root 2 1e-5)
;Value: 577/408
1 ]=> (exact->inexact (* 577/408 577/408))
;Value: 2.000006007304883
1 ]=> (square-root 2 1e-8)
;Value: 665857/470832
1 ]=> (exact->inexact (* 665857/470832 665857/470832))
;Value: 2.000000000004511
1 ]=> (square-root 2 1e-25)
;Value: 1572584048032918633353217/1111984844349868137938112
1 ]=> (exact->inexact (*
1572584048032918633353217/1111984844349868137938112
1572584048032918633353217/1111984844349868137938112))
;Value: 2.
1 ]=> ; on s’attend a 50 chiffres significatifs environ
(floor (* (*
1572584048032918633353217/1111984844349868137938112
1572584048032918633353217/1111984844349868137938112)
(expt 10 100)))
;Value:
200000000000000000000000000000000000000000000000080
87275979834283525943226474697820217857072909414290
2.8
Conclusion
Tout en présentant de nombreux points communs avec certains langages impératifs à structure de blocs
(Pascal, Algol 60), Scheme s’en démarque par une syntaxe utilisant des expressions parenthésées préfixées, et
11 En
Scheme !
30
CHAPITRE 2. UNE INTRODUCTION À SCHEME
une absence de déclarations (hormis les définitions — ou déclarations — de fonctions locales).
2.9
Travail personnel
Que suggérer à ce stade comme travail personnel? Tout d’abord, utiliser, encore et encore, le système. Les
notions introduites dans ce chapitre sont élémentaires. Seule la pratique est utile à ce stade.
Ecrire n’est pas tout. Il faut aussi lire des fonctions écrites par d’autres, pour les comprendre, être capable
de les corriger ou de les modifier.
Quelques ouvrages sur Scheme, ou illustrant le langage : [R4R90], [SF92], [ASS85] (dont la traduction française est disponible : [ASS89]), [FWH89].
Transformez des expressions arithmétiques usuelles en Scheme, et réciproquement. Pratiquez avec les exercices suivants :
Dérivée
Programmez la fonction der-exp fournissant la dérivée de la fonction exponentielle.
Intervalle
Ecrire une fonction à trois paramètres, a, b et c, indiquant si le nombre a est situé dans l’intervalle fermé
défini par les deux autres.
Valeur absolue
La fonction abs fournit comme résultat la valeur absolue de son paramètre. Trouver d’autres formulations,
ne faisant pas appel à abs, pour réaliser cette fonction au moyen des opérations que nous avons présentées.
Module d’un segment
Ecrire la fonction calculant la longueur d’un segment AB défini par les coordonnées des points A (xa, ya),
et B (xb, yb).
Fonctions hyperboliques
Définir les fonctions sh, ch et th calculant les sinus, cosinus, et tangente hyperboliques de leurs arguments.
Moyenne olympique
Ecrire une fonction à six paramètres, représentant les notes attribuées par un jury olympique, dont le résultat
est la moyenne olympique de ces six notes, c’est à dire la moyenne des quatre notes qui restent lorsque l’on a
supprimé la plus haute et la plus basse.
Bissextilité
Ecrire le prédicat bissextile? qui prend comme paramètre un entier représentant une année, et fournit un
booléen indiquant si l’année correspondante est bissextile ou non.
Polynômes
Ecrire en Scheme les fonctions correspondant aux polynômes en x suivants :
7x5 + 3x2 − 17x + 3
x4 + x3 − 5x2 − 1
Calculer la valeur de ces polynômes pour diverses valeurs de x.
Chapitre 3
Eléments de Scheme
Le chapitre précédent nous a permis d’introduire certaines notions fondamentales de Scheme par l’intermédiaire des opérations qui nous sont les plus habituelles, les fonctions arithmétiques. Mais le langage a aussi été
conçu pour permettre d’autres types d’opérations, les manipulations symboliques.
3.1
Symboles
La plupart des langages de programmation permettent la représentation de données symboliques, habituellement sous la forme de chaı̂nes de caractères. Mais une chaı̂ne n’est jamais qu’une suite de caractères, dont la
sémantique est très éloignée du concept qu’un mot peut recouvrir. Ainsi, peut-on écrire :
Paris est la capitale de la France. “Paris” est un mot de cinq lettres.
La première phrase fait référence à une ville, la seconde est une constatation au sujet d’un mot. Dans ce dernier
cas, les guillemets indiquent que l’on s’intéresse à ce mot particulier, et non au concept . La même distinction
existe en Scheme (et en Lisp). Les chaı̂nes de caractères permettent de réaliser des manipulations textuelles, les
symboles des traitements conceptuels, ou symboliques.
Cependant une ligne telle que :
x
représente une expression Scheme valide, dont l’effet est d’évaluer et d’imprimer la valeur de la variable x. Que
faire si nous voulons imprimer, non pas la valeur de la variable x, mais le symbole x ? Il nous faut faut une
méthode pour indiquer au système de ne pas évaluer x, mais de l’utiliser en tant que valeur litérale.
L’examen de Septembre 91 du cours du MIT “6.001–Structure and Interpretation of computer programs”
débutait par ces deux questions :
Write your name here:
Write “your name” here:
Le corrigé nous apprend que “examples of acceptable answers are: Eric Grimson and your name.” Cet exemple
illustre la nécessité, même dans la vie courante, d’une notation spéciale pour distinguer les données littérales
des autres éléments du discours.
Le mécanisme qui nous permet d’obtenir cet effet en Scheme est la forme spéciale quote :
(quote x)
=⇒
x
quote a donc pour mission de fournir son paramètre sous forme textuelle, sans évaluation :
(quote toto)
(quote 1423)
(quote "hello")
=⇒
=⇒
=⇒
toto
1423
"hello"
31
32
CHAPITRE 3. ELÉMENTS DE SCHEME
Ce dernier exemple nous montre une donnée qui représente sa propre valeur : l’évaluation de (quote 1423),
tout comme celle de 1423, fournit 1423 (mais ceci n’est pas nécessairement pour nous surprendre : les deux
phrase : dis bonjour et dis “bonjour” n’appellent-elles pas à la même action?).
Considérons maintenant cet exemple de dialogue avec MIT-Scheme :
1 ]=> (quote
;Value: toto
1 ]=> (quote
;Value: toto
1 ]=> (quote
;Value: toto
1 ]=> (qUotE
;Value: toto
toto)
Toto)
TOTO)
toTO)
La fonction quote ne tient manifestement pas compte de la distinction entre lettres majuscules et minuscules.
Mieux (ou pire), le système lui-même ne fait pas cette distinction (comme le montre le dernier exemple) dans
les expressions qui sont introduites. Ce système Scheme particulier choisit de représenter tous les atomes en
convertissant les majuscules en minuscules. D’autres versions de Scheme ou de Lisp, tout autant case insensitive,
n’utiliseront que les majuscules, d’autres enfin tiendront compte des deux formes, et Toto sera considéré différent
de toto.1 Seules les chaı̂nes de caractères échappent à cette conversion :
1 ]=> (quote "Hello")
;Value: "Hello"
Cet outil (la forme quote) qui permet la désignation d’objets symboliques à l’intérieur d’expressions Scheme
est très fréquemment utilisé. Il lui correspond une notation particulière, le caractère apostrophe ’. Il y a identité
entre les expressions suivantes :
(quote abc)
⇔
’abc
Dans la pratique, on peut considérer la forme quote comme un mécanisme destiné à bloquer l’évaluation d’une
expression, celle-ci pouvant être une variable, mais aussi une expression parenthésée quelconque :
1 ]=> (+ 2 3)
;Value: 5
1 ]=> ’(+ 2 3)
;Value: (+ 2 3)
3.2
Opérations sur données symboliques
3.2.1
Les mystères de la comparaison
Que peut-on faire avec des objets symboliques? Les comparer entre eux, les conserver dans des structures,
les utiliser de mille et une manières.
Les comparaisons font appel à une fonction de base, eq?, qui permet de tester l’identité de deux objets (c.f.
[R4R90], § 6.2). La règle est la suivante : le système répond vrai s’il juge que les deux objets sont égaux2.
Dans le cas de MIT-Scheme, on obtient les résultats suivants3 :
1 ]=> (eq?
;Value: #T
1 ]=> (eq?
;Value: #T
1 ]=> (eq?
;Value: #T
1 ]=> (eq?
;Value: #T
(quote toto) (quote toto))
125 125)
4 (+ 2 2))
(+ 1 3) (* 2 2))
1 Est-ce mieux, est-ce moins bien? Cet argument agite, non seulement la communauté Lisp, mais tout le monde de l’informatique,
depuis des décennies, et l’on pourrait remplir des livres entiers avec les arguments des uns et des autres. Nous nous en tiendrons
ici, pour rester cohérent avec le système utilisé, aux seules lettres minuscules.
2 ce
qui dépend naturellement du système utilisé.
3 souvenons
nous que la notation () en MIT-Scheme représente aussi la valeur false #f.
3.2. OPÉRATIONS SUR DONNÉES SYMBOLIQUES
1 ]=> (eq?
;Value: #T
1 ]=> (eq?
;Value: ()
1 ]=> (eq?
;Value: ()
1 ]=> (eq?
;Value: ()
1 ]=> (eq?
;Value: ()
1 ]=> (eq?
;Value: ()
33
100000 100000)
(* 100000 100000) (* 100000 100000))
12345678987654321 12345678987654321)
12.4 12.4)
(quote (+ 2 3)) (quote (+ 2 3)))
"Hello" "Hello")
Deux exemplaires du symbole toto sont considérés identiques. De même, des petits entiers sont identiques. Par
contre, les grands nombres, les valeurs flottantes, les listes sont différentes.
Ces résultats surprenants sont dûs au fait que eq? est une fonction de très bas niveau. La fonction eq? compare ses paramètres, et rend vrai si ceux-ci sont identiques. Deux petits entiers égaux (au sens mathématique),
des caractères sont représentés par le même opérande immédiat, une certaine chaine de bits. Cette représentation est identique dans le cas de la constante 4 et du résultat de l’évaluation de (+ 2 2). Par contre, les entiers
longs tels 12345678987654321, les nombres flottants ou complexes, les listes, les chaı̂nes de caractères, sont
représentés par l’adresse en mémoire centrale où l’objet est conservé. Ces adresses sont différentes, même pour
deux objets qui semblent identiques. La fonction eq? rend donc faux dans ce cas.
1 ]=> (define truc 12.4)
;Value: truc
1 ]=> truc
;Value: 12.4
1 ]=> (eq? truc truc)
;Value: #T
1 ]=> (eq? truc 12.4)
;Value: ()
1 ]=> (define machin "Hello")
;Value: machin
1 ]=> (eq? machin machin)
;Value: #T
1 ]=> (eq? truc machin)
;Value: ()
Ici, nous avons défini une variable truc, de valeur 12.4. La comparaison de cette variable avec elle-même fournit
naturellement vrai : les deux opérandes sont identiques, car ils désignent la même valeur en mémoire centrale.
Il en est de même si eq? reçoit deux adresses identiques de chaı̂nes de caractères.
Scheme propose deux autres fonctions générales de comparaison : eqv? et equal?.
1 ]=> (eqv? (quote toto) (quote toto))
;Value: #T
1 ]=> (eqv? (* 100000 100000) (* 100000 100000))
;Value: #T
1 ]=> (eqv? truc 12.4)
;Value: #T
1 ]=> (eqv? (quote (+ 2 3)) (quote (+ 2 3)) )
;Value: ()
1 ]=> (eqv? "Hello" "Hello")
;Value: ()
1 ]=> (equal? (quote toto) (quote toto))
;Value: #T
1 ]=> (equal? (* 100000 100000) (* 100000 100000))
;Value: #T
1 ]=> (equal? truc 12.4)
;Value: #T
1 ]=> (equal? (quote (+ 2 3)) (quote (+ 2 3)) )
;Value: #T
1 ]=> (equal? "Hello" "Hello")
;Value: #T
34
CHAPITRE 3. ELÉMENTS DE SCHEME
La fonction eqv? est voisine de eq?, en élargissant le champ d’application de cette dernière aux nombres. La
fonction equal? généralise cette notion d’équivalence : deux objets sont égaux s’ils ont même type interne, et
même représentation externe. Notons que la seule fonction qui reconnaisse l’égalité de 125 (un nombre entier)
et de 125.0 (un nombre flottant) est la fonction de comparaison numérique “=” :
1 ]=> (eq? 125 125.0)
;Value: ()
1 ]=> (eqv? 125 125.0)
;Value: ()
1 ]=> (equal? 125 125.0)
;Value: ()
1 ]=> (= 125 125.0)
;Value: #T
Il existe de même des fonctions spécialisées de comparaison des chaı̂nes de caractères :
1 ]=> (string=? "Hello" "Hello")
;Value: #T
1 ]=> (string=? "Hello" "HELLO")
;Value: ()
1 ]=> (string-ci=? "Hello" "HELLO")
;Value: #T
(la dernière étant case insensitive, ce qui est symbolisé par la partie “-ci” du suffixe.)
3.3
Listes
Nous avons déja rencontré les listes à plusieurs reprises. Un programme Scheme est (hormi les plus triviaux)
composé d’expressions fonctionnelles, donc de listes. En Scheme, une liste est une collections d’items placés entre
parenthèses. Ainsi, (toto titi tutu) est une liste contenant les trois symboles toto, titi et tutu. La liste
la plus utilisée est probablement la liste vide, notée () ! Les listes peuvent être introduites dans un programme
sous la forme d’expressions symboliques grâce à la fonction quote :
1 ]=> (define (is-hello-world truc)
(equal? truc ’(hello world)))
;Value: is-hello-world
1 ]=> (is-hello-world 125)
;Value: ()
1 ]=> (is-hello-world ’(hello world))
;Value: #T
1 ]=> (is-hello-world ’(hello world goodbye world))
;Value: ()
Naturellement, il existe de multiples fonctions de manipulation de listes : création, extraction d’éléments, etc.
La fonction de création de listes est la fonction list. Elle accepte un nombre arbitraire de paramètres, et
fournit la liste contenant ces paramètres :
1 ]=> (list 2 3 5)
;Value: (2 3 5)
1 ]=> (list ’hello ’world)
;Value: (hello world)
1 ]=> (list 3 (+ 4 5) (sqrt (* 2 7)) 9)
;Value: (3 9 3.7416573867739413 9)
1 ]=> (list)
;Value: ()
Dans ces exemples, la fonction list est utilisée successivement avec 3, 2, 4, et 0 paramètres. Dans ce dernier
cas, le résultat est la liste vide, ().
En vérité, list n’est pas la fonction de base pour la création des listes. La fonction élémentaire est cons4 ,
qui permet la création d’une cellule, ou doublet , ou paire-pointée :
4 que
l’on prononce à l’américaine, conssssse, en insistant sur le s final.
35
3.3. LISTES
1 ]=> (cons 1 2)
;Value: (1 . 2)
1 ]=> (cons ’hello ’world)
;Value: (hello . world)
Cette nouvelle structure est l’élément de base, la brique élémentaire dont sont faites les listes. Une paire pointée
est formée de deux entités, le car et le cdr . Ces deux champs peuvent désigner n’importe quelle valeur, nombre,
chaı̂ne, symbole, ou une autre paire pointée :
1 ]=> (cons 2 ’z)
;Value: (2 . Z)
1 ]=> (cons "abc" ’abc)
;Value: ("abc" . ABC)
1 ]=> (cons (cons ’a "b") 3)
;Value: ((A . "b") . 3)
Les fonctions car et cdr permettent d’extraire les valeurs des champs correspondants d’une paire pointée :
1 ]=> (car (cons 1 2))
;Value: 1
1 ]=> (cdr (cons ’hello ’world))
;Value: world
1 ]=> (cdr (car (cons (cons ’toto ’titi) ’tutu)))
;Value: titi
Une liste est un assemblage de paires pointées :
’(a b c d)
⇔
⇔
⇔
⇔
(cons
(cons
(cons
(cons
’a ’(b c d))
’a (cons ’b ’(c d)))
’a (cons ’b (cons ’c ’(d))))
’a (cons ’b (cons ’c
(cons ’d ’()))))
La liste vide () est, dans les systèmes Lisp traditionnels, une autre notation de l’atome nil.
La figure 3.1 explicite la structure, en termes de paires pointées, de différentes listes.
Les fonctions car et cdr permettent l’accès aux divers éléments d’une liste :
(car
(car
(car
(car
’(a b c d))
=⇒
a
(cdr ’(a b c d)))
=⇒
b
(cdr (cdr ’(a b c d))))
=⇒
(cdr (cdr (cdr ’(a b c d)))))
c
=⇒
d
Une erreur est signalée si les fonctions car ou cdr sont appliquées à autre chose que des paires pointées.5
Signalons aussi l’existence de fonctions réalisant des combinaisons, jusqu’à quatre niveaux, de car et de cdr,
dont voici quelques représentants :
(caar -obj .)
(cadr -obj .)
(caaar -obj .)
(caaaar -obj .)
(caadar -obj .)
⇔
⇔
⇔
⇔
⇔
(car
(car
(car
(car
(car
(car
(cdr
(car
(car
(car
-obj .))
-obj .))
(car -obj .)))
(car (car -obj .))))
(cdr (car -obj .))))
Ces fonctions (caar, cadr, cdar, cddr, caaar, caadr, cadar, caddr, cdaar, cdadr, cddar, cdddr, caaaar,
caaadr, caadar, caaddr, cadaar, cadadr, caddar, cadddr, cdaaar, cdaadr, cdadar, cdaddr, cddaar, cddadr,
cdddar, et cddddr, pour ne pas en oublier) permettent une écriture plus concise et plus efficace des programmes. Cependant, on préfèrera toujours, à l’utilisation brute (caddr fiche-ss) une écriture telle que
(prenom fiche-ss), où prenom est défini par :
(define (prenom fiche) (caddr fiche))
ne serait-ce que pour comprendre le programme trois mois plus tard, ou être capable de modifier la représentation
des données sans devoir réécrire le tout de A à Z...
Notons que la représentation des listes est “universelle” en ce sens que l’expression Scheme :
(+ 3 5)
5 Certains
systèmes autorisent l’application de ces fonctions sur la liste vide, fournissant la liste vide comme résultat.
36
CHAPITRE 3. ELÉMENTS DE SCHEME
Fig. 3.1 - Quelques structures de Listes
La liste (A B)
.....
..
.....
A
B
!
"
"!
!"
!
"
La liste (A B C)
.....
..
.....
A
B
C
!
"
"!
!"
!
"
D
!
"
"!
!"
!
"
.....
..
.....
C
!
"
"!
!"
"
!
.....
..
.....
B
!
"
"!
!"
!
"
.....
..
.....
La liste ((A) (B C) D)
.....
..
.....
..... ....
...
A
.....
..
.....
..... ....
...
!
"
"!
!"
"
!
B
Le resultat de (cons x x) avec x = (A B)
..... .....
...
.....
..
.....
A
et la donnée symbolique qui est le résultat de l’évaluation :
1 ]=> ’(+ 3 5)
;Value: (+ 3 5)
sont représentées par des structures de données absolument similaires. Ce dernier résultat est représenté par
l’assemblage de paires pointées suivant :
(+ . (3 . (5 . ())))
assemblage qui est également une expression Scheme valide. Il suffit pour s’en convaincre de la proposer au
système :
1 ]=> (+ . (3 . (5 . ())))
;Value: 8
3.4. PRÉDICATS UTILES
3.4
37
Prédicats utiles
Il nous faut maintenant disposer de prédicats permettant de distinguer l’un de l’autre les différents types d’objets. C’est le rôle de ces prédicats : boolean?, symbol?, pair?, number?, char?, vector?, string?, procedure?.
Nous avons déjà rencontré la plupart de ces objets. Voici une brève description de leurs propriétes :
– Il y a deux objets de type booléen, notés #t et #f (c.f. [R4R90], § 6.1). Le prédicat boolean? est vrai
pour ces objets, faux pour tout autre valeur. Dans le langage strict [R4R90], les booléens sont distincts
de tous les autres éléments du système. Certaines implantations de Scheme (c’est le cas de MIT-Scheme)
confondent le booléen #f et la liste vide ().
– Les symboles jouent un rôle important dans les systèmes Lisp, aussi bien par leur rôle en tant que variables
que par leur utilisation comme données symboliques. Les symboles offrent la propriété d’unicité dans un
système Lisp : deux symboles formés de suites identiques de caractères (après conversion éventuelle en
minuscules ou majuscules) sont reconnus comme identiques par la fonction eq? (et a fortiori eqv? ou
equal? :
(eq? ’NaBuChodonoSaure ’nabuchoDONAUSAUrE)
=⇒
#t
Les symboles sont caractérisés par le prédicat symbol?.
– Les nombres sont distingués par le prédicat number?. Il existe une hiérarchie entre les nombres correspondant au sous-typages : le type entier est un sous-type du type rationnel , qui est un sous-type du
type réel , qui est un sous-type du type complexe, qui est un sous-type du type numérique. Les prédicats
correspondants sont integer?, rational?, real?, complex?, number?.
Une distinction supplémentaire est apportée par la notion d’exactitude. De nombreux systèmes Scheme
(c’est le cas de MIT-Scheme) implantent des entiers en précision illimité. Les rationnels peuvent également
être représentés sous la forme du rapport de deux entiers premiers entre eux. Les nombres peuvent donc
être exacts ou inexact . Les prédicats correspondants sont exact? et inexact? (c.f. [R4R90], § 6.5).
– Les paires pointées sont reconnues par le prédicat pair?. Appliqué à une liste non vide (qui est donc
composée de paires pointées), le prédicat rend vrai. Les listes non vides constituent un sous-type des
paires-pointées : formellement, une liste est une paire pointée dont le cdr contient soit la liste vide, soit
la désignation d’une autre liste. Le prédicat list? rend vrai pour toute liste (y compris la liste vide). Le
prédicat null? rend vrai pour la liste vide.6
– Les chaı̂nes de caractères sont des séquences de caractères, placées entre double quote ". Un petit nombre
de fonctions s’appliquent à ce type (c.f. [R4R90], § 6.7). La fonction caractéristique des chaı̂nes est string?.
En ce qui nous concerne, l’utilisation des chaı̂nes ne présente pas de caractère essentiel.
– Les vecteurs constituent une structure de données dont les éléments sont désignés par des entiers (le premier
a pour numéro 0). Les éléments d’un vecteur peuvent être quelconques. Le prédicat vector? permet de
reconnaı̂tre un vecteur. Tout comme les chaı̂nes, les vecteurs ne présentent pas d’intérêt particulier dans
notre présentation des techniques de programmation applicative. On se reportera à [R4R90], § 6.8 pour
une description générale des vecteurs.
– Les caractères sont des objets qui représentent des éléments imprimables, tels les chiffres, les lettres, les
signes spéciaux. Le prédicat char? caractérise ces objets. Les chaines de caractères sont composées de
caractères. Diverses fonctions et prédicats s’appliquent sur les caractères (c.f. [R4R90], § 6.6), permettant
des conversions de caractères en entiers ou en chaı̂nes, et réciproquement.7
– Les fonctions, qui sont caractérisées par le prédicat procedure? correspondent également à un type primitif
du langage. Nous reviendrons plus en détail sur le sujet au chapitre 4.
– Signalons enfin que les systèmes Scheme peuvent offrir d’autres types primitifs d’objets, qui sont disjoints des 8 présentés ici : ports, utilisés pour représenter des organes d’entrée/sortie, tables hashées, flots,
promesses, environnements, continuations, paires faibles, etc. Certains de ces types seront présentés ultérieurement dans ce cours.
6 Techniquement,
le résultat de (cons ’a (cons ’b ’c)) n’est pas une liste — puisque le dernier élément n’est pas la liste vide
— mais de nombreux systèmes répondent vrai si le prédicat list? est appliqué à cet objet.
7 Les caractères correspondent typiquement aux codes ASCII ou EBCDIC , c’est à dire à des codes à 7 ou 8 bits ; certaines
implantations de Lisp proposent des codes étendus (par exemple 12 à 14 bits), les bits de poids fort correspondant à des modifieurs
comme contrôle, super , hyper , méta, etc.
38
CHAPITRE 3. ELÉMENTS DE SCHEME
3.5
Structures de Controles
Il n’est pas inutile, à ce stade, de consacrer quelques instants à présenter les autres formes de contrôle du
langage Scheme. Bien que toutes puissent s’exprimer au moyen de la forme de base, if, elles sont souvent utiles
pour alléger l’écriture des procédures.
3.5.1
La forme cond
Cette forme spéciale obéit à la syntaxe suivante :
(cond -W1 . -W2 . ... -Wn .)
dans laquelle les -Wi . sont eux-mêmes des couples parenthésés de la forme :
(-Ti . -Ei .)
Une expression cond s’exécute par évaluation des -Ti . des clauses successives, dans l’ordre, jusqu’à ce que
l’une d’elles fournisse la valeur “vrai” (c.f. section 2.4). Quand une expression -Ti . fournit la valeur “vrai”,
l’expression -Ei . est évaluée, et le résultat de cette évaluation devient le résultat de la forme cond.
Si toutes les expressions -Ti . fournissent le résultat “faux”, le résultat de l’expression conditionnelle est non
spécifié. Le dernier des couples -Wi . peut avoir la syntaxe suivante :
(else -En .)
Si tous les -Ti . fournissent le résultat “faux”, et qu’une telle clause apparait, l’expression -En . est évaluée et
devient le résultat de l’expression cond.
Un exemple : Fibonacci
La fonction de Fibonacci est ainsi définie :
F ib(0)
= 0
F ib(1)
= 1
F ib(n + 2) = F ib(n + 1) + F ib(n)
L’écriture récursive est l’occasion de mettre en action la forme cond :
(define (fib
(cond
((= n
((= n
(else
n)
0) 0)
1) 1)
(+ (fib (- n 1)) (fib (- n 2))))))
Fonction d’Ackerman
Programmez la fonction d’Ackerman, A(n, p), ainsi définie :
A(0, p)
= p+1
A(n + 1, 0)
= A(n, 1)
A(n + 1, p + 1) = A(n, A(n + 1, p))
Soient les fonctions suivantes :
f (p)
g(p)
h(p)
i(p)
j(p)
=
=
=
=
=
A(0, p)
A(1, p)
A(2, p)
A(3, p)
A(4, p)
Pouvez-vous donner des définitions non récursives (c’est à dire qui ne soient pas elles-mêmes récursives, ou ne
fassent pas appel à des fonctions récursives) de ces fonctions, f , g, h, i et j ?
39
3.6. APPLICATIONS
Remarque
Nous avons signalé que ces structures de contrôle pouvaient s’exprimer en termes de if. Réciproquement,
if peut aussi s’exprimer en termes de cond (par exemple). Cependant, les choses ne sont pas aussi simples qu’il
le semble. Voici une version d’une procédure qui prétend égaler le if originel :
(define (mon-if test cas-vrai cas-faux)
(cond
(test cas-vrai)
(else cas-faux)))
Quelques essais superficiels semblent confirmer que cette procédure fonctionne correctement :
1 ]=> (mon-if (= 2 3) 0 5)
;Value: 5
1 ]=> (mon-if (> 5 2) 1 (- 2 7))
;Value: 1
Pourtant... Sauriez-vous mettre en évidence le vice caché de cette formulation?
3.5.2
Les formes or et and
Ces deux formes spéciales ont comme syntaxe :
(or -e1 . -e2 . ... -en .)
(and -e1 . -e2 . ... -en .)
La forme or évalue successivement -e1 . -e2 .... jusqu’à rencontrer une expression -ei . dont la valeur soit différente
de #f. or fournit alors comme résultat la valeur de cette expression, les -ei+1 . ... -en . n’étant pas exécutées. or
rend #f si toutes les -ei . rendent #f.
La forme and évalue successivement -e1 . -e2 . ... jusqu’à rencontrer une expression -ei . dont la valeur soit
#f. and fournit alors #f comme résultat, les -ei+1 . ... -en . n’étant pas exécutées. Si toutes les -ei . fournissent
un résultat différent de #f, and rend comme résultat celui de -en ..
Exemples
Les formes and et or sont particulièrement utiles pour réaliser des prédicats. Ainsi, tester qu’un objet x est
un multiple de 3 peut s’écrire :
(and (integer? x) (zero? (modulo x 3)))
Vérifier que l’entier x est divisible par 2, 3 ou 7 peut s’écrire :
(or (zero? (modulo x 2)) (zero? (modulo x 3)) (zero? (modulo x 7)))
3.6
Applications
Voici quelques exemples typiques (à notre niveau) d’exercices sur les listes.
3.6.1
Parcours de listes
La première technique à acquérir est celle du parcours de listes. Les algorithmes typiques testent si la liste
est vide, applique une certaine opération au premier élément (le car ), puis récursent pour traı̂ter le reste de la
liste (le cdr ). Un exemple classique de parcours de listes est celui de la fonction primitive length qui calcule la
longueur (le nombre d’éléments) d’une liste. Comment l’écririez-vous?
(length ’(a b c d e))
=⇒
5
La fonction suivante a pour rôle de calculer la somme des éléments d’une liste. Elle n’est pas très robuste :
l’utilisation d’une liste contenant un élément non numérique provoquera une erreur.
(define (sommer liste)
(if (list? liste)
(if (pair? liste)
(+ (car liste) (sommer (cdr liste)))
0)
0))
40
CHAPITRE 3. ELÉMENTS DE SCHEME
Celle-ci vérifie que son paramètre est bien une liste de nombres :
(define (list-of-numbers? l)
(if (null? l)
#t
(if (pair? l)
(if (number? (car l))
(list-of-numbers? (cdr l))
#f)
#f)))
On notera que la récursion s’interrompt dès que possible, autrement dit, dès qu’un élément non numérique est
rencontré dans la liste.
La formulation suivante est peut-être plus lisible :
(define (list-of-numbers? l)
(or (null? l)
(and (pair? l)
(number? (car l))
(list-of-numbers? (cdr l)))))
Elle fait appel aux deux formes spéciales, or et and, que nous venons de présenter (c.f. 3.5.2).
3.6.2
Génération de listes
Tout comme le parcours de listes fait appel aux fonctions car et cdr , la génération de liste fait une grosse
consommation de cons.8
iota
Notre première fonction crée une liste des n premiers entiers :
(define (iota n)
(if (= n 0)
’()
(cons n (iota (- n 1)))))
(iota 12)
=⇒
(12 11 10 9 8 7 6 5 4 3 2 1)
Le résultat n’est pas très agréable à l’oeil. Comment remettre cette liste dans l’ordre? Essayer de trouver une
nouvelle formulation.
reverse
On peut envisager, pour résoudre le problème précédent, l’écriture d’une fonction permettant de renverser
une liste :
(rev ’(a b c d e))
=⇒
(e d c b a)
Certains prétendent que la fonction suivante peut rendre ce service. Qu’en pensez-vous?
(define (rev l)
(if
(null? l)
l
(if
(null? (cdr l))
l
(cons
(car (rev (cdr l)))
(rev (cons
(car l)
(rev (cdr (rev (cdr l))))))))))
Essayez de proposer d’autres versions de cette fonction de réversion de listes. Comparer leur fonctionnement
avec la fonction primitive reverse.
8 d’ailleurs
cons vient de consommer.
41
3.7. EXERCICES D’APPLICATION
3.7
Exercices d’application
Après avoir étudié les différentes fonctions de manipulation de listes (c.f. [R4R90], § 6.3), écrivez les fonctions
suivantes :
append
Proposer votre version de la fonction append, dont le rôle est de créer une liste en plaçant bout à bout ses
deux paramètres (c.f. [R4R90], § 6.3) :
=⇒
(append ’(a b) ’(x y z))
(a b x y z)
Recherche en liste
Trois fonctions Scheme permettent les recherches dans des listes :
(memq -obj . -list.)
(memv -obj . -list.)
(member -obj . -list.)
Ces procédures fournissent la sous-liste de -list. dont le car est égal à -obj .. Si l’élément n’est pas trouvé, elles
fournissent la valeur #f. Les fonctions diffèrent entre elles par le prédicat de comparaison utilisé : eq? pour memq,
eqv? pour memv, et equal? pour member. Ex :
1 ]=> (memq ’titi ’(tata toto titi tutu))
;Value: (titi tutu)
1 ]=> (memq ’jojo ’(tata toto titi tutu))
;Value: ()
pick
pick permet d’accéder aux éléments d’une structure quelconque, en précisant un chemin d’accès :
m
(pick ’(1 2) m)
(pick ’(0) m)
(pick ’() m)
=⇒
=⇒
=⇒
=⇒
((a b c) (d e f g h i j))
f
(a b c)
((a b c) (d e f g h i j))
rho
rho crée une liste de longueur égale au premier argument, à partir des éléments du second argument :
(rho 3 ’(a b c d e))
(rho 8 ’(a b c d e))
=⇒
=⇒
(a b c)
(a b c d e a b c)
rotate
rotate réalise une rotation circulaire, de valeur spécifiée par le premier argument, sur les éléments du second :
(rotate 3 ’(a b c d e))
(rotate 6 ’(a b c d e))
(rotate -1 ’(a b c d e))
=⇒
=⇒
=⇒
(d e a b c)
(b c d e a)
(e a b c d)
transpose
transpose réalise une transposition de son argument. Celui-ci est une liste de listes. Si l’on désigne un
élément par son indice i dans la liste j du paramètre, il se retrouve, dans le résultat, comme élément j de la
liste i :
(transpose ’((a b) (c d) (e f)))
=⇒
((a c e) (b d f))
42
CHAPITRE 3. ELÉMENTS DE SCHEME
Comptage
Un grand classique, compter les symboles d’une structure quelconque :
(compte-symboles ’((john paul george ringo)
(axl slash izzy duff (steven matt))
(james lars kirk jason) ()))
=⇒
14
Mise a plat
Mettre à plat une structure quelconque :
(a-plat ’((john paul george ringo)
(axl slash izzy duff (steven matt))
(james lars kirk jason) ()))
=⇒
(john paul george ringo axl slash izzy duff
steven matt james lars kirk jason)
Permutations
Générer toutes les permutations des éléments d’une liste :
(permutations ’(a b c))
=⇒
((a b c) (a c b) (b a c) (b c a) (c a b) (c b a))
(permutations ’((belle marquise) ... ))
PGCD
Calculer le PGCD d’une liste de nombres. On peut essayer les deux méthodes suivantes : utiliser n − 1 fois
une fonction auxiliaire calculant le PGCD de deux nombres, ou (mieux), tenir compte de l’ensemble des nombres
à chaque étape.
Modulo - encore
On donne deux listes de nombres naturels, a1 , a2 . . . , an et m1 , m2 . . . , mn , tels que ai = x mod mi pour
un certain x. Reconstituer le plus petit x positif satisfaisant l’ensemble de ces relations.
Euclide étendu
Etant donnée une liste de nombres naturels (a b c d e...) construire la liste des entiers (x y z...) tels
que :
ax + by + cz + · · · = gcd(a, b, c, . . .)
3.8
Un exemple complet
Nous nous proposons de réaliser une mini-application complète, constituant un micro système expert.
Nous nous basons sur un article de M. Gondran, “Introduction aux systèmes experts” [Gon83], qui décrit une
petite base de données dans
& le domaine de la botanique, dans laquelle la proposition la plante est une plante à
fleurs se note fleur, où représente la connexion logique, et ¬ la négation. Voici un ensemble de propositions :
&
a) SI fleur
graine&ALORS phanerogame
b) SI phanerogame & graine-nue ALORS sapin
c) SI phanerogame & 1-cotyledone ALORS monocotyledone
d) SI phanerogame 2-cotyledone
ALORS dicotyledone
&
e) SI monocotyledone rhizome ALORS muguet
f ) SI dicotyledone ALORS
anemone
&
g) SI monocotyledone
¬
rhizome
ALORS lilas
&
h) SI feuille
¬ fleur
ALORS
cryptogame
&
i) SI cryptogame & ¬ racine ALORS mousse
j) SI cryptogame
racine ALORS fougere
&
k) SI ¬ feuille &
plante ALORS thallophyte
l) SI thallophyte chlorophylle ALORS algue
43
3.8. UN EXEMPLE COMPLET
&
m) SI thallophyte
¬ chlorophylle
ALORS champignon
&
&
n) SI ¬ feuille
¬ fleur ¬ plante ALORS collibacille
La proposition g, par exemple, signifie : “si la plante est une monocotylédone et ne possède pas de rhizome, il
s’agit du lilas”.
Parmi les conséquences possibles des propositions, certaines représentent des étapes intermédiaires de la
détermination de l’espèce, d’autres sont des conclusions :
sapin
fougere
muguet
algue
anemone
champignon
lilas
colibacille
mousse
Une représentation possible pour la base de données est une liste formée de suites -conclusion. -prémisses.,
les -prémisses. contenant l’ensemble des faits nécessaires (vrais et faux) pour que la -conclusion. soit vérifiée.
La proposition g peut ainsi se traduire par :
lilas (monocotyledone - rhizome)
Ici, le symbole - est utilisé comme marqueur pour indiquer la négation du fait qui suit. Voici, selon cette
convention, une définition de la base de données :
1 ]=>
(define BDD ’(
phanerogame (fleur graine)
sapin (phanerogame graine-nue)
monocotyledone (phanerogame 1-cotyledone)
dicotyledone (phanerogame 2-cotyledone)
muguet (monocotyledone rhizome)
anemone (dicotyledone)
lilas (monocotyledone - rhizome)
cryptogame (feuille - fleur)
mousse (cryptogame - racine)
fougere (cryptogame racine)
thallophyte (- feuille plante)
algue (thallophyte chlorophylle)
champignon (thallophyte - chlorophylle)
collibacille (- feuille - fleur - plante)
))
Une autre liste contient les buts possibles des déductions :
1 ]=>
(define BUTS ’(
sapin muguet anemone lilas mousse
fougere algue champignon collibacille))
La méthode la plus simple de résolution du problème utilise la technique dite de saturation, qui consiste à
essayer toutes les hypothèses de la base de données, en s’appuyant sur les faits connus (vrais ou faux), et en
ajoutant à ces faits ceux que l’on peut déduire des relations de la base de connaissances.
Supposons que notre problème soit de déterminer l’espèce dont l’observation des caractéristiques nous apprend les faits suivants : elle a un rhizome, une fleur, des graines à un cotylédon. L’ensemble de faits se représente,
suivant nos conventions, par la liste :
(rhizome fleur graine 1-cotyledone)
Les déductions possibles font évoluer les faits de la manière suivante :
(fleur graine) =⇒ phanerogame
(phanerogame 1-cotyledone) =⇒ monocotyledone
(monocotyledone rhizome) =⇒ muguet
Le dernier fait déduit est l’un des buts possibles, ce qui termine la recherche.
Comment mettre en place une solution du problème ? Nous nous appuyons sur deux ensembles fixes de
données, qui sont la base de données, BDD et l’ensemble des buts, BUTS. D’autre part, chaque détermination
utilise des faits observés, que nous rangerons dans deux listes, vrais et faux, qui contiennent les faits relatifs
au problème. Proposons comme interface générale :
(deduire -vrais. -faux . -BDD . -BUTS .)
44
CHAPITRE 3. ELÉMENTS DE SCHEME
dont voici quelques exemples d’utilisation :
1 ]=>
(deduire ’(rhizome fleur graine 1-cotyledone)
’() BDD BUTS)
;Value: muguet
1 ]=> (deduire ’(fleur graine 2-cotyledone)
’() BDD BUTS)
;Value: anemone
1 ]=> (deduire ’()
’(fleur feuille plante) BDD BUTS)
;Value: collibacille
1 ]=> (deduire ’(thallophyte)
’(chlorophylle) BDD BUTS)
;Value: champignon
1 ]=> (deduire ’(chlorophylle)
’(fleur feuille rhizome) BDD BUTS)
;Value: ()
Le dernier exemple ne permet d’arriver à aucune conclusion : la procedure le signifie en fournissant la valeur
“faux” (il eut fallu préciser que la chose était une plante pour que le programme reconnaisse une algue).
Que doit faire la fonction deduire ? Elle doit déduire de nouveaux faits, les ajouter aux faits reconnus
comme “vrais”, jusqu’à obtenir l’un des buts. Faute de meilleure méthode, une façon d’obtenir de nouveaux
faits consiste à essayer chaque entrée de la base des faits, de tenter de le valider avec les informations dont on
dispose (faits réputés vrais et faux), et de l’ajouter à la base des faits vrais.
Une première procédure, tente, accepte comme premier paramètre une liste contenant un extraı̂t de la base,
c’est à dire une liste dont le premier élément est une conclusion, le second une liste de prémisses. Les autres
paramètres sont vrais et faux, les listes des faits localement tenus pour vrais et faux. La fonction s’écrit :
(define (tente truc vrais faux)
(if (valide? (cadr truc) vrais faux)
(car truc)
#f))
Elle fournit en résultat soit la conclusion correspondante, si les prémisses sont vérifiées par les faits, soit #f.
La procédure valide? va réaliser la validation effective. Les éléments des prémisses sont recherchés soit dans
faux, s’ils sont précédés de -, soit dans vrais :
(define (valide? proposition vrais faux)
(or
(null? proposition)
(and (eq? ’- (car proposition))
(memq (cadr proposition) faux)
(valide? (cddr proposition) vrais faux))
(and (memq (car proposition) vrais)
(valide? (cdr proposition) vrais faux))))
La proposition est validée lorsque l’on a vérifié l’ensemble des faits, c’est à dire lorsque le paramètre est une
liste vide.
L’analyse de la base, à la recherche d’un fait nouveau, est réalisée par un couple de procédures mutuellement
récursives :
(define (balaye base vrais faux)
(if (null? base)
#f
(essai (tente base vrais faux) base vrais faux)))
(define (essai fait base vrais faux)
(if (and fait (not (memq fait vrais)))
fait
(balaye (cddr base) vrais faux)))
La procédure balaye va passer en revue l’ensemble des règles de la base, renvoyant #f en fin de parcours. Pour
chaque proposition, tente est appelée, et son résultat transmis à essai. Si ce résultat, ce fait potentiel, n’est
3.8. UN EXEMPLE COMPLET
45
pas #f et n’est pas déja connu, il est fourni comme résultat ; dans le cas contraire, essai procède à un appel de
balaye sur le reste de la base. Ce couple de procédures fournit donc soit un fait “nouveau”, dans essai, soit
encore #f, dans balaye, lorsque l’on a examiné toute la base et que rien de neuf n’a été découvert. On notera
que ces deux procédures sont terminales récursives, et travaillent donc dans un espace mémoire constant.
L’itération principale va elle-même faire appel à une technique similaire. Le jeu consiste cette fois-ci à
chercher un fait nouveau ; si c’est un des buts, la solution est trouvée ; sinon, on réitère en ajoutant ce fait
nouveau en tête de la liste vrais des fait connus :
(define (itere vrais faux)
(itere2 (balaye BDD vrais faux) vrais faux))
(define (itere2 fait vrais faux)
(cond
((not fait) #f)
((memq fait BUTS) fait)
(else (itere (cons fait vrais) faux))))
La procedure itere demande un fait nouveau et le transmet à itere2. Cette dernière examine les différents
cas. Si le fait est #f, c’est que rien de neuf n’a pu être déduit, et la valeur #f est fournie pour indiquer qu’il
n’y a pas de solution. Si le fait trouvé fait partie des buts potentiels, c’est une solution qui peut être fournie.
Sinon, le nouveau fait est ajouté en tête des faits connus, et un nouveau parcours de la base est lancé par appel
de itere. Là encore, nous sommes en présence d’un couple de procédures terminales récursives travaillant dans
un espace mémoire constant.
La figure 3.2 montre l’ensemble de ces procédures, intégrées comme fonctions locales à deduire, la fonction
principale, qui a été utilisée ci-dessus pour analyser les différents exemples.
La formulation de cette solution fait bien apparaı̂tre les caractéristiques de la programmation fonctionnelle.
Les opérations sont réalisées par l’application de procédures, sans effet de bord. L’état du système, qui ici,
en première approximation, se limite aux valeurs des variables vrais et faux, est transmis explicitement aux
diverses procédures, tout au long du calcul. Enfin, bien que l’ensemble soit rédigé avec des récursions multiples
et croisées, le processus engendré par l’exécution opère en espace constant. Ceci veut dire, en particulier, que
l’espace mémoire nécessaire à l’exécution est indépendant du nombre de règles de la base, ainsi que de la taille
de ces règles.
46
CHAPITRE 3. ELÉMENTS DE SCHEME
Fig. 3.2 - Interrogation de la base de données
(define (deduire vrais faux BDD BUTS)
(define (tente truc vrais faux)
(define (valide? proposition vrais faux)
(or
(null? proposition)
(and (eq? ’- (car proposition))
(memq (cadr proposition) faux)
(valide? (cddr proposition) vrais faux))
(and (memq (car proposition) vrais)
(valide? (cdr proposition) vrais faux))))
(if (valide? (cadr truc) vrais faux)
(car truc)
#f))
(define (balaye base vrais faux)
(if (null? base)
#f
(essai (tente base vrais faux) base vrais faux)))
(define (essai fait base vrais faux)
(if (and fait (not (memq fait vrais)))
fait
(balaye (cddr base) vrais faux)))
(define (itere vrais faux)
(itere2 (balaye BDD vrais faux) vrais faux))
(define (itere2 fait vrais faux)
(cond
((not fait) #f)
((memq fait BUTS) fait)
(else (itere (cons fait vrais) faux))))
(itere vrais faux))
Chapitre 4
Fonctions
Nous nous sommes assez peu intéressés jusqu’à présent aux fonctions, pourtant éléments de première classe
en Scheme. Ce chapitre va nous permettre d’approfondir le sujet, tout en maintenant l’emphase sur les aspects
liés à la programmation fonctionnelle.
4.1
La fonction, objet de première classe
Jusqu’à présent, nous avons essentiellement considéré la procédure Scheme comme la simple traduction d’un
algorithme. Nous allons voir que les procédures sont aussi des données comme les autres dans le langage : elles
peuvent être passées en paramètre à des procédures, fournies en résultat de procédures, et conservées dans des
structures de données arbitraires. Elles constituent donc, à l’instar des nombres et des chaı̂nes de caractères,
un type de données primitif du langage. Ces sont des citoyens de première classe du langage, first class citizen
selon la terminologie consacrée.
4.1.1
Retour sur la syntaxe des expressions
La syntaxe même du langage accorde un rôle particulier, dans une expression qui représente un appel de
fonction, au premier élément, qui désigne la fonction. En effet, contrairement à ce qui se passe dans les autres
dialectes de Lisp, cet élément est évalué, de la même manière que les autres éléments de la liste. Une expression
telle que :
(+ 2 3)
est donc évaluée de la manière suivante, en deux étapes :
1. Tous les éléments de la liste sont évalués, dans un ordre arbitraire. Dans ce cas particulier, l’évaluation
des constantes 2 et 3 fournit les valeurs 2 et 3 ; Le symbole + est le nom d’une variable ; la valeur de cette
variable est la fonction +.
2. Cette fonction d’addition est appliquée aux valeurs 2 et 3 , fournissant le résultat 5 .
Le premier élément d’une expression Scheme est habituellement un symbole, qui va être le nom d’un objet
prédéfini du système, ou défini par l’utilisateur au moyen de define ; mais ce premier élément peut être lui-même
une expression quelconque, dont la valeur est une fonction :
[1] ((if (> 3 5) + *) 5 7)
35
Ce exemple montre que la forme if (mais c’est vrai aussi de n’importe quelle construction du langage, que ce soit
un appel fonctionnel ou une forme spéciale) peut être utilisée dans n’importe quelle expression, en particulier en
position fonctionnelle. Voici l’une des caractéristiques qui font que le langage puisse être qualifié d’orthogonal ,
comme nous le signalions au chapitre 2.1.
Nous pouvons maintenant donner des règles un peu plus précises concernant l’évaluation en Scheme. Tout
programme Scheme est représenté par une expression symbolique. Les règles d’évaluation d’une expression
symbolique sont les suivantes :
– Les constantes (nombres, chaines de caractères) représentent leur propre valeur. Ainsi :
3
est un programme Scheme, dont l’effet est de calculer la valeur 3 .
47
48
CHAPITRE 4. FONCTIONS
– Les atomes représentent les variables Scheme. Ces variables peuvent être prédéfinies, comme +, -, *, sin
cos, remainder, ou définies par l’utilisateur. L’évaluation d’un atome consiste à le remplacer par la valeur
qui lui est associée dans le contexte courant.1 L’évaluation d’un atome auquel aucune valeur n’est associée
dans le contexte courant provoque une erreur.
La plupart des valeurs associées aux variables prédéfinies sont des valeurs fonctionnelles.
– Une liste représente une application fonctionnelle, ou une forme spéciale.
– Une application fonctionnelle consiste en l’application d’une fonction sur ses opérandes. Les éléments
de la liste (constantes, atomes ou autres listes) sont d’abord évalués, dans un ordre non précisé.
Le premier élément de la liste, qui doit être une valeur fonctionnelle, est alors appliqué aux autres
éléments. Le résultat de cette application devient le résultat de l’expression symbolique.
– Une forme spéciale est un cas très particulier d’expression Scheme. Les formes spéciales ne sont pas
des applications fonctionnelles. Le premier élément d’une telle forme n’est pas une désignation de
fonction, mais un mot clef . Les autres éléments d’une forme spéciale ne sont pas nécessairement
évalués : l’utilisation qui en est faite dépend de la sémantique associée à la forme spéciale.
Alors que le mécanisme d’exécution d’une application fonctionnelle est toujours le même, celui d’une
forme spéciale est ainsi propre à chaque forme. Heureusement, il n’existe qu’un tout petit nombre
de formes spéciales, qui, pour la plupart, correspondent aux instructions de contrôle des langages
impératifs.
Voici quelques exemple d’applications de ces règles :
1 ]=> (define plus +)
;Value: plus
1 ]=> (plus 3 2)
;Value: 5
1 ]=> (define (fun f) (f 3 2))
;Value: fun
1 ]=> (fun +)
;Value: 5
1 ]=> (fun *)
;Value: 6
1 ]=> (fun modulo)
;Value: 1
1 ]=> (define (gag +) (+ 3 2))
;Value: gag
1 ]=> (gag *)
;Value: 6
Les expressions 1 et 2 nous montrent qu’un nom peut effectivement désigner une valeur fonctionnelle, ici celle de
la fonction +. La ligne 3 définit une fonction, fun, qui attend une autre fonction en paramètre, et l’applique sur
les nombres 3 et 2. Cette application se note tout simplement (f 3 2), le paramètre f ayant, lors de l’appel de
fun, reçu une valeur fonctionnelle telle que + ou *. Enfin, la ligne 7 définit une fonction, gag, qui est strictement
équivalente à fun. L’unique différence, qui n’influe en rien sur son comportement, est que nous avons choisi +
pour le nom du paramètre, ce qui est tout à fait légal en Scheme (mais qui, admettons le, n’améliore pas la
compréhension de ce programme particulier).
4.1.2
Création de fonctions
Une fonction se définit, comme en Lisp2 , par une lambda-expression :
(lambda -paramètres-formels. -expression.)
-paramètres-formels. est une liste de symboles représentant les paramètres de la fonction, -expression. est le
corps de la fonction. Ainsi :
(lambda (x) (+ x 1))
1 Nous
utilisons ici le terme de contexte de manière tout à fait informelle ; nous reviendrons plus précisément sur cette importante
notion au § 6.6.3.
2 La syntaxe est identique, mais certains aspects font que les fonctions ne sont pas first class citizen dans la plupart des autres
dialectes de Lisp.
49
4.2. FONCTIONNELLES
est la fonction successeur.
(lambda (x) (* x x))
définit la fonction élévation au carré.
Une lambda expression n’est pas une fonction : c’est son exécution qui définit la fonction. Cette fonction
peut être directement appliquée à son, ou ses paramètres :
((lambda (x) (* x x)) 3)
La fonction peut être également affectée à un identificateur :
(define square (lambda (x) (* x x)))
Cette formulation est strictement identique, sur le plan de la sémantique, à l’écriture plus classique que nous
avons utilisée jusqu’à présent :
(define (square x) (* x x))
Une procédure ainsi créée par une lambda-expression peut donc être associée à un nom (c’est le rôle de la
forme define), immédiatement appliquée à son, ou ses paramètres, mais aussi passée en paramètre à d’autres
fonctions, ou fournie comme résultat.
4.2
Fonctionnelles
Les fonctionnelles sont des fonctions d’ordre supérieur, c’est à dire des fonctions qui acceptent d’autres
fonctions comme paramètres. En réalité, il n’y a pas en Scheme de différence fondamentale entre fonctions et
fonctionnelles. Considérons la fonction suivante :
(define (2f f x) (f x x))
=⇒
(2f + 3)
=⇒
6
(2f * 3)
=⇒
9
(2f cons ’x)
=⇒
(x . x)
(2f append ’(a b c))
=⇒
(a b c a b c)
2f
La fonction 2f présente, par rapport à celles que nous avons déjà écrites, l’originalité d’utiliser un paramètre
en position fonctionnelle. Elle permet d’appliquer son premier paramètre, une fonction, sur deux opérandes
identiques. Lors de l’appel (2f + 3), les deux valeurs qui sont transmises à la fonction 2f sont la fonction + et
le nombre 3 , désignés à l’intérieur de la fonction par les variables f et x. Les règles d’évaluation de l’expression
(f x x) conduisent alors à :
( f x x )
⇓ ⇓ ⇓
+ 3 3
puis en l’aplication de + sur 3 et 3.
Cette fonction particulière ne présente bien sûr qu’un intérêt assez restreint. Un type de problème beaucoup
plus fréquent est celui où une même opération doit être appliquée à un ensemble d’éléments. Par exemple,
ajouter 1 à tous les nombres d’une liste peut s’écrire :
(define (incrementer-chaque l)
(if (null? l)
l
(cons (+ 1 (car l))
(incrementer-chaque (cdr l)))))
On peut, dans cette écriture envisager d’abstraire la procédure utilisée :
(define (appliquer-sur-chaque l f)
(if (null? l)
l
(cons (f (car l))
(appliquer-sur-chaque (cdr l) f))))
50
CHAPITRE 4. FONCTIONS
Le paramètre supplémentaire correspond à la fonction à appliquer. Notre procédure initiale peut ainsi s’écrire :
(define (incrementer-chaque l)
(define (1+ x) (+ 1 x))
(appliquer-sur-chaque l 1+))
dans laquelle la fonction locale 1+ représente la fonction successeur que l’on veut appliquer sur chaque élément
de la liste. Mais on peut aussi obtenir la valeur absolue de tous les éléments d’une liste par :
(appliquer-sur-chaque l abs)
Alors que la fonction incrementer-chaque est relativement spécialisée, et, partant, d’un usage peu fréquent,
appliquer-sur-chaque est beaucoup plus générale. En fait, elle est même tellement utile que Scheme en
propose une version encore plus générale, map, qui permet d’appliquer une fonction sur les éléments d’un nombre
arbitraire de listes :
(map + ’(1 2 3) ’(10 20 30))
=⇒
(11 22 33)
(map cons ’(1 2 3) ’(10 20 30))
=⇒
((1 . 10) (2 . 20) (3 . 30))
(map max ’(17 2 13) ’(4 6 19) ’(11 12 13))
=⇒
(17 12 19)
Notons aussi, pour la petite histoire, que dans certains systèmes (en dépit de ce qu’affirme la norme), map
accepte des listes comportant des nombres différents d’éléments, mais que la taille du résultat sera celle de la
plus petite des listes :
1 ]=> (map + ’(2 5 3 4) ’(7 9) ’(10 11 12))
;Value: (19 25)
Le champ d’utilisation de cette fonction est en fait très vaste. En voici un exemple typique. Créer une liste
de sinus à partir d’une liste de nombres est une opération triviale grâce à la fonction map :
(map sin -liste.)
fait parfaitement l’affaire. Générer une liste de carrés serait tout aussi simple, si nous disposions de la fonction
élévation au carré. Il n’en existe pas en Scheme, mais nous pouvons utiliser une fonction anonyme, écrite à cet
effet :
(map (lambda (x) (* x x)) -liste.)
Cette fonction n’a d’existence que le temps de l’application par map.
Reprenons l’exemple du maximum appliqué à un ensemble de nombres. Peut-on obtenir le même effet par
application d’une fonction définie par une lambda expression, plutôt qu’en utilisant la fonction primitive max ?
Cette fonction partage avec d’autres (+, *, min, list, etc) la particularité d’admettre un nombre quelconque
de paramètres. Le même effet peut s’obtenir avec une lambda expression, dans laquelle la liste d’arguments est
remplacée par un symbole unique :
(lambda x -corps.)
La fonction créée par une telle lambda expression admet un nombre arbitraire (éventuellement égal à zéro) de
paramètres. Les valeurs de ces paramètres sont rassemblés en une liste unique, désignée par le paramètre formel
de la lambda expression :
((lambda x x) 2 3 (* 5 6) 7)
=⇒
(2 3 30 7)
Remarquons que cette formulation, (lambda x x), correspond très exactement à la fonction primitive list.
Signalons encore une autre syntaxe de la forme lambda, permettant de déclarer des paramètres obligatoires
et des paramètres optionnels :
(lambda (x y z . t) -corps.)
définit une fonction attendant au moins trois paramètres, les paramètres supplémentaires éventuels étant rassemblés en une liste désignée ici par le symbole t. Ex :
((lambda (x y z . t) (list x y z t)) 1 2 3 4 5 6 7)
=⇒ (1 2 3 (4 5 6 7))
((lambda (x y z . t) (list x y z t)) 1 2 3)
=⇒ (1 2 3 ())
51
4.2. FONCTIONNELLES
Ces mêmes formes syntaxiques sont utilisables avec define :
(define (fun a b . z) ... )
définit une fonction attendant deux paramètres obligatoires, d’autres paramètres éventuels étant rassemblés
dans la liste z. Sous la forme :
(define (fun . z) ... )
la fonction attend un nombre quelconque de paramètres, qui seront rassemblés en une liste. Ainsi, la fonction
suivante :
(define (f . l) l)
est strictement identique à la fonction list prédéfinie dans le système.
4.2.1
Application de fonction
Nous ne sommes cependant pas au bout de nos peines. Comment appliquer une fonction à une liste de
nombres? C’est le travail de la fonction apply :
=⇒
=⇒
(apply + ’(2 3 5 6))
(apply cons ’(a b))
16
(a . b)
La fonction apply 3 respecte l’équivalence suivante :
(-e0 . -e1 . -e2 . ... -en .)
⇔
(apply -e0 . (list -e1 . -e2 . ... -en .))
Une fonction calculant la moyenne entre le plus grand et le plus petit de ses paramètres peut s’écrire ainsi :
((lambda l (/ (+ (apply max l) (apply min l)) 2))
2 3 5 6 7 8 7 7 7 7)
=⇒
5
4.2.2
Exemples
Les fonctions map et apply, combinées aux lambda-expressions, offrent souvent une alternative simplificatrice
à l’utilisation de fonctions définies.4
Donnons quelques exemples : fournir, à partir d’une liste L de nombres, la liste des valeurs absolues de ces
nombres, peut se programmer très simplement par une fonction récursive ; mais il est encore plus simple d’écrire :
(map abs L)
Sommer les nombres d’une liste d’une liste s’écrit (apply + L).
Calculer la moyenne olympique d’une liste L s’écrit :
(/ (- (apply + L) (apply min L) (apply max L)) (- (length L) 2))
Voici encore un autre exemple : une fonction de gestion de listes associatives qui permet de choisir le prédicat
utilisé pour la recherche :
(define (assp key records equal?)
(cond ((null? records) #f)
((equal? key (caar records))
3 La
fonction apply accepte en fait un nombre quelconque de paramètres :
(apply + 2 3 ’(5 6))
(apply + 2 3 ’())
=⇒
=⇒
16
5
Tout se passe comme si les premiers paramètres étaient concaténés en tête du dernier, avant l’application de la fonction.
4 L’écriture de fonctions anonymes récursives n’est pas directement possible avec la seule forme lambda. Il faut utiliser une
construction telle que letrec — que l’on abordera au § 4.4.1 — pour nommer la fonction créée par la forme lambda, afin de
permettre des définitions récursives. On peut ainsi écrire :
1 ]=> (map (letrec ((fact (lambda (n)
(if (= n 0) 1 (* n (fact (- n 1))))))) fact)
’(2 3 5 7 8))
;Value: (2 6 120 5040 40320)
52
CHAPITRE 4. FONCTIONS
(car records))
(else
(assp key
(cdr records)
equal?))))
Cette procédure permet de simuler assq, assv et assoc. Un appel tel que :
(assp -obj . -liste. eq?)
est équivalent à
(assq -obj . -liste.)
et de même :
(assp -obj . -liste. equal?)
est équivalent à :
(assoc -obj . -liste.)
4.2.3
Zéros d’une fonction
La recherche des zéros d’une fonction continue dans un intervalle donné permet d’offrir un exemple intéressant
de passage d’une fonction commme argument d’une autre fonction.
Nous allons écrire une fonction recherchant un zéro d’une fonction dans un intervalle donné, avec une
précision donnée :
(zero -f . -a. -b. -epsilon.)
Le principe va être de réduire l’intervalle initial, en choisissant un point au centre de cet intervalle, en
calculant la valeur de la fonction en ce point, et en sélectionnant l’un des deux demi-intervalles obtenus. Pour
éviter de multiplier les calculs de la fonction, on va transmettre, en plus des bornes de l’intervalle, les valeurs
de la fonction à ces bornes. Chaque itération ne nécessitera donc qu’un seul calcul de fonction. A chaque étape,
on éliminera l’intervalle [u v] dans lequel le produit f (u) × f (v) est positif. Le paramètre ε permet de fixer la
taille de l’intervalle final, donc la précision obtenue.
(define (zero f a b epsilon)
(define (inter a b c fa fb fc)
(if (<= (- b a) epsilon)
c
(if (positive? (* fa fc))
(aux c b fc fb (/ (+ c b) 2))
(aux a c fa fc (/ (+ a c) 2)))))
(define (aux u v fu fv p)
(inter u v p fu fv (f p)))
(aux a b (f a) (f b) (/ (+ a b) 2)))
L’utilisation d’une fonction auxiliaire permet de calculer en deux étapes la valeur de c, milieu de l’intervalle
[a b], puis de f (c). Le test d’arrêt porte sur la taille de l’intervalle, divisée par deux à chaque itération. Lorsque
le test est satifait, la fonction fournit le milieu de cet intervalle.
Voici quelques exemples :
1 ]=> (zero (lambda (x) (- (* x x) 4)) 0 5 1e-6)
;Value: 2.0000001788139343
1 ]=> (zero sin 0.1 5.25 1e-7)
;Value: 3.141592641547322
On notera que les fonctions sont récursives terminales (il n’y a donc pas de consommation d’espace dans la pile
de l’interprète), et que l’algorithme est en O(log n), n étant fonction de la précision demandée et de la taille de
l’intervalle initial (prendre un ε deux fois plus petit n’impose donc qu’une itération supplémentaire).
La fonction fournit l’un des zéros de la fonction, s’il en existe dans l’intervalle, ou la borne b si la fonction
n’a pas de zéros dans l’intervalle :
1 ]=> (zero sin 12.23 731.09 1e-12)
53
4.3. ENVIRONNEMENT
;Value: 301.59289474462025
1 ]=> (sin 301.59289474462025)
;Value: 1.0193061672492121e-13
1 ]=> (zero (lambda (x) (* x x) ) 1.0 5.0 1e-15)
;Value: 5.
4.2.4
Map encore
Nous pouvons tenter de proposer une écriture de la fonction map au moyen de apply. map doit appliquer
une fonction à tous les cars d’un ensemble de listes, puis il faut réitérer la chose sur le reste des listes. Il nous
faut donc être capable de sélectionner tous les cars et tous les cdrs d’un ensemble de listes : c’est ce que font
only-cars et only-cdrs. (Ces fonctions sont programmées suivant un algorithme récursif : on ne saurait ici se
permettre d’écrire (map car l) ou (map cdr l)...) Il faut aussi être capable d’interrompre l’itération lorsque
l’une des listes est épuisée. C’est le rôle de any-null?.
Fig. 4.1 - Une versions de map
(define (my-map f . ops)
(define (only-cars l)
(if (null? l)
’()
(cons (caar l) (only-cars (cdr l)))))
(define (only-cdrs l)
(if (null? l)
’()
(cons (cdar l) (only-cdrs (cdr l)))))
(define (any-null? l)
(if (null? l)
#f
(if (null? (car l))
#t
(any-null? (cdr l)))))
(if (or (null? ops) (any-null? ops))
’()
(cons (apply f (only-cars ops))
(apply my-map f (only-cdrs ops)))))
La figure 4.1 rassemble toutes ces procédures. Notons que pour pouvoir gérer un nombre arbitraire de
paramètres, il faut utiliser apply à la fois pour l’application de la fonction et pour la récursion.
4.3
Environnement
Nous devons maintenant introduire une nouvelle notion, celle d’environnement.
4.3.1
Définitions
Tout identificateur qui n’est pas un mot-clef syntaxique peut être utilisé comme variable. Une variable peut
nommer un emplacement où peut être conservée une valeur. Une telle variable est dite liée à cet emplacement.
L’ensemble de toutes les liaisons visibles en un point particulier d’un programme est dit environnement en effet
à ce point. La valeur conservée dans l’emplacement auquel est lié une variable est dite valeur de cette variable.
Par abus de langage, la variable est quelquefois dite “nommer” la valeur, ou “être liée” à la valeur. Ce n’est pas
entièrement exact, mais il est rare qu’une réelle confusion résulte de cette pratique.
Certains types d’expressions sont utilisés pour créer de nouveaux emplacements, et lier des variables à ces
emplacements. La plus fondamentale de ces constructions “liantes” est la lambda expression, car toutes les
autres constructions peuvent s’exprimer en termes de lambda expressions. Les autres constructions de liaisons
sont le let, le let*, le letrec, et le do.
54
CHAPITRE 4. FONCTIONS
Il existe dans tout système Scheme un environnement initial , ou environnement global , qui contient les objets
prédéfinis.
4.3.2
Environnement dynamique
Toute exécution d’une fonction crée un nouvel environnement. Cet environnement va éventuellement disparaitre à la fin de l’exécution de la fonction. Montrons ceci sur un vieil exemple :
1 ]=> (define (square x) (* x x))
;Value: square
1 ]=> (square (+ 3 2))
;Value: 25
Pendant l’exécution de la fonction square, une nouvelle variable, de nom x et de valeur 5, est définie dans l’environnement liée à cette exécution particulière de square. Nous allons décrire en détail cet aspect du système,
et introduire diverses constructions relatives aux environnements.
Tout comme Algol 60 et Pascal, et à la différence de la plupart des autres dialectes de Lisp hormi Common
Lisp, Scheme est un langage à portée lexicale et à structure de blocs. A chaque variable liée dans un programme
correspond une région du texte du programme dans laquelle cette liaison est active. La région est déterminée
par la construction particulière qui réalise la liaison ; si, par exemple, cette liaison est établie par une lambda
expression, la région correspondante est la lambda expression toute entière. Toute référence ou toute affectation
à une variable a pour cible la liaison de cette variable qui a été établie dans la région la plus interne contenant
cette référence ou cette affectation. S’il n’existe aucune liaison de cette variable dont la région contient cette
utilisation, celle-ci fait référence à la liaison de cette variable établie dans l’environnement global, si cette liaison
existe ; s’il n’existe aucune liaison associée à cet identificateur, celui-ci est dit non-lié.
4.3.3
Visibilité et Persistance
Nous abordons ici l’un des aspects non triviaux du langage. La compréhension des notions de persistance et
de visibilité (ou portée) entre en jeu chaque fois qu’une entité est référencée dans un programme.
4.3.3.1
Définitions
La visibilité fait référence à la portion du texte d’un programme dans laquelle une référence à l’entité est
possible. La persistance détermine l’intervalle de temps durant lequel une référence peut être faite à cette entité.
Considérons l’expression :
((lambda (x) (+ 2 x)) (* 3 5))
La visibilité du paramètre x est déterminée par le corps de la fonction, en l’occurence l’expression (+ 2 x). Il
n’est pas possible de faire référence à cet x particulier en dehors de cette expression. De même, la persistance
de ce paramètre correspond à la période comprise entre l’activation de la fonction (le paramètre est créé avec
la valeur 15) et sa terminaison.
En Scheme, une entité est généralement créée par l’exécution d’une construction particulière du langage, les
propriétés de persistance et de visibilité de l’entité étant déterminées par la nature de cette construction. Il est
utile, dans les descriptions, de préciser la nature de l’entité concernée. Ainsi, l’expression suivante définit une
fonction de nom foo :
(define (foo x)
(+ x 2))
Le nom x, unique, fait référence au paramètre de la fonction. Cependant, chaque appel de la fonction va créer
une nouvelle incarnation de ce paramètre. L’entité qui nous intéresse ici est en fait la liaison nom/valeur établie
à cette occasion : une nouvelle liaison est créée à chaque appel de la fonction foo.
4.3.4
Notions
Voici quelques définitions utiles :
Portée lexicale : c’est la visibilité habituelle dans les langages à structure de blocs, comme Scheme. Les références
aux entités ainsi établies ne peuvent s’effectuer que dans la portion du programme (souvent désignée sous
le nom de corps) qui est textuellement enchassée dans la construction ayant créé l’entité. Un exemple
typique est celui des paramètres d’une fonction, qui ne peuvent être référencés que dans le corps de la
fonction.
4.3. ENVIRONNEMENT
55
Portée indéfinie : une entité a une portée indéfinie si on peut y faire référence n’importe où dans le programme.
C’est le cas en particulier des variables globales, telles les identificateurs utilisés pour désigner les fonctions
prédéfinies du système : +, *, max, sin, cons, etc.
Persistance indéfinie : une entité a une persistance indéfinie (ou illimitée) si son existence se poursuit aussi
longtemps que subsiste une possibilité d’y faire référence. Toutes les entités de Scheme jouissent d’une
persistance indéfinie. Aucune n’est jamais détruite, et il n’existe pas de possibilité explicite de le faire.
Cependant, lorsqu’un objet n’est plus référencé, le système peut récupérer la mémoire qui lui a été allouée.
C’est ce qui arrive, en général, aux valeurs des paramètres après exécution d’une fonction.
Environnement : en ce qui nous concerne, il s’agit de l’ensemble des liaisons nom-valeurs qui sont accessibles à
un instant donné, par un fragment de programme donné.
Environnement global : ce terme recouvre l’espace de référence du système. Dans cet environnement, toutes les
fonctions primitives du système sont désignées par un nom particulier (comme car, * ou append).
Notons qu’un identificateur peut être masqué par un autre identificateur de même nom, situé dans un bloc
plus interne :
(define (f x)
(define (g x)
(+ x 2))
(g (* x 3)))
Ici, la portée du paramètre x de la fonction f est naturellement le corps tout entier de cette fonction. Cependant,
ce nom est masqué, à l’intérieur du corps de la fonction g, par le paramètre de même nom de cette dernière
fonction. Un exemple plus troublant est celui-ci :
(define (foo *)
(* 2 3))
La valeur de la variable globale * (c’est à dire la fonction produit ) est masquée par le paramètre. L’appel (foo
+) va fournir comme résultat 5, alors même que le corps de foo ressemble furieusement à la multiplication de
2 par 3 !
Notons que les mots clefs du langage (quote, lambda, set!, etc) sont réservés et ne peuvent pas être redéfinis.
4.3.4.1
Note
C’est la combinaison de la portée lexicale et de la persistance illimitée des entités qui fournit à Scheme toute
sa puissance sur le plan des manipulations fonctionnelles. Considérons cet exemple :
(define baz 3)
(define (bar n) (+ n baz))
(define (foo baz) (bar (+ 1 baz)))
L’exécution de ces trois expressions définit trois variables globales baz, bar et foo, dont la persistance et la
portée sont illimitées. Plus précisément, à la création de la fonction bar, le nom baz utilisé dans le corps est lié
à l’instance globale de même nom, lexicalement visible à cet instant. De même, lors de la création de foo, le
nom bar est lié à l’instance de la variable globale qui vient d’être définie.5
L’exécution d’une expression telle que (foo 5) fournit le résultat 9. Dans le corps de foo, la fonction bar
reçoit 1+baz , c’est à dire la valeur 6 en paramètre. L’expression (+ n baz) est exécutée alors que n vaut 6 , et
baz, qui désigne la variable globale, 3 .6 L’environnement dans lequel une fonction opère est donc l’environnement
dans lequel elle a été créée, et non l’environnement dans lequel elle est exécutée.
5 En
fait, cette résolution des noms ne dépend nullement de l’ordre dans lequel ces trois instructions sont exécutées.
6 Le résultat aurait été différent dans un système Lisp pratiquant la liaison dynamique, c’est à dire un système Lisp dans lequel la
résolution des noms [recherche de l’objet effectivement désigné par un nom] s’effectue dynamiquement, au moment de l’exécution.
Au moment de l’exécution de bar, l’objet visible de nom baz est alors le paramètre de la fonction foo, de valeur 5. Le résultat
aurait alors été 11.
56
CHAPITRE 4. FONCTIONS
4.4
Environnements locaux
Nous abordons dans ce paragraphe les constructions permettant la définition d’environnements locaux.
C’est cette constitution dynamique d’environnements, qui, associée à la création de fonctions à l’intérieur de
ces environnements, va donner naissance à des objets aux propriétés intéressantes, les fermetures.
Nous avons présenté jusqu’à présent des fonctions sans environnement local, ou dans lesquelles cet environnement local ne contenait que des fonctions. Scheme propose différentes méthodes permettant la déclaration
d’objets locaux.
4.4.1
Création d’environnements locaux
C’est le rôle des formes spéciales let, let*, letrec et fluid-let (c.f. [Han91b], § 2.2 et 2.3).
La syntaxe de ces différentes constructions est la suivante :
(-let .
((-v1 . -e1 .)
(-v2 . -e2 .) ...
(-vn . -en .))
-exp1 .
-exp2 . ...
-expp .)
La forme déclare des variables locales -v1 ., -v2 . ... -vn ., qui reçoivent les valeurs initiales -e1 ., -e2 . ... -en .. Dans
cet environnement, les formes -exp1 ., -exp2 . ... -expp . sont alors exécutées séquentiellement, le résultat de la
dernière étant fourni comme résultat de la forme -let ..
Ces formes diffèrent entre elles par l’environnement actif au moment de l’exécution des expressions -ei .. Il
y en a pour tous les goûts :
let Les -ei . sont exécutées dans le contexte externe à la forme let. Leur ordre d’exécution est non spécifié.
let* Les -ei . sont exécutées séquentiellement, chaque liaison -vi . -ei . venant s’ajouter au contexte. Plus précisément, -e1 . est exécutée dans le contexte externe à la forme let*. -e2 . est exécutée dans le contexte
externe, incrémenté de la liaison -v1 . -e1 .. -e3 . est exécutée dans ce nouveau contexte, incrémenté de la
liaison -v2 . -e2 ., etc.
letrec Un nouveau contexte est créé, correspondant au contexte externe à la forme letrec, incrémenté des
variables -vi . (n’ayant pas reçu de valeurs). Dans ce contexte, les formes -ei . sont évaluées dans un ordre
non spécifié. Enfin, les valeurs correspondantes sont affectées aux -vi .. Cette forme permet la définition
de fonctions locales récursives, de fonctions mutuellement récursives, et plus généralement, de fonctions
pouvant faire référence aux autres objets créés par la forme.
fluid-let La sémantique de cette forme est assez particulière, puisqu’elle ne crée pas en fait de nouveau
contexte. Les variables -ei . visibles à l’intérieur de la forme fluid-let sont celles qui seraient visibles à
l’extérieur, mais elles ont reçu les valeurs -ei . évaluées dans le contexte externe. Tout se passe donc comme
si les -ei . avaient simplement été réaffectées. Cependant, à la sortie de la forme fluid-let, ces variables
vont retrouver leur valeur initiale.
Quelle est la différence avec les autres formes de type let ? Si à l’intérieur de fluid-let il y a appel d’une
fonction externe pour laquelle certaines des variables de la forme fluid-let sont visibles, les modifications
temporaires induites par fluid-let seront également observables dans cette fonction.
4.4.1.1
Exemples
Quelques exemples simples vont permettre de fixer les idées :
(let ( (u 2) (v 3) )
(+ u v 1))
=⇒
6
est une instruction qui crée un environnement local, dans lequel les variables u et v sont définies, avec comme
valeurs respectives 2 et 3. L’expression (+ u v 1) est évaluée dans cet environnement, fournissant en résultat
la valeur 6. Après exécution de la forme, l’environnement disparait. L’exemple suivant montre que les valeurs
initiales des variables locales peuvent être le résultat d’expressions quelconques :
(define a 5)
(define b 8)
(let ( (u a) (v (* b 3)) ) (list u v))
=⇒
(5 24)
57
4.4. ENVIRONNEMENTS LOCAUX
Les expressions a et (* b 3) font référence à des objets externes à la forme let. L’exemple suivant est strictement identique, sur le plan de la sémantique, au précédent :
(define a 5)
(define b 8)
(let ( (a a) (b (* b 3)) ) (list a b))
=⇒
(5 24)
Les variables locales a et b recoivent les valeurs des expressions a et (* b 3) calculées hors du contexte de la
forme let.
(define a 5)
(define b 8)
(let ( (a b) (b a) ) (list a b))
=⇒
(8 5)
Cette fois, a locale reçoit la valeur de b externe, et réciproquement.
(define a 5)
(define b 8)
(let* ( (a b) (b a) ) (list a b))
=⇒
(8 8)
C’est une construction différente qui est maintenant utilisée ! Le let* calcule la première expression, b, dans
le contexte externe, puis constitue un premier contexte local, dans lequel la variable a reçoit le résultat de ce
calcul, c’est à dire 8. A l’intérieur de ce nouveau contexte, l’expression a est calculée (sa valeur est donc 8), puis
un second contexte, interne au premier, est constitué, dans lequel b reçoit cette valeur. L’expression (list a
b), evaluée dans ce dernier contexte, fournit enfin le résultat (8 8). Cette forme let* est équivalente à :
(let ( (a b) )
(let ( (b a) )
(list a b)))
qui met en relief les imbrications des contexes successifs.
4.4.2
Remarques
4.4.2.1
Equivalence entre let et lambda
La forme let est sémantiquement identique à une forme lambda : il y a équivalence formelle entre ces deux
expressions :
(let ((-v1 . -e1 .) (-v2 . -e2 .) ... (-vn . -en .)) -corps.)
((lambda (-v1 . -v2 . ... -vn .) -corps.) -e1 . -e2 . ... -en .)
La première forme insiste sur l’aspect “environnemental” : créer un environnement particulier pour l’exécution
d’une expression.
La seconde insiste sur l’aspect fonctionnel. Il est clair, dans la formulation du lambda, que les -ei . sont
exécutées hors du contexte local.
L’une des motivations de cette forme est de permettre la redéfinition d’opérations, en faisant appel à leur
valeur externe. Voici par exemple un environnement fournissant des car et des cdr “sécurisés” :
(let ((car (lambda (x) (if (pair? x) (car x) ’())))
(cdr (lambda (x) (if (pair? x) (cdr x) ’()))))
... )
4.4.2.2
La forme fluid-let
Elle n’est utilisée que pour permettre de faire appel à la liaison dynamique des anciens Lisp. Définissons une
variable “globale” *var*, et une fonction fun susceptible d’en consulter la valeur.
1 ]=> (define *var* ’globale)
;Value: *var*
1 ]=> (define (fun) (display *var*) (newline) *var*)
;Value: fun
1 ]=> (fun)
globale
;Value: globale
58
CHAPITRE 4. FONCTIONS
L’exemple suivant montre la différence de comportement entre let et fluid-let :
1 ]=> (let ((*var* ’locale)) (fun)
(let ((*var* ’encore-plus-interne)) (fun))
(fun))
globale
globale
globale
;Value: globale
1 ]=> (fluid-let ((*var* ’locale)) (fun)
(fluid-let ((*var* ’encore-plus-interne)) (fun))
(fun))
locale
encore-plus-interne
locale
;Value: locale
La fonction fun permet de suivre l’évolution de la valeur de la variable globale *var*. Les deux formes let
imbriquées créent de nouvelles variables *var*, de valeurs ’locale et ’encore-plus-interne respectivement,
qui sont différentes de la variable globale *var*. Il n’y a donc pas d’évolution de celle-ci. Par contre, les deux
formes fluid-let imbriquées modifient la liaison de la variable *var* visible à l’extérieur de la forme, c’est
à dire la variable globale *var*. Les différents appels de fun reflètent donc l’évolution de la valeur de cette
variable.
Un dernier exemple permet de fixer les idées :
1 ]=> (let ((*var* ’du-let))
(fluid-let ((*var* ’du-fluid-let)) (fun)
(fluid-let ((*var* ’de-l-autre-fluid-let)) (fun))
(fun)))
globale
globale
globale
;Value: globale
Ici, les formes fluid-let modifient la liaison de la variable *var* locale au let externe, variable différente de
celle qui est connue de fun. fun imprime donc trois fois la valeur de la variable globale *var*, qui demeure
inchangée durant l’exécution.
Notons encore que la forme fluid-let réalise une modification de liaison, non une création. Les variables
concernées par une forme fluid-let doivent donc posséder antérieurement une valeur à l’extérieur de la forme :
1 ]=> (fluid-let ((toto 4)) toto)
Unbound variable toto
2 Error->
mais :
1 ]=> (define toto 0)
;Value: toto
1 ]=> (fluid-let ((toto 4)) toto)
;Value: 4
1 ]=> toto
;Value: 0
4.4.2.3
La forme define
Lorsqu’une variable a été définie par une forme define, l’effet d’une nouvelle forme define, dans le même
environnement, n’est pas d’ajouter une nouvelle liaison nom/valeur masquant la précédente, mais simplement
de réaffecter une nouvelle valeur au même nom. La seconde forme define est donc équivalente à un set!.
L’une des motivations de ce comportement est de permettre une mise au point incrémentale des programmes :
(define (foo x y z) -un truc pas bon.)
(define (bar a b) (foo a 0 (- b 1)))
(bar 0 1) ; zut ca marche pas
(define (foo x y z) -le bon algorithme.)
59
4.5. UTILISATION DYNAMIQUE DES FONCTIONS
(bar 0 1) ; super, ca marche!
La forme define ne crée donc de nouvelle liaison qu’à l’intérieur d’une forme qui définit elle-même un nouvel
environnement : corps de fonction ou de lambda expression, forme let, let* ou letrec.
Plus précisément, un ensemble d’objets ainsi définis par une suite de define se comporte comme si ces objets
étaient définis à l’intérieur d’un nouveau letrec : ils peuvent se faire des références réciproques et récursives,
mais la définition de l’un ne peut pas utiliser la valeur d’un autre, condition qui est vérifiée si l’on ne définit
que des fonctions.
4.4.3
Le let nommé
Certaines implantations de Scheme admettent une variante de la syntaxe de let, dite “let nommé”, fournissant une construction d’itération qui permet aussi d’exprimer les récursions non terminales.
(-let . -nom.
((-v1 . -e1 .)
(-v2 . -e2 .) ...
(-vn . -en .))
-corps.)
Le let nommé a une syntaxe et une sémantique proches de celle du let “ordinaire”, mais un identificateur,
-nom., lui est associée ; -nom. est liée, à l’intérieur du -corps., à une procédure dont les arguments sont les
variables liées, et dont le corps est le -corps. lui-même. L’exécution du corps peut dont être répétée en appelant
la procédure dénommée par -nom..
Voici un exemple d’utilisation du let nommé.
(define (enum min max)
; la liste contenant les entiers de min a max
(let loop ((res ’()) (i max))
(if (< i min)
res
(loop (cons i res) (- i 1)))))
La fonction suivante lui est tout à fait équivalente :
(define (enum min max)
(define (loop res i)
(if (< i min)
res
(loop (cons i res) (- i 1))))
(loop ’() max))
ainsi d’ailleurs que cette dernière :
(define (enum min max)
(letrec ( (loop (lambda (res i)
(if (< i min)
res
(loop (cons i res) (- i 1)))))
(loop ’() max)))
4.5
)
Utilisation dynamique des fonctions
Tous les exemples abordés jusqu’à présent faisaient usage de définitions “statiques” de fonctions, obtenues
grâce à la forme define, ou par des lambda expression définies en mode interactif. Nous allons analyser ce qui
se produit lorsque la forme lambda est utilisée à l’intérieur d’une autre fonction.
4.5.1
Création dynamique de fonction
La création d’une fonction est en fait une action dynamique, réalisée, comme nous l’avons vu, par la forme
lambda. Une telle fonction est anonyme — tant qu’elle n’est pas affectée à une variable.
Cependant, le fait que cette création se produise à l’intérieur d’une autre fonction, donc dans un contexte
contenant les objets locaux de la fonction, va donner des propriétés intéressantes aux objets ainsi créés.
60
CHAPITRE 4. FONCTIONS
4.5.2
Quelques exemples
Une opération mathématique classique est la composition de fonctions : f ◦ g est une fonction qui, appliquée
à x, fournit f (g(x)). Nous pouvons écrire en Scheme :
(define (compose f g)
(lambda (x) (f (g x))))
La fonction compose, à deux paramètres f et g, fournit donc une nouvelle fonction, qui applique f sur le résultat
de (g x). Un exemple :
((compose - sin) 2.17)
=⇒
-0.825785
Quel est le principe de fonctionnement? Un appel de (compose f g) a pour premier effet de créer deux nouvelles
liaisons pour les noms f et g. Ces liaisons sont effectives lors de l’exécution de la forme lambda interne, donc de
la création de la fonction résultat. Cette fonction, qui fait référence aux objets f et g (du fait de l’utilisation de
la liaison lexicale), va donc conserver, dans son environnement, des références à ces deux valeurs (les objets de
Scheme ayant une persistance illimitée). Le résultat de cet appel particulier est donc une fonction qui calcule
l’opposé du sinus de son argument. Un autre appel tel que (compose sin cos) va créer de nouvelles liaisons,
une nouvelle fonction résultante, calculant le sinus du cosinus de son argument. La fonction peut par exemple
être affectée à une variable :
(define sincos (compose sin cos))
Chacune de ces fonctions est créée comme une entité séparée. Il n’y a pas d’interférence avec d’autres objets
également créés par compose. Une telle entité, composée d’une fonction et d’un certain environnement, est dite
fermeture (closure). Dans ce cas particulier, l’environnement de la fonction sincos ainsi créée se compose de
l’environnement global, incrémenté des liaisons de f et g avec les fonctions sin et cos. Ces dernières variables ne
peuvent ici être modifiées, puisqu’elles ne sont connues que de l’environnement de sincos, et qu’aucun dispositif
n’a été prévu pour les modifier.
4.5.3
Quelques utilisations des fermetures
Une première vision d’une fermeture est donc celle d’une fonction dont le comportement est différent parce
qu’elle a été créée dans un environnement particulier. Ainsi, la fonction suivante permet de créer des fonctions
constantes, qui rendent toujours la même valeur :
(define make-constant (c)
(lambda () c))
Il est possible d’agir sur l’environnement associé à une fonction. Considérons l’exemple suivant :
(define make-counter (v)
(lambda ()
(begin
(set! v (+ v 1))
v)))
Nous nous éloignons ici de la programmation fonctionnelle, en utilisant deux traits impératifs du langage :
le séquencement d’instructions, et l’affectation, définis par les formes spéciales begin et set!.
Forme begin
La syntaxe de begin est la suivante :
(begin -e1 . -e2 . ... -en .)
Les expressions -e1 ., -e2 . ... -en ., sont exécutées dans cet ordre, puis le résultat de la dernière est fournie comme
résultat de la forme begin. Il est clair que l’utilisation d’une telle forme n’a de sens que si les expressions internes
ont des effets de bord : entrées/sorties ou affectation. C’est effectivement le cas ici, puisque la première instruction
de la forme begin a pour but d’affecter à la variable v la valeur (+ v 1), autrement dit, d’incrémenter cette
variable.
Contrairement à ce que l’on pourrait penser d’un langage qui se veut un héritier d’Algol 60, la forme begin
ne crée pas d’environnement temporaire, comme nous le montre cet exemple :
1 ]=>
(define x 5)
61
4.5. UTILISATION DYNAMIQUE DES FONCTIONS
;Value: x
1 ]=> (begin (define x 10) (* x 2))
;Value: 20
1 ]=> x
;Value: 10
Forme set!
Définissons également la syntaxe de set! :
(set! -identificateur . -expression.)
-identificateur . désigne la variable qui va être modifiée, -expression. la valeur (calculée) qui va lui être affectée.7
Notons à cette occasion que toutes les opérations qui modifient physiquement un emplacement de la mémoire, comme set!, set-car!, set-cdr!, etc., sont repérées par des noms qui se terminent par un point
d’exclamation, !.
Retour aux compteurs
Chaque appel de make-counter va donc créer une nouvelle fonction gérant un compteur :
(define c1 (make-counter 0))
(c1)
(c1)
(c1)
(c1)
(define c2 (make-counter 1000))
(c2)
(c1)
(c2)
=⇒
=⇒
=⇒
=⇒
=⇒
=⇒
=⇒
=⇒
=⇒
c1
1
2
3
4
c2
1001
5
1002
Un environnement n’est pas nécessairement lié à une seule fonction. L’exemple suivant est une extension
du précédent. La fonction de génération de compteurs fournit maintenant comme résultat une liste de deux
fonctions, l’une permettant l’incrémentation du compteur, l’autre la remise à zéro de ce dernier :
(define make-counter (v)
(list
(lambda ()
(begin
(set! v (+ v 1))
v))
(lambda ()
(set! v 0))))
Voici un exemple d’utilisation :
(define c (make-counter 10))
(car (c))
=⇒
11
(car (c))
=⇒
12
(cadr (c))
=⇒
0
(car (c))
=⇒
1
(car (c))
=⇒
2
=⇒
c
Les deux fonctions créées par make-counter le sont dans le même environnement, et partagent donc l’accès à
la valeur de v visible au moment de la création.
Nous verrons que les fermetures constituent une solution techniquement viable à l’implantation d’objets
dans le langage. Plus simplement, montrons qu’il est possible de réaliser n’importe quelle structure de données
grâce à elles.
Prenons un exemple simple, celui des paires pointées. Une paire pointée est l’analogue d’une structure C
composée de deux champs de même nature, le car et le cdr .
7 Plus exactement, #identificateur $ est une désignation de la liaison qui va être modifiée. Cette liaison doit avoir été antérieurement
établie, par l’exécution d’une forme define, let, ou l’appel d’une fonction dans laquelle #identificateur $ désigne un paramètre.
62
CHAPITRE 4. FONCTIONS
On imagine bien la fonction cons générant une fermeture, dans laquelle deux valeurs liées représentent
respectivement le car et le cdr de la fonction. Comment rendre ces valeurs accessibles de l’extérieur ? Il suffit
de pouvoir passer au résultat de cons une fonction pouvant accéder à ces valeurs. Voici une première solution :
(define (kons a d)
(lambda (f) (f a d)))
(define (kar c)
(c (lambda (u v) u)))
(define (kdr c)
(c (lambda (u v) v)))
On peut aussi envisager de confier à la fonction générée par kons le travail de restituer à la demande le car
ou le cdr dont elle a la garde :
(define (kons a d)
(lambda (k)
(if (eq? k ’car) a d)))
(define (kar c)
(c ’car))
(define (kdr c)
(c ’cdr))
Ces fonctions ne sont pas très robustes. Dans tous les cas, il est possible de se passer de kar et kdr pour arriver
à lire les valeurs du car et du cdr d’une cellule c : dans le premier cas,
(c (lambda (p q) (+ p q)))
fournit la somme des composants, dans le second,
(c ’toto)
donne la valeur du cdr .
Cependant, ces exemples sont assez typiques de l’intérêt du choix de Scheme en ce qui concerne la portée et
la persistance des objets.
4.5.4
Define encore
Signalons pour terminer une extension syntaxique de la forme define.
Nous avons signalé que la syntaxe :
(define (-f . -p1 . -p2 . . . . -pn .) -corps.)
était équivalente à la forme :
(define -f . (lambda (-p1 . -p2 . . . . -pn .) -corps.))
Une telle transformation est en fait répétée par le système autant de fois que nécessaire, jusqu’à ce que -f . soit
un identificateur simple. Ainsi, l’expression :
(define ((trinome a b c) x) (+ (* a x x) (* b x) c))
est identique aux deux suivantes :
(define (trinome a b c) (lambda (x) (+ (* a x x) (* b x) c)))
(define trinome (lambda (a b c) (lambda (x) (+ (* a x x) (* b x) c))))
Voici un exemple :
1 ]=> ((trinome 2 5 -3) 1)
;Value: 4
1 ]=> ((trinome 2 5 -3) 4)
;Value: 49
1 ]=> (define f (trinome 2 5 -3))
;Value: F
1 ]=> (f 1)
;Value: 4
1 ]=> (f 5)
63
4.6. EXERCICES D’APPLICATION
;Value: 72
1 ]=> (define g (trinome 1 0 -4))
;Value: G
1 ]=> (g 2)
;Value: 0
1 ]=> (g 10)
;Value: 96
1 ]=> (f 10)
;Value: 247
4.6
Exercices d’application
Comme d’habitude, les exercices proposés sont plus des cas d’écoles que de réels problèmes informatiques.
Leur but est de vous permettre de vérifier votre assimilation des notions exposées dans le chapitre.
funcall
Les systèmes Lisp classiques définissent la fonction funcall qui permet l’application d’une fonction sur un
ensemble de paramètres :
(funcall + 2 3 5)
=⇒
10
Comment réaliseriez-vous cette fonction en Scheme? Serait-il d’un très gros intérêt d’en disposer, en standard,
dans le langage?
map encore
Comment utiliseriez-vous la fonction map pour pouvoir appliquer, élément par élément, une liste de fonctions
sur une liste de valeurs :
(map -??? . (list - + * max)
’(1 2 3 4) ’(8 4 6 2) ’(2 6 2 3))
=⇒
(-9 12 36 4)
(le résultat est donc la liste formée des valeurs (- 1 8 2), (+ 2 4 6), (* 3 6 2) et (max 4 2 3)).
map/and, map/or
La fonction map permet d’appliquer un prédicat, pred?, sur un ensemble d’éléments organisés sous forme
d’une liste. Cependant, si nous essayons de savoir si tous ces éléments (ou si quelques un d’entre eux) vérifient
le prédicat, par des écritures telles que :
(apply and (map pred? liste))
(apply or (map pred? liste))
le système nous dispute, parce que and et or sont des formes spéciales, et non des fonctions ordinaires. Comment
contourner le problème?
sum
On a défini la fonction suivante, recopiée de [ASS85] :
(define (sum term a next b)
(if (> a b)
0
(+ (term a)
(sum term (next a) next b))))
Comment utiliser cette fonction pour calculer :
– La somme des 100 premiers entiers.
– La somme des cubes de 100 premiers entiers.
– Une valeur approchée de l’intégrale de la fonction f (x) = x2 − 1 entre 3 et 5.
64
CHAPITRE 4. FONCTIONS
for-each
for-each est une fonction prédéfinie des systèmes Scheme. La syntaxe de for-each et ses arguments sont
identiques à ceux de map, mais la for-each ne fournit pas de résultat : la fonction est appliquée uniquement
pour ses effets de bord éventuels. A la différence de map, for-each nous assure que la fonction transmise en
paramètre sera appliquée séquentiellement sur les éléments successifs des listes :
[1] (for-each
25hello boy
display ’(2 5 "hello" #\space boy))
Il est possible d’écrire sa propre version de for-each. Le code est identique à celui de la figure 4.1, l’unique
différence est l’utilisation de begin au lieu de cons dans la dernière expression :
....
(begin (apply f (only-cars ops))
(apply my-for-each f (only-cdrs ops)))))
[2] (my-for-each display ’(2 5 "hello" #\space boy))
25hello boy
Qu’est-ce qui nous garantit que cette version de for-each respecte bien le contrat, c’est à dire que les
applications de la fonction f sont bien réalisées de manière séquentielle? Qu’est-ce qui nous garantit également,
dans la version de map de la figure 4.1, que les applications de f sont effectivement réalisées dans un ordre
arbitraire?
fold
Nous avons, au chapitre 1, donné l’exemple de la fonctionnelle fold du langage Haskell ainsi définie :
(fold f init) []
= init
(fold f init) (x:l)= f x ((fold f init) l)
Cette fonction, fold, s’applique à trois paramètres : une fonction, f, un élément neutre, init, et une liste l.
Ainsi, le résultat de :
fold + 0 [2,3,5]
vaut 10.
Le résultat est, soit la valeur init si la liste l est vide, soit le résultat de l’exécution de f entre le car de la
liste l et le résultat de l’appel de fold sur le reste des éléments.
Ecrivez votre version Scheme sous la forme d’une fonction à trois paramètres. Testez la sur quelques exemples.
Modifiez votre fonction pour pouvoir réaliser l’équivalent des opérations suivantes :
sum = fold add 0
prod = fold mul 1
compose
Comment redéfinir la fonction compose afin que la première fonction puisse être appliquée à un nombre
arbitraire de paramètres, comme dans :
((compose - +) 2 3 5 7 8)
=⇒
-25
Comment redéfinir (encore!) cette fonction compose, pour qu’elle puisse composer un nombre illimité de fonctions?
((compose abs sin cos) -2.2734)
=⇒
0.602162
c?r
On se propose de définir un générateur de fonction d’accès à un élément arbitraire d’une structure binaire.
Ce générateur accepte un paramètre, qui est une liste contenant les symboles a ou d, suivant que le chemin
d’accès à l’élément désiré nécessite de choisir le car ou le cdr . Exemple :
((c?r ’(a d d)) ’(1 2 3 4 5))
=⇒
3
Le résultat de l’exécution de (c?r ’(a d d)) ayant ici le même effet que caddr. Notons que la liste paramètre
peut être aussi longue que l’on veut, ou éventuellement vide.
65
4.6. EXERCICES D’APPLICATION
producteur/distributeur
Ecrire une fonction fournissant des couples producteur/distributeur .
Le producteur est une fonction à un paramètre. La valeur de ce paramètre est conservée dans une structure
partagée avec le distributeur. Celui-ci est une fonction sans paramètre, qui, à chaque appel, fournit l’un des
objets produits, ou faux si il n’y en a aucun en réserve. Naturellement, chaque appel de la fonction de génération
fournit une nouvelle paire, dont le fonctionnement est indépendant de tout autre couple produit. Ex :
(define w (make-producer/retailer))
(define producer (car w))
(define retailer (cadr w))
(producer 1)
(producer 2)
(producer ’hello)
(retailer)
=⇒ 1
(retailer)
=⇒ 2
(producer 3)
(retailer)
=⇒ hello
(retailer)
=⇒ 3
(retailer)
=⇒ ()
(retailer)
=⇒ ()
(retailer)
=⇒ ()
(producer ’ok)
(retailer)
=⇒ ok
...
Mémoı̈sation
La technique de la mémoı̈sation est relativement classique en programmation fonctionnelle. Elle consiste
à conserver, associés à une fonction f , des couples (xi , f (xi )) correspondant à des appels antérieurs de f ,
et rassemblant le paramètre d’appel et la valeur correspondante de la fonction. Puisque nous travaillons en
“programmation fonctionnelle”, le résultat de f (x), pour un x donné, est toujours le même. Il est donc légitime,
lors d’un nouvel appel de la fonction avec le même paramètre, de fournir un résultat calculé précédemment.
On se propose d’écrire une fonction, make-memoise-function, qui accepte comme paramètre une fonction à
un argument, f , et fournisse en résultat une fonction à un argument, g, qui se comporte comme f . Cette fonction
g mémoı̈se les appels de f , fournissant un résultat antérieur si son paramètre est déjà connu, ou réalisant un
appel de f si le paramètre est “nouveau”.
Séries entières
On s’intéresse aux séries entières
'
ai xi /i!
i≥0
où ai est une fonction calculable. Par exemple l’exponentielle correspond à ai = 1, la fonction cosinus à la
fonction définie par :

si i = 0 mod 4
 1
ai 0→
−1 si i = 2 mod 4

0
sinon
1. Construire serie-exp, serie-cos,. . .
2. Développer les opérations de somme, de produit par un scalaire.
3. Définir les fonctions de dérivation et d’intégration sur les séries entières.
4. La notion de fonction associée à une série entière est vue à travers la notion d’approximation par les N
premiers termes (N ≥ 0 donné). Définir cette notion de fonction et en déduire une définition de π via la
résolution de l’equation sin(x) = 1/2 par dichotomie entre 0 et 1, 5.
66
CHAPITRE 4. FONCTIONS
Ensembles d’entiers
On se propose ici de définir des ensembles d’entiers de la forme {x ∈ N | φ(x)} où φ est un prédicat (fonction
caractéristique) calculable.
1. Construire les ensembles : vide, entiers, multiples de k, singleton n, carrés parfaits, intervalle m, n,. . .
2. Définir le prédicat appartient? d’appartenance d’un entier à un ensemble.
3. Développer les opérations intersection, union, complémentaire, . . .
Autour des nombres premiers
1. Ecrire un prédicat (divise? x i j) qui est vrai si ∃k ∈ [i, j].k|x. En déduire le prédicat (premier? x).
2. Ecrire les fonctions renvoyant, respectivement, le plus petit premier strictement plus grand que x et le
plus grand premier plus petit que x.
3. En déduire les fonctions (kieme-premier k) et (rang-premier x), qui retournent respectivement le
kième nombre premier, et l’ordre du plus grand nombre premier plus petit que x.
4. Généraliser au problème d’une fonctionnelle (rang-fun f n) qui renvoie le plus grand entier k tel que
f (k) ≤ x, pour f croissante.
Chapitre 5
Algorithmes en Scheme
Nous allons maintenant nous intéresser à l’utilisation des listes, en tant que structure de données, pour la
réalisation de divers algorithmes, dont le but n’est plus simplement cette fois d’apprendre à manipuler les listes,
mais de progresser dans la connaissance de la notion de liste, ainsi que dans l’utilisation de Scheme. Ce chapitre
introduit également les opérations permettant la modification physique des valeurs de Scheme. Nous insisterons
en particulier sur les aspects liés à l’évaluation des ressources consommées par nos algorithmes.
5.1
Ressources
Nous vivons dans un univers où les ressources sont limitées : l’espace, le temps, l’argent nous font souvent
cruellement défaut. Même si l’argent ne fait pas le bonheur, il permet d’acquérir des machines plus rapides, et
disposant de plus de mémoire. Faute d’argent, il nous faut apprendre à vivre avec nos ressources limitées, et les
gérer de notre mieux. Comprendre ce qui se passe lorsqu’un programme s’exécute peut nous y aider.
5.1.1
Remarques sur les algorithmes
Les manipulations de listes font appel à des fonctions de bas niveau : créer une cellule, extraı̂re le car ou le
cdr d’une cellule, et recommencer un certain nombre de fois. Par exemple, la fonction suivante permet d’ajouter
un élément au bout d’une liste :
(define (append1 l e)
(if (null? l)
(list e)
(cons (car l) (append1 (cdr l) e))))
Un appel typique de la fonction est :
(append1 ’(a b c d) ’x)
Cette fonction est-elle optimale ? On peut raisonnablement penser qu’elle l’est, du moins dans le cadre de la
programmation fonctionnelle. Elle opère en effet par recopie d’une liste de n éléments, en n étapes, et il y a
création de n cellules de mémoire. Nous utiliserons la notation O(n) (dite “O de n”, ou “grand O de n”) pour
caractériser un tel algorithme.1
La concaténation de deux listes peut alors être vue comme la concaténation successive de chaque élément
de la seconde liste au bout de la première. C’est ce que fait cette fonction :
(define (konkat l1 l2)
(if (null? l2)
l1
(konkat (append1 l1 (car l2)) (cdr l2)))))
Quel est l’ordre de grandeur de cet algorithme? Une lecture superficielle de la fonction laisserait supposer qu’il
est également en n : le nombre d’appels récursifs est en effet égal à la taille de la liste l2. Mais, pour chaque
1 En programmation impérative, on peut par exemple imaginer que l’on va modifier la dernière cellule de la liste (même Scheme
permet ceci) pour qu’elle pointe sur une nouvelle cellule, contenant l’élément à placer au bout, évitant ainsi la duplication de la
liste. L’algorithme reste cependant en O(n), mais il n’y a plus consommation que d’une cellule, au lieu de n + 1. On peut aussi
envisager de représenter la liste par une structure un peu plus complexe, par exemple par une cellule pointant sur la tête et la
queue de la liste. L’ajoût en queue de liste devient ainsi une opération en O(1).
67
68
CHAPITRE 5. ALGORITHMES EN SCHEME
élément de la liste, on va utiliser la fonction append1, qui travaille en O(n). Notre algorithme est donc en réalité
en O(n2 ), aussi bien en ce qui concerne le temps d’exécution que le nombre de cellules mémoires utilisées.
La difficulté d’évaluer l’ordre de grandeur d’un algorithme Scheme n’est cependant pas plus grande que dans
un langage traditionnel. Il nous faut simplement tenir compte du fait que certaines fonctions, y compris des
fonctions primitives, peuvent avoir des temps d’exécution qui dépendent de la taille de leurs opérandes.
Revenons à notre concaténation. Un algorithme beaucoup plus raisonnable est le suivant :
(define (quonquat l1 l2)
(if (null? l1)
l2
(cons (car l1) (quonquat (cdr l1) l2))))
Cette fois ci, le temps d’exécution, tout comme le nombre de cellules utilisées sont proportionnels à la taille de
la première liste. Notre algorithme est maintenant en O(n), et probablement proche de l’optimum. La fonction
primitive append, qui place bout à bout deux listes (n listes, en fait) opère selon cet algorithme.
5.1.2
Critères
Nous avons avons fait intervenir, dans notre analyse, les critères de temps et d’espace, et ce d’une manière
un peu abstraite. Essayons de préciser un peu notre vision de ces deux éléments.
5.1.2.1
Le temps
Celui-ci est le plus évident. L’analyse de complexité d’un programme permet de le classer en O(n), O(n2 ),
O(log n), etc. On peut tenter d’affiner cette analyse en associant des coûts aux diverses opérations et constructions du langage. En pratique, une méthode raisonnable consiste à classer les primitives en opérations dont le
temps d’exécution est indépendant des opérandes, et en opérations dont la durée dépend des opérandes.
La plupart des fonctions de bas niveau appartiennent à la première catégorie. Des fonctions comme car,
cdr, cons, eq?, opèrent en temps constant. Ce n’est plus vrai pour certaines fonctions primitives dont on peut
considérer que le niveau est “plus élevé” : ainsi, le prédicat equal?, lorsqu’il est appliqué à des listes, va effectuer
une exploration arborescente de ses opérandes. Le temps d’exécution va être en O(n). De même, les fonctions
arithmétiques opérant sur des big nums vont également bénéficier de temps d’exécution qui dépendent de la
taille — nombre de chiffres — des opérandes. La fonction primitive append, qui place bout à bout deux listes,
va recopier la première, opérant ainsi en O(n). Enfin, la fonction map applique une opération en un temps
proportionnel à la taille de ses arguments. Suivant cette opération, l’ordre de grandeur sera n, n log n, n2 , etc.
5.1.2.2
L’espace
Nous faisons ici appel à cette notion car Scheme (tout comme Lisp) est un langage très dynamique, dont la
plupart des algorithmes reposent sur l’utilisation de cellules de mémoire, allouées dynamiquement. Si l’allocation
d’une cellule de mémoire est une opération unitaire (le système constitue à l’initialisation une liste de cellules
libres, dans laquelle il puise pour chaque allocation de mémoire), la mémoire constitue en elle-même une ressource
limitée. L’épuisement de la liste libre va déclancher un algorithme relativement complexe, le garbage collector ,
dont le but va être de récupérer le plus grand nombre possible de cellules qui ont été allouées à un moment
donné, mais ne sont plus actuellement utilisées, afin de reconstituer cette liste libre. Ces algorithmes (ils sont
nombreux et variés) sont typiquement en O(n), n représentant cette fois la taille de la mémoire, c’est à dire le
nombre total de cellules.
Un autre aspect intervient encore, qui est l’aspect récursif du langage. La récursion fait appel à la pile
d’exécution pour conserver le contexte de la fonction appelante. Ainsi, dans l’exemple suivant :
(define (fact n)
(if (= n 0)
1
(* n (fact (- n 1)))))
chaque appel récursif de la fonction fact nécessite la sauvegarde de la variable locale n, du contexte de l’appel,
etc. Un algorithme très récursif peut ne pas s’exécuter correctement s’il conduit à un dépassement de la pile
d’exécution. Ainsi, l’écriture
(define (somme x y)
(if (= x 0)
y
(+ 1 (somme (- x 1) y))))
(somme 100000 100000)
5.2. ALGORITHMES ITÉRATIFS
69
va probablement conduire à une erreur d’exécution. Il est alors nécessaire de transformer l’algorithme, éventuellement en faisant apparaı̂tre une récursion terminale. Nous allons maintenant étudier cette technique.
5.2
Algorithmes itératifs
L’une des caractéristiques de Scheme est de permettre l’expression d’algorithmes itératifs sous forme récursive. Ce résultat est bien connu : un algorithme itératif peut toujours s’écrire sous forme récursive. On suppose
l’algorithme ramené à la forme suivante :
while -condition.
begin
-affectation de variables.
end
-expression finale.
On suppose en fait que l’état du calcul est modélisé par un ensemble de variables, qui reçoivent à chaque étape
un nouvel ensemble de valeurs.
La version récursive de l’algorithme s’écrit :
(define (fun -variables d’état .)
(if -condition.
-expression finale.
(fun -nouvelles valeurs.)))
Les -variables d’état . deviennent les paramètres de la fonction, la récursion permettant de transférer les nouvelles
valeurs des variables d’état.
Comment transformer un algorithme itératif en algorithme récursif? Reprenons l’exemple de notre fonction
somme, que l’on ne s’autorise à écrire qu’avec les fonctions d’incrémentation et de décrémentation. L’algorithme
itératif peut s’écrire :
while x > 0
begin
x := x - 1;
y := y + 1;
end
le résultat étant disponible dans la variable y en fin d’itération. La version récursive s’en déduit tout aussitôt :
somme x y = if x > 0
then somme (x-1) (y+1)
else y
qui va s’écrire en Scheme :
(define (somme x y)
(if (> x 0)
(somme (- x 1) (+ y 1))
y))
Exprimé sous cette forme, notre algorithme diffère de manière significative de sa première version : il est
exprimé sous forme récursive terminale.
Que se passe-t-il lorsque le calcul de (somme (- x 1) (+ y 1)) est réalisé? La valeur fournie comme résultat
de l’appel interne est immédiatement fournie, sans transformation aucune, comme résultat. Un évaluateur rusé
peut sauter cette étape, en la remplaçant par l’équivalent d’un branchement à la procédure interne, ici somme
également. C’est ce qui va se passer ici : l’appel récursif interne va en fait être traduit en “affecter à x et y les
valeurs (- x 1) et (+ y 1) respectivement, puis se brancher au début de la fonction.
Le mécanisme d’exécution a donc transformé notre processus récursif en un processus itératif. Cette fois,
l’exécution de
(somme 100000 100000)
70
CHAPITRE 5. ALGORITHMES EN SCHEME
va nous fournir un résultat correct sans aucun débordement de pile !
On peut tenir le même raisonnement sur l’exemple de la factorielle. Le calcul de 7! peut se représenter par
les étapes suivantes :
7!
7 × 6!
7 × 6 × 5!
7 × 6 × 5 × 4!
7 × 6 × 5 × 4 × 3!
7 × 6 × 5 × 4 × 3 × 2!
7 × 6 × 5 × 4 × 3 × 2 × 1!
7 × 6 × 5 × 4 × 3 × 2 × 1 × 0!
7×6×5×4×3×2×1×1
7×6×5×4×3×2×1
7×6×5×4×3×2
7×6×5×4×6
7 × 6 × 5 × 24
7 × 6 × 120
7 × 720
5040
Faisons apparaı̂tre les premières lignes comme une suite de produits d’un nombre par une factorielle :
La version itérative s’en déduit :
1 × 7!
7 × 6!
42 × 5!
210 × 4!
840 × 3!
2520 × 2!
5040 × 1!
5040 × 0!
r := 1
n := 7
while n > 0
begin
r := r * n;
n := n - 1;
end
version qui ne fait plus apparaı̂tre qu’un compteur n, et un résultat partiel, r. Le programme Scheme terminal
récursif équivalent est :
(define (fact n r)
(if (> n 0)
(fact (- n 1) (* r n))
r))
Le calcul de 7! s’écrivant alors (fact 7 1).
Plus généralement, ce mécanisme s’applique à tout appel de fonction placé en position terminale :
(define (even? int)
(if (zero? int) #t
(odd? (- int 1))))
(define (odd? int)
(if (zero? int) #f
(even? (- int 1))))
Ce couple de fonction à récursivité croisée opère à la façon d’une boucle, sans utiliser d’espace dans la pile.
5.3
La liste, structure de donnée
Nous avons présenté dans les chapitres précédents une structure de donnée jouant un rôle fondamental dans
le langage Scheme, la liste. La notion de liste, en tant que structure de données dépasse naturellement le cadre
du langage Scheme, ce que nous allons montrer ici.
71
5.3. LA LISTE, STRUCTURE DE DONNÉE
5.3.1
Définitions
Une liste est une séquence finie, de zéro, un ou plusieurs éléments. Si tous les éléments d’une liste sont du
même type, par exemple des nombres ou des caractères, nous parlerons de listes de nombres ou de caractères.
De telles listes sont dite homogènes. Certains langages de programmation imposent que tous les éléments d’une
liste soient du même type. Ce n’est pas le cas en Scheme, où les éléments d’une liste peuvent être de types
quelconques.
Cependant, la notion de liste, ou de séquence, peut aussi se traduire en Scheme par d’autres représentations,
telles les chaı̂nes de caractères, ou les vecteurs.
5.3.1.1
Longueur d’une liste
La longueur d’une liste est le nombre d’éléments dans la liste. Si ce nombre est zéro, on dit que la liste est
vide. La liste vide est représentée par une paire de parenthèses, (), parfois par un epsilon, ).
5.3.1.2
Eléments d’une liste
Les algorithmes s’intéressent souvent au premier élément d’une liste, la tête, et au reste des éléments, la
queue.
On utilise parfois les termes de sous-liste, obtenue en supprimant les n premiers et les p derniers éléments
d’une liste, et de sous-séquence, obtenue en supprimant certains éléments d’une liste, tout en conservant les
positions relatives. Pour donner un exemple, si on considère la liste L contenant les éléments suivants :
(1 2 3 4 5 6 7 8 9 10)
on peut qualifier ainsi certaines parties de cette liste :
1
(2 3 4 5 6 7 8 9 10)
(5 6 7 8)
(1 2)
(2 3 5 7 8)
5.3.2
la tête
la queue
une sous − liste
une autre sous − liste
une sous − séquence
Utilisation des listes : le tri
Les algorithmes de tri sont des passages obligés dans toute carrière de programmeur. Les algorithmes de tri
sont nombreux, d’efficacité et de complexité diverses.
5.3.2.1
Tri par insertion
Nous débuterons par un algorithme simple, qui consiste à prendre les éléments les uns après les autres, et à
les insérer dans une liste déja triée.
Une première procédure, insert-element, permet d’insérer un élément dans une liste triée (en ordre croissant). L’élément est comparé avec les éléments successifs de la liste, et placé en tête de ce qui reste de la liste dès
qu’il est reconnu inférieur ou égal à la tête de la liste. Voici cette procédure, avec un exemple de son utilisation :
1 ]=> (define (insert-element n l)
(cond
((null? l) (list n))
((<= n (car l)) (cons n l))
(else (cons (car l) (insert-element n (cdr l))))))
1 ]=> (insert-element 34 ’(2 3 5 7 12 28 31 37 39 40 55))
;Value: (2 3 5 7 12 28 31 34 37 39 40 55)
Trier une liste ne revient plus, maintenant, qu’à insérer les éléments successifs de la liste initiale, non triée, dans
la liste qui contiendra les résultats, et qui est initialement vide :
1 ]=> (define (inserer-tous elems l)
(cond
((null? elems) l)
(else (inserer-tous (cdr elems) (insert-element (car elems) l)))))
1 ]=> (inserer-tous ’(9 2 4 1 9 2 4 7 2 3 5 4) ’())
;Value: (1 2 2 2 3 4 4 4 5 7 9 9)
72
CHAPITRE 5. ALGORITHMES EN SCHEME
Fig. 5.1 - Tri de liste – Algorithme 1 : tri par insertion
(define (tri1 l)
(define (insert-element n l)
(cond
((null? l) (list n))
((<= n (car l)) (cons n l))
(else (cons (car l) (insert-element n (cdr l))))))
(define (inserer-tous elems l)
(cond
((null? elems) l)
(else (inserer-tous (cdr elems) (insert-element (car elems) l)))))
(inserer-tous l ’()))
La figure 5.1 montre ces deux procédures, incluses dans une procédure principale réalisant le tri.
Voici, pour terminer, un exemple d’utilisation de cette première fonction de tri :
1 ]=> (tri1 ’(2 9 4 6 3 9 1 20 5 2 8 3 5 83 6 28 54 62
23 73 52 10 0 4 9 34 45 76 87 67 46 28 49))
;Value: (0 1 2 2 3 3 4 4 5 5 6 6 8 9 9 9 10 20 23 28 28 34
45 46 49 52 54 62 67 73 76 83 87)
L’algorithme est simple à comprendre, mais est-il efficace? Quelle est sa complexité? Pour une liste de taille
n, il y a n éléments à insérer. Les insertions vont s’effectuer dans une liste initialement vide, puis qui comporte
un élément, puis deux, etc. Si les éléments de la liste initiale sont dans un ordre arbitraire, il faut parcourir en
moyenne la moitié de la liste résultat avant de pouvoir placer un élément. Le nombre d’opérations est de l’ordre
de 1+2+3+· · ·+(n−1)+n, soit n(n+1)/2, multiplié par un certain facteur constant, c’est à dire proportionnel
à n2 . La complexité de cette opération est donc O(n2 ). L’algorithme est dit quadratique, son temps d’exécution
étant multiplié par quatre chaque fois que la taille de l’entrée est doublée.
5.3.2.2
Tri par par fusion
Nous allons analyser et programmer un autre algorithme, à peine plus complexe. Celui-ci fait appel à une
stratégie très classique, dite divide and conquer : trier une liste de n éléments consiste à partager cette liste en
deux listes de tailles n/2, trier ces deux listes, puis les fusionner. La méthode est récursive, l’étape finale est
triviale, puisqu’il s’agit de trier une liste qui ne comporte plus qu’un élément.
Voici l’une des premières briques de l’ensemble, la procédure qui réalise la fusion de deux listes déjà triées.
Il faut tester les cas limites où l’une des listes est vides. Dans le cas général, le résultat est une liste dont le
premier élément est le plus petit des têtes de chaque liste, et dont le reste est le résultat de la fusion des restes
des listes.
1 ]=>
1 ]=>
(define (merge l1 l2)
(cond
((null? l1) l2)
((null? l2) l1)
((< (car l1) (car l2))
(cons (car l1) (merge (cdr l1) l2)))
(else (cons (car l2) (merge l1 (cdr l2))))))
(merge ’(2 3 5 7 8 9 10 10 10 11 14 17 19)
’(2 3 4 6 8 9 9 10 12 20 34 37 40))
;Value: (2 2 3 3 4 5 6 7 8 8 9 9 9 10 10 10 10 11 12 14 17 19 20 34 37 40)
5.3. LA LISTE, STRUCTURE DE DONNÉE
73
Voici une seconde partie de l’algorithme. Il s’agit ici de partager une liste en deux listes de tailles sensiblement
égales. La fonction utilise donc trois paramètres, la liste d’entrée, l, et les deux listes de sortie l1 et l2. Plutôt
que de faire des calculs savants sur la taille de l, l’algorithme choisi consiste à placer les éléments successifs de
l alternativement dans l1 et l2 (notons dans l’appel récursif l’échange de l1 et l2). La question est de savoir
que faire de ces deux résultats. Dans cette version d’essai, nous nous contentons d’en faire la liste :
1 ]=> (define (cut l l1 l2)
(if (null? l)
(list l1 l2)
(cut (cdr l) (cons (car l) l2) l1)))
1 ]=> (cut ’(2 2 3 3 4 5 6 7 8 8 9 9 9 10 10 10 10 11 12 14 17 19 20 34 37 40)
’() ’())
;Value: ((40 34 19 14 11 10 10 9 8 7 5 3 2) (37 20 17 12 10 10 9 9 8 6 4 3 2))
Fig. 5.2 - Tri de liste – Algorithme 2 : tri par fusion
(define (tri2 l)
(define (merge l1 l2)
(cond
((null? l1) l2)
((null? l2) l1)
((< (car l1) (car l2))
(cons (car l1) (merge (cdr l1) l2)))
(else (cons (car l2) (merge l1 (cdr l2))))))
(define (sort-aux l)
(if
(or (null? l) (null? (cdr l)))
l
(cut l ’() ’())))
(define (cut l l1 l2)
(if (null? l)
(merge (sort-aux l1) (sort-aux l2))
(cut (cdr l) (cons (car l) l2) l1)))
(sort-aux l))
Dans la version définitive (cf. figure 5.2), les résultats de la fonction cut sont triés, par appel de sort-aux,
puis fusionnés par merge.
La fonction de tri auxiliaire, sort-aux, fournit directement comme résultat le paramètre, si celui-ci est une
liste vide ou comportant un seul élément, ou fait appel à la fonction cut.
Enfin, la fonction principale effectue simplement un appel à sort-aux.
Il est satisfaisant de constater que notre nouvel algorithme fournit les mêmes résultats que le premier :
1 ]=> (tri2 ’(2 9 4 6 3 9 1 20 5 2 8 3 5 83 6 28 54 62
23 73 52 10 0 4 9 34 45 76 87 67 46 28 49))
;Value: (0 1 2 2 3 3 4 4 5 5 6 6 8 9 9 9 10 20 23 28 28 34
45 46 49 52 54 62 67 73 76 83 87)
Ce nouvel algorithme, sensiblement plus long que le premier, est cependant meilleur que celui-ci sur le plan
de la complexité. Quelles sont, en effet, les opérations réalisées? Deux des opérations impliquées ont des temps
d’exécution qui sont proportionnels au nombre d’éléments : le partage de la liste initiale, puis la fusion des listes
triées. Combien de fois ces opérations sont-elles exécutées ? La liste initiale, de taille n, est fractionnée une
première fois (puis ses élements sont ultérieurement rassemblés). Les deux listes obtenues, de taille n/2, vont
74
CHAPITRE 5. ALGORITHMES EN SCHEME
subir le même sort, puis les quatre listes de taille n/4, et ainsi de suite. Chacune de ces étapes a un temps
d’exécution proportionnel à n. Il y a une étape pour la liste de taille n, une pour les listes de taille n/2, une
pour les listes de taille n/4, etc. Le nombre d’étapes est donc proportionnel au logarithme en base deux de
n. Le temps total de l’algorithme est donc proportionnel à n multiplié par log n : sa complexité est O(n log n).
L’algorithme se comporte nettement mieux que le premier.
Voici, pour fixer les idées, une simulation des durées de ces deux algorithmes sur des listes identiques.
Imaginons que, pour une certaine liste L, le premier algorithme donne un temps d’exécution d’une milliseconde,
le second de deux millisecondes. On peut calculer, à partir de cette donnée, les durées d’exécution pour des
listes de taille double, triple, etc., jusqu’à un facteur 1000. Voici ces résultats :
f acteur
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
15.
20.
25.
30.
35.
40.
45.
50.
100.
200.
500.
1000.
tri1
1.
4.
9.
16.
25.
36.
49.
64.
81.
100.
121.
144.
225.
400.
625.
900.
1225.
1600.
2025.
2500.
10000.
40000.
250000.
1000000.
tri2
2.
6.3
12.0
18.5
25.8
33.6
42.0
50.7
59.8
69.2
78.9
88.8
120.0
175.7
235.0
297.3
361.9
428.6
497.1
567.2
1331.6
3060.4
8968.6
19934.4
Dès que les tailles des données manipulées deviennent importantes, les différences de performances sont extrêmement sensibles : dans le dernier cas, le second algorithme ne met que 20 secondes, là où le premier prend près
de 20 minutes ! On voit l’intérêt du calcul de complexité pour évaluer les performances d’un programme, et permettre d’éliminer, a priori, certaines solutions dont on peut deviner qu’elles conduiront à des temps d’exécution
désastreux...
5.3.3
Modification physique des listes
Bien que toutes les applications relatives aux listes puissent être réalisées dans un mode fonctionnel pur,
certains algorithmes traditionnels utilisent des opérations de modification physique de la structure représentant
la liste.
Scheme comporte de nombreux traits impératifs, et en particulier des opérations qui permettent des telles
modifications physiques des paires pointées utilisées dans la représentation des listes. On dit que ces opérations
comportent un effet de bord (side-effect en anglais), car, en plus du résultat qu’elles fournissent, elles ont modifié
quelque chose dans le système. Deux procédures permettent donc de modifier le contenu des paires pointées,
set-car! et set-cdr! :
(set-car! -pair . -obj .)
(set-cdr! -pair . -obj .)
Ces opérations remplacent physiquement le car (ou le cdr ) de leur premier paramètre par le second. Exemples :
1 ]=> (define toto (cons 3 6))
1 ]=> toto
;Value: (3 . 6)
1 ]=> (set-car! toto 7)
5.3. LA LISTE, STRUCTURE DE DONNÉE
75
1 ]=> toto
;Value: (7 . 6)
5.3.4
Pile
Une pile (stack , en anglais) est un type de données basé sur le modèle des listes, toutes les opérations étant
effectuées sur l’une des extrémités de la liste, dite sommet de la pile. Les opérations de base sur les piles sont :
sommet (top),qui fournit la valeur de l’élement situé au sommet de la pile, empiler (push), et dépiler (pull ).
Empiler ajoute un élément au sommet de la pile ; cet élément devient donc le nouveau sommet. Dépiler fournit
l’élément situé au sommet de la pile, en le retirant de la pile. La pile peut être vide, auquel cas les opérations
sommet et dépiler sont interdites. Le prédicat vide? (empty? ) permet de tester ce cas. Une pile est dite travailler
en LIFO, pour last-in, first-out.
5.3.4.1
Une implantation des piles
Réaliser une pile est (presque) trivial grâce aux fonctions de Scheme. Une première idée est d’utiliser l’implantation suivante :
(define
(define
(define
(define
(define
(new-stack) ’())
(empty? pile) (null? pile))
(top pile) (car pile))
(push valeur pile) (cons valeur pile))
(pull valeur pile) ?)
Cependant, l’implantation de push n’est pas très satisfaisante : il faudrait pouvoir réaffecter à la variable représentant la pile le résultat de la fonction. Enfin, l’implantation de pull est particulièrement délicate : il faut à la
fois fournir un résultat, et modifier la pile.
Une meilleure solution est d’utiliser une structure intermédiaire, par exemple une paire-pointée, pour “supporter” la pile. Convenons de conserver dans le car de cette paire, qui ne sera pas utilisé par les algorithmes, un
symbole particulier – par exemple, stack2 – et dans le cdr la valeur effective de la pile. Nous obtenons alors :
(define (new-stack) (cons ’stack ’()))
(define (stack? x) (and (pair? x) (eq? ’stack (car x))))
(define (empty? pile) (or (not (stack? pile)) (null? (cdr pile))))
(define (top pile)
(if (empty? pile)
#f
(cadr pile)))
(define (push valeur pile)
(if (stack? pile)
(set-cdr! pile (cons valeur (cdr pile)))
#f))
(define (pull pile)
(define (aux u p)
(set-cdr! p (cdr (cdr p)))
u)
(if (empty? pile)
#f
(aux (cadr pile) pile)))
Nous avons introduit un nouveau prédicat, stack?, qui permet de vérifier si un objet est une pile (représentée
selon nos conventions). Enfin, une fonction auxiliaire, aux, dans pull, nous permet de rendre un résultat,
l’ancien sommet de la pile, puis de modifier la pile. Pour ce faire, la fonction aux est appelée avec un premier
paramètre, le (cadr pile), qui est la valeur à fournir en résultat. Cette valeur est calculée à l’appel de aux,
donc avant la modification effective de la pile. La fonction réalise alors la modification physique de la pile, qui lui
a été transmise comme second argument, et fournit la valeur de l’ancien sommet, qu’elle avait reçu en premier
argument.
2 Ce
symbole nous servira uniquement de marqueur , nous permettant de reconnaı̂tre cette structure comme une pile.
76
CHAPITRE 5. ALGORITHMES EN SCHEME
5.3.4.2
La fonction reverse gr^
ace aux piles
Une pile, on l’a vu, travaille en LIFO ; son contenu est restitué dans l’ordre inverse où les éléments on été
introduits. Transférer le contenu d’une pile dans une autre fournit une pile contenant les mêmes éléments que
la pile originelle, mais conservés dans l’ordre inverse. Puisque nos piles sont réalisées avec des listes ordinaires,
on peut utiliser cette propriété pour obtenir la fonction d’inversion de listes, reverse.
Voici cet algorithme :
(define (rev source destination)
(if (null? source)
destination
(rev (cdr source) (cons (car source) destination))))
Un exemple d’utilisation :
1 ]=> (rev ’(a b c d e) ’(u v w))
;Value: (e d c b a u v w)
L’inverse d’une liste s’obtient par :
1 ]=> (rev ’(1 2 3 4) ’())
;Value: (4 3 2 1)
La fonction reverse, dans sa forme traditionnelle, peut donc s’écrire :
(define (reverse liste)
(define (rev source destination)
(if (null? source)
destination
(rev (cdr source) (cons (car source) destination))))
(rev liste ’()))
5.4
Exercices
Complexités
Analyser la complexité des différents exercices des chapitres antérieurs (que vous n’avez certes pas manqué
de traiter). Cette analyse vous suggère-t-elle des améliorations?
tri par sélection
Nous nous proposons d’implanter l’algorithme de tri par sélection : à chaque étape du tri, le plus petit
élément (ou plus exactement, l’un des plus petits) de la liste est sélectionné ; il est placé en tête du résultat du
tri de la liste restante, une fois que l’on a supprimé cet élément.
Implantez cet algorithme ; caractérisez sa complexité.
Les classiques
Transformez en fonctions à récursivité terminale les fonctions factorielle, Fibonacci, append, reverse
et quelques autres, dont vous avez le sentiment que ça peut marcher.
Peut-on faire la même chose avec la fonction d’Ackerman?
Analyse de programmes
Ecrire une procédure analysant le texte source d’une fonction, et déterminant quels sont les appels récursifs
finaux de celle-ci.
Chapitre 6
Structures de données en Scheme
Nous allons dans ce chapitre aborder la notion de type et de structures de données, d’abord par une étude
des caractéristiques des types de données natifs de Scheme, ensuite par la création de nouveaux types. Ce
sera l’occasion de revenir plus en détail sur certains aspects du langage, comme les notions de fermetures et
d’environnements.
6.1
Les séquences
Nous avons déja abordé la notion de séquence, en signalant qu’elle pouvait se traduire par plusieurs représentations en Scheme : listes, mais aussi vecteurs ou chaı̂nes.
Nous allons voir certaines des spécificités de ces divers types d’objets.
6.1.1
Vecteurs
Les vecteurs du langage Scheme correspondent grossièrement aux tableaux des langages C ou Pascal.
Un vecteur est une suite d’éléments physiquement contigüs dans la mémoire de l’ordinateur. Les éléments
d’un vecteur sont désignés directement par leur indice, qui est leur position dans le vecteur. Voici les indices
d’un vecteur de huit éléments :
0
1
2
3
4
5
6
7
Puisque les éléments sont consécutifs en mémoire, il est possible de désigner immédiatement n’importe quel
élément d’un vecteur dès que l’on sait où se trouve le début de ce vecteur. On dit que l’accès à un élément d’un
vecteur est en O(1), contrairement à l’accès à un élément d’une liste, qui est en O(n).
Comme les listes, les vecteurs représentent des ensembles sur lesquels existe une relation d’ordre, puisque
les éléments sont désignés par des indices.
Enfin, les éléments d’un vecteur, tout comme ceux d’une liste, peuvent, en Scheme, représenter des objets
arbitraires.
6.1.1.1
Procédures primitives sur vecteurs
Différentes constructions permettent la création des vecteurs.
Constante vectorielle
Il existe une notation spécifique pour la représentation de constantes vectorielles. Elle consiste en un dièze,
#, suivi de la liste des éléments du vecteur. Voici quelques constantes vectorielles :
#(2 3 5 7)
#(T for 2 two 4 tea)
#((2 3 5) (6 7 8))
#(#(2 3 5) #(6 7 8))
Le premier de ces vecteurs est dit homogène, car tous ses éléments sont de même nature. Le second comporte
des symboles et des nombres. Le troisième vecteur est formé deux éléments, qui sont eux-mêmes de listes. Enfin,
les éléments du dernier sont eux-mêmes des vecteurs.
Tout comme les listes, les vecteurs doivent être “quotés” dans les expressions du langage :
77
78
CHAPITRE 6. STRUCTURES DE DONNÉES EN SCHEME
1 ]=> ’#(2 3 5 7)
;Value: #(2 3 5 7)
1 ]=> (cons ’hello ’#(2 3 5 7))
;Value: (hello . #(2 3 5 7))
Opérations vectorielles
Les principales opérations sur vecteurs sont décrites dans le tableau 6.2. Voici quelques manipulations sur
vecteurs :
1 ]=> (define v ’#(T for 2 two 4 tea))
;Value: v
1 ]=> (vector-length v)
;Value: 6
1 ]=> (vector-ref v 0)
;Value: t
1 ]=> (vector-ref v 2)
;Value: 2
1 ]=> (vector-ref v 3)
;Value: two
1 ]=> (vector->list v)
;Value: (t for 2 two 4 tea)
1 ]=> (vector 2 3 (+ 2 3) (* 2 3) (list 2 3))
;Value: #(2 3 5 6 (2 3))
1 ]=> (make-vector 10 6)
;Value: #(6 6 6 6 6 6 6 6 6 6)
1 ]=> (list->vector (cons ’hello ’(a b 6)))
;Value: #(hello a b 6)
On notera les deux opérations vector->list et list->vector permettant de passer un ensemble d’éléments
d’une représentation à une autre (de manière générale, les caractères -> apparaissent dans la plupart des
opérations réalisant une conversion de données).
Les vecteurs ne jouent pas en Scheme un rôle aussi important que dans les langages traditionnels, car il
existe la liste, qui est une structure de données sensiblement plus puissante et plus souple dans le cadre de la
programmation fonctionnelle.
6.1.1.2
Utilisation des vecteurs
Nous allons analyser quelques algorithmes opérant sur vecteurs.
Maximum d’un vecteur
Voici un premier algorithme recherchant le plus grand élément d’un vecteur. Il consiste en un simple balayage
des éléments du vecteur. La fonction interne utilise deux paramètres, l’indice courant, i, qui varie de 1 jusqu’à
la taille du vecteur1 , et le plus grand élément trouvé jusqu’alors, m. La fonction teste le cas du vecteur vide, le
cas où le vecteur n’a qu’un élément rentrant dans le cas général :
1 ]=> (define (vector-max vec)
(define vl (vector-length vec))
(define (max i m)
(cond
((>= i vl) m)
((>= m (vector-ref vec i))
(max (+ i 1) m))
(else
(max (+ i 1) (vector-ref vec i)))))
(if (= vl 0)
#f
1 L’indice varie à partir de 1, car l’élément d’indice 0 est transmis comme “plus grand élément rencontré jusqu’alors”. La récursion
s’interrompt dès que cet indice courant est égal à la taille du vecteur, ce qui veut dire qu’à l’étape précédente on a pris en compte
le dernier élément du vecteur.
79
6.1. LES SÉQUENCES
(max 1 (vector-ref vec 0))))
;Value: vector-max
Voici quelques utilisations de cette procédure :
1 ]=> (vector-max ’#())
;Value: ()
1 ]=> (vector-max ’#(3))
;Value: 3
1 ]=> (vector-max ’#(2 9 3 5))
;Value: 9
L’algorithme ci-dessus ne présente pas de différence fondamentale avec un algorithme de recherche dans une
liste. Il faut en effet, comme dans le cas d’une liste, balayer l’ensemble des éléments du vecteur.
Recherche dans un vecteur
Cependant, le fait que les éléments d’un vecteur soient directement accessibles peut permettre d’utiliser des
algorithmes différents (et meilleurs). Prenons le cas de la recherche d’un élément dans une liste de nombres triée.
Il est naturellement possible, si par exemple la liste est triée en ordre croissant, d’interrompre la recherche dès
qu’un élément supérieur au nombre cherché est rencontré : on sait alors que le nombre n’est pas dans la liste.
Cependant, cette recherche a une complexité qui est toujours proportionnelle à la taille de la liste, c’est à dire
en O(n).
Il est possible, sur des vecteurs, d’appliquer un algorithme plus efficace : on peut comparer l’élément cherché
avec la valeur de l’élément situé au milieu du tableau. Suivant le résultat de cette comparaison, on peut éliminer l’une des deux sous-parties de ce tableau. On peut réitérer cette opération, jusqu’à ce que la sous-partie
sélectionnée soit réduite à un seul élément. L’élément cherché est donc égal, ou non, à cet élément, et est donc
présent, ou non, dans le tableau. L’intérêt de l’algorithme est que la taille de la section sélectionnée est divisée
par deux à chaque étape.
Voici une implantation possible de l’algorithme. La fonction interne, search, recherche l’élément dans l’intervalle [i, j]. Le test d’arrêt et la réduction de l’intervalle posent toujours de petits problèmes. Le milieu de
[i, j], k, est calculé comme la moyenne entière de i et j. On choisit alors l’un des deux segments [i, k] ou [k, j].
Cependant, lorsque j = i + 1, k est égal à i ou à j. Le test d’arrêt d’arrêt porte donc sur la taille de l’intervalle
[i, j] ; lorsque celui-ci n’a plus que deux éléments, on teste directement l’égalité du nombre cherché avec ces
deux éléments.
(define (vector-search vec elt)
(define vl (vector-length vec))
(define (search i j)
(if (< (- j i) 2)
(or (= elt (vector-ref vec i))
(= elt (vector-ref vec j)))
(let* ((k (round (/ (+ i j) 2)))
(v (vector-ref vec k)))
(if (> v elt)
(search i k)
(search k j)))))
(if (= vl 0)
#f
(search 0 (- vl 1))))
Un exemple d’utilisation :
1 ]=> (vector-search ’#(2 3 5 7 8 8 9 11 11 12 23 45 48 56 67 78 81
83 87 89 92 95 99 100 101 102 105 108 109 109 111)
56)
;Value: #t
Voici l’évolution des indices, ainsi que les valeurs situées aux limites de l’intervalle correspondant :
[0, 30]
[0, 15]
[8, 15]
[12, 15]
[12, 14]
[13, 14]
(2 111)
(2 78)
(11 78)
(48 78)
(48 67)
(56 67)
80
CHAPITRE 6. STRUCTURES DE DONNÉES EN SCHEME
Tris de vecteurs
Tout comme les recherches, les tris dans un vecteur vont tenir compte du fait que les éléments sont directement accessibles.
La figure 6.1 est un exemple d’un tel programme. La fonction s’applique à un tableau, qui va être trié in
situ, le critère étant la fonction de comparaison de deux éléments. L’algorithme utilisé est celui du quicksort .
Le principe consiste à choisir un élément du vecteur, qui va servir de pivot. On va ensuite balayer le vecteur,
de manière à grouper tous les éléments en trois zones : les éléments qui sont inférieurs au pivot, ceux qui lui sont
égaux (il y en a au moins un !) et ceux qui lui sont supérieurs. Les éléments vont être déplacés (par échange)
dans le tableau. Après cette première étape, les éléments égaux au pivot sont déjà à leur emplacement définitif.
Il ne reste plus qu’à trier les parties droite et gauche. Cette étape est proportionnelle à la taille du vecteur,
donc en n. Statistiquement, les tailles des parties qui restent sont de l’ordre de n/2, et chaque nouvelle étape va
aussi créer des sous-parties de tailles moitiés. Le nombre total de ces étapes est donc de l’ordre de log n. Comme
chaque étape manipule n éléments au plus, la complexité de l’algorithme est O(n log n).
Voici un exemple d’utilisation de l’algorithme, la fonction de comparaison utilisée étant < :
1 ]=> (define vvv (vector 4 0 17 54 28 34 45 28 5 0 267 51 42
34 65 82 9 53 6 0 324 16 45 723 341 54 87 29 98 76 432 34 173
23 435 17 24 378 3 62 73 47))
;Value: vvv
1 ]=> (quicksort! vvv <)
1 ]=> vvv
;Value: #(0 0 0 3 4 5 6 9 16 17 17 23 24 28 28 29 34 34 34 42 45 45
47 51 53 54 54 62 65 73 76 82 87 98 173 267 324 341 378 432 435 723)
Tableaux multidimensionnels
Il est souvent utile de pouvoir manipuler des tableaux multidimensionnels, c’est à dire des structures de
données contenant des éléments rangés suivant plusieurs indices. Les matrices sont un exemple typique de telles
structures à deux axes, les lignes et les colonnes. Certains langages de programmation, tel APL, offrent comme
structure native des tableaux multidimensionnels. Ce n’est pas le cas de Scheme, mais nous allons voir qu’il est
facile de proposer des solutions pour pallier ce manque.
La première consiste à utiliser des vecteurs de vecteurs. Une matrice de taille 3 par 4, contenant les éléments
suivants :
1
2
3
4
5
6
7
8
9
10
11
12
peut se représenter par le vecteur :
#(#(1 2 3 4) #(5 6 7 8) #(9 10 11 12))
Il est facile alors de définir les opérations d’accès aux éléments :
(define (matrix-ref mat i j)
(vector-ref (vector-ref mat i) j))
L’opération (matrix-ref mat i j) fournissant l’élément situé à la ligne i et à la colonne j de la matrice.
Une autre solution consiste à arranger les éléments de la matrice dans un vecteur, sous la forme :
#(1 2 3 4 5 6 7 8 9 10 11 12)
L’accès à l’élément d’indices i et j s’écrit alors :
(define (matrix-ref mat i j)
(vector-ref mat (+ (* i 4) j)))
Naturellement, cette dernière fonction ne s’applique qu’à des matrices de 4 colonnes. Il est simple là aussi de
trouver des solutions pour généraliser cette solution (cf. par exemple l’exercice 6.3).
6.1.2
Chaı̂nes
Les chaı̂nes sont des séquences de caractères (cf. section 2.2.1.1).
Syntaxiquement, une chaı̂ne de caractères est une constante. Il n’est donc pas nécessaire de la faire précéder
d’une quote dans les programmes. Diverses opérations (cf. fig. 6.2) s’appliquent aux chaı̂nes de caractères. Il
81
6.1. LES SÉQUENCES
Fig. 6.1 - Un Quicksort sur Vecteurs
;; Copyright Juha Heinanen 1988
;; Modifs JJG - 1993
(define (quicksort! vec less?)
(define (vector-swap! vec i1 i2)
; swaps vector elements in indices i1 and i2
(let ((temp (vector-ref vec i1)))
(vector-set! vec i1 (vector-ref vec i2))
(vector-set! vec i2 temp)))
; Sorts vector vec according to less?
(define (sort first last)
; sorts vector elements vec[first] ... vec[last]
(define pivot
(vector-ref vec (quotient (+ first last) 2)))
(define (partition left right)
; partitions vec[first] .. vec[last] so that
; (a) vec[k] <= pivot, when
;
k = first .. left-index - 1
; (b) vec[k] >= pivot, when
;
k = right-index + 1 .. last
; (c) vec[k] = pivot, when
;
k = right-index + 1 ... left-index - 1
(define left-index
(let repeat ((index left))
(if (less? (vector-ref vec index) pivot)
(repeat (+ index 1))
index)))
(define right-index
(let repeat ((index right))
(if (less? pivot (vector-ref vec index))
(repeat (- index 1))
index)))
; partition
(cond
((< left-index right-index)
(vector-swap! vec left-index right-index)
(partition (+ left-index 1) (- right-index 1)))
((= left-index right-index)
(cons (+ left-index 1) (- right-index 1)))
(else
(cons left-index right-index))))
; sort
(let* ((indexes (partition first last))
(left-index (car indexes))
(right-index (cdr indexes)))
(if (< first right-index)
(sort first right-index))
(if (< left-index last)
(sort left-index last))))
; quicksort
(let ((high (- (vector-length vec) 1)))
(if (>= high 0) (sort 0 high))))
82
CHAPITRE 6. STRUCTURES DE DONNÉES EN SCHEME
est possible également de convertir une chaı̂ne en une liste de caractères, par string->list et réciproquement
(list->string).
Enfin, certaines opérations spécialisées permettent de comparer entre elles des chaı̂nes de caractères, la
comparaison pouvant ne pas tenir compte de la distinction entre majuscules et minuscules (cf. [R4R90], § 6.7).
Les chaı̂nes de caractères sont des structures homogènes, qui offrent une représentation compacte pour des
séquences de caractères.
6.1.3
Opérations sur séquences
Fig. 6.2 - Opérations sur Séquences
Opération
Création
Liste
(list -k . . . . )
Type
Taille
Premier élément
Reste
Elément -n.
Mod. élément -n.
Concaténation
(list? -s.)
(length -s.)
(car -s.)
(cdr -s.)
(list-ref -s. -n.)
Remplissage
(append -s1 . . . . )
(append! -s1 . . . . )
Chaı̂ne
(string -k . . . . )
(make-string -n.)
(make-string -n. -fill .)
(string? -s.)
(string-length -s.)
Vecteur
(vector -k . . . . )
(make-vector -n.)
(make-vector -n. -fill .)
(vector? -s.)
(vector-length -s.)
(string-ref -s. -n.)
(string-set! -s. -k . -n.)
(string-append -s1 . . . . )
(vector-ref -s. -n.)
(vector-set! -s. -k . -n.)
(string-fill! -s. -fill .)
(vector-fill! -s. -fill .)
Le tableau 6.2 résume les différentes opérations primitives du langage, s’appliquant aux différentes représentations des séquences.
6.1.4
Algorithmes fonctionnels
A côté de ces solutions “classiques”, que l’on peut programmer dans n’importe quel langage impératif,
Scheme nous permet de définir des formes “fonctionnelles” d’algorithmes.
Voici un exemple : on veut définir une fonction qui explore une liste selon un prédicat. Cependant, l’on ne
souhaite pas créer la liste des résultats, mais obtenir ceux-ci les uns après les autres. Une solution consiste
à définir l’opération sous la forme d’une fermeture, utilisant et modifiant la liste à chaque appel. Voici une
solution : les trois paramètres de la fonction sont la liste à explorer, le prédicat à appliquer, et la valeur à rendre
lorsqu’il ne reste plus aucun élément satisfaisant le prédicat dans la liste.
(define (make-list-searcher liste pred? neutre)
(define (aux)
(cond
((null? liste) neutre)
((pred? (car liste))
(let ((val (car liste)))
(set! liste (cdr liste))
val))
(else
(set! liste (cdr liste))
(aux))))
aux)
La fonction fournit comme résultat une fonction sans paramètres, qui, à chaque appel, délivre un nouvel élément
satisfaisant le prédicat, ou encore l’élément neutre signalant la fin de la recherche.
Voici un exemple d’utilisation de cette fonction, une même liste étant parcourue au moyen de deux prédicats
différents :
(define l ’(2 5 3 -6 7 -19 0 -2 -3 -2 5 8))
(define f1 (make-list-searcher l odd? #f))
6.1. LES SÉQUENCES
83
(define f2 (make-list-searcher l positive? #f))
(f1)
=⇒
5
(f1)
=⇒
3
(f1)
=⇒
7
(f1)
=⇒
-19
(f2)
=⇒
2
(f1)
=⇒
-3
(f2)
=⇒
5
(f1)
=⇒
5
(f2)
=⇒
3
(f1)
=⇒
#F
Inversement, on peut désirer une fonction qui soit capable de recevoir des éléments les uns après les autres,
en les conservant jusqu’à ce qu’on ait besoin de ces valeurs. Ce fonctionnement était le thème de l’exercice
producteur-distributeur , proposé au § 4.6.
Essayons d’aller plus loin. Voici une nouvelle version de make-list-searcher. L’unique différence avec la
précédente est que nous avons remplacé notre élément neutre de la fin par une fonction cont. Cette fonction
sans paramètres est appelée lorsqu’il n’y a plus dans la liste d’élément satisfaisant notre critère de recherche.
(define (make-list-searcher liste pred? cont)
(define (aux)
(cond
((null? liste) (cont))
((pred? (car liste))
(let ((val (car liste)))
(set! liste (cdr liste))
val))
(else
(set! liste (cdr liste))
(aux))))
aux)
Pour utiliser cette fonction, il faut maintenant lui fournir comme second paramètre une procédure, qui elle-même
fournira l’élément neutre résultat ; par exemple :
(make-list-searcher l even? (lambda () ’()))
Cependant, si l’on considère ce qui se passe réellement, on voit que cette fonction intervient pour poursuivre
le calcul effectif. On peut dire que le résultat de (make-list-searcher l pred fun) est une fonction qui
filtre l selon le prédicat pred, puis, quand la liste est épuisée, exécute la fonction fun. Cette dernière n’a pas
nécessairement comme but de fournir un élément neutre. Elle peut, par exemple, réaliser une autre opération
de recherche, comme le montre cette expression :
(define f3 (make-list-searcher l odd?
(make-list-searcher l even? (lambda () ’()))))
(f3)
=⇒
5
(f3)
=⇒
3
(f3)
=⇒
7
(f3)
=⇒
-19
(f3)
=⇒
-3
(f3)
=⇒
5
(f3)
=⇒
2
(f3)
=⇒
-6
(f3)
=⇒
0
(f3)
=⇒
-2
(f3)
=⇒
-2
(f3)
=⇒
8
(f3)
=⇒
#f
(f3)
=⇒
#f
Ici, la poursuite de la recherche des éléments impairs de l est la recherche des éléments pairs de cette même
liste, puis une procédure fournissant #f.
84
CHAPITRE 6. STRUCTURES DE DONNÉES EN SCHEME
Cette technique de programmation est dite continuation passing style ou CPS . Elle revêt, pour diverses
raisons, une importance fondamentale en Scheme et dans les langages applicatifs. Nous ne développerons pas
plus avant le sujet dans ce chapitre, mais nous reviendrons dans ce cours sur cette technique au chapitre 9.
6.2
Les arbres
Un arbre est un type de données qui modélise une structure hiérarchique. De telles structures sont extrêmement fréquentes en informatique. On peut considérer par exemple que l’arbre est le modèle sous-jacent de la
structure de donnée essentielle de Lisp, la paire-pointée.
6.2.1
Terminologie
6.2.1.1
Définition
Un arbre est un cas particulier de graphe, et l’on peut utiliser à son propos la même terminologie. Un graphe
est un ensemble de noeuds et d’arcs, un arc liant deux noeuds. Les listes, par exemple, sont des cas particuliers
de graphes. Les arbres sont, de même, des graphes particuliers. Un arbre a les propriétés suivantes :
– Tout arc lie deux noeuds distincts.
– Les arcs sont orientés : l’un des noeuds est l’origine, l’autre l’extrémité. Le noeud situé à l’origine est dit
père du noeud situé à l’extrémité ; ce dernier est dit fils du noeud origine.
– Il existe un noeud particulier, qui n’est le fils d’aucun autre, et qui est dit racine de l’arbre.
– A tout noeud de l’arbre, on peut associer un chemin unique qui va de la racine à ce noeud (en suivant les
arcs dans le sens père-fils).
Un noeud peut avoir zéro, un, ou plusieurs fils. Tous les noeuds, sauf la racine, ont un père unique.
Par analogie avec une construction fort commune dans la nature, les noeuds qui n’ont pas de fils sont dits
feuilles.
6.2.1.2
Une définition récursive
Un ensemble de noeuds et d’arcs constitue un arbre si :
– L’ensemble contient un noeud unique, et aucun arc. Ce noeud constitue alors la racine de l’arbre.
– L’ensemble contient un noeud particulier, R, qui est le père de noeuds n1 , n2 . . . , np . Chaque ni est la
racine d’un arbre Ti , tous les Ti étant indépendants (aucun noeud x d’un Ti n’appartient à un Tj ), R
n’appartient à aucun des Ti . R constitue la racine de l’arbre résultat, T .
6.2.1.3
Ancêtres et descendants
Nous avons vu qu’un chemin liait chaque noeud de l’arbre avec la racine. Un chemin est donc une succession
de noeuds, n1 , n2 . . . , np , tels que n1 est le père de n2 , n2 est le père de n3 , etc., et np−1 le père de np . n1 est
alors dit ancêtre de np , et np descendant de n1 . La longueur du chemin est p − 1, c’est à dire le nombre d’arcs
parcourus pour aller de l’ancêtre à son descendant.
La hauteur d’un noeud est le plus long chemin que l’on puisse parcourir depuis ce noeud jusqu’à l’une des
feuilles. La hauteur d’un arbre est celle de la racine ; c’est donc le plus long chemin que l’on puisse parcourir
dans un arbre.
Le niveau d’un noeud est la longueur du chemin liant la racine à ce noeud. La racine est donc de niveau
zéro.
Les fils d’un même noeud sont dits frères. On définit parfois une relation d’ordre sur ces fils, ce qui permet
de parler de premier fils, de fils ou frère suivant, etc.
6.2.1.4
Arbre binaire
Un arbre binaire est un arbre dont les noeuds peuvent avoir au maximum deux fils. Ces fils sont dits fils
gauche et fils droit .
85
6.2. LES ARBRES
6.2.2
Représentation des arbres
La méthode la plus naturelle de représentation des arbres en Scheme consiste à associer à chaque noeud la
liste des fils :
(-valeur . -fils1 . -fils2 . . . . -filsn .)
On peut introduire des fonctions particulières pour la création et l’utilisation d’arbres. Si nous adoptons la
représentation ci-dessus, en imaginant que l’on associe une valeur à tout noeud d’un arbre, la fonction suivante
va nous servir de constructeur :
(define (faire-noeud valeur . fils) (cons valeur fils))
Cette fonction va nous permettre de masquer la représentation de l’arbre2 . Voici un exemple de son utilisation :
(define T
(faire-noeud 2.3
(faire-noeud 1.17
(faire-noeud
(faire-noeud
(faire-noeud
(faire-noeud 9.21
(faire-noeud
(faire-noeud
6.4)
2.4)
1.2))
7.33)
6.21
(faire-noeud 4.32)))))
Il nous faut maintenant quelques prédicats et procédures opérant sur des arbres ainsi construits.
(define (noeud? x) (and (pair? x) (list? (cdr x))))
(define (feuille? x) (and (pair? x) (null? (cdr x))))
(define (fils x)
(if (noeud? x) (cdr x) ’()))
(define (valeur x)
(cond
((noeud? x) (car x))
(else #f)))
Cependant d’autres représentations sont possibles pour des arbres, en utilisant soit des listes, soit des vecteurs. Nous verrons au chapitre suivant quelques exemples de représentations fonctionnelles de structures.
6.2.3
Exemples de parcours d’arbres
Les arbres vont représenter une information structurée, hiérarchique. Le parcours d’arbre va consister à
analyser cette structure, pour en extraire des informations selon divers critères.
L’algorithme de recherche de la profondeur d’un arbre peut s’écrire, avec les opérations ci-dessus :
1 ]=> (define (profondeur x)
(if (feuille? x) 0
(+ 1 (apply max (map profondeur (fils x))))))
1 ]=> (profondeur T)
;Value: 3
Nous allons naturellement nous intéresser à d’autres types de parcours : en préordre (c’est à dire en examinant
le père avant les fils), en ordre terminal (c’est à dire en examinant les fils avant le père), etc. Que veut-on faire
dans l’arbre? Par exemple, énumérer les feuilles, faire une liste de l’ensemble des éléments, etc.
Voici par exemple deux fonctions réalisant l’exploration d’un arbre, l’une en préordre, l’autre en ordre
terminal . La première va donc fournir, dans sa liste de résultat, la valeur de chaque père avant celle de ses fils.
La racine se trouve donc en tête du résultat. La seconde va fournir les fils avant les pères, la racine se retrouvant
donc en queue de liste :
2 Nous n’utilisons plus cons, car, cdr pour le manipuler, mais des fonctions “abstraites”, qui nous permettent de réfléchir à
l’objet, non en termes de sa représentation physique (des listes), mais en fonction de sa signification pour notre algorithme (des
noeuds, des feuilles, etc.).
86
CHAPITRE 6. STRUCTURES DE DONNÉES EN SCHEME
(define (preordre A)
(cond
((feuille? A) (list (valeur A)))
((noeud? A) (cons (valeur A)
(apply append (map preordre (fils A)))))))
(define (postordre A)
(cond
((feuille? A) (list (valeur A)))
((noeud? A)
(append (apply append (map postordre (fils A)))
(list (valeur A))))))
Voici un exemple d’utilisation de ces fonctions pour le parcours d’un arbre :
1 ]=> (define E
(faire-noeud ’+
(faire-noeud ’* (faire-noeud 3)
(faire-noeud 7) (faire-noeud 6))
(faire-noeud ’/ (faire-noeud 12)
(faire-noeud ’- (faire-noeud 2)))))
1 ]=> e
;Value: (+ (* (3) (7) (6)) (/ (12) (- (2))))
1 ]=> (preordre e)
;Value: (+ * 3 7 6 / 12 - 2)
1 ]=> (postordre e)
;Value: (3 7 6 * 12 2 - / +)
Les résultats obtenus correpondent ici à des linéarisations (ou énumérations) de l’arbre, fournissant la liste
de ses composants. On peut vouloir effectuer des filtrages, des recherches, etc.
6.2.3.1
Une vision fonctionnelle
Cependant, la programmation en Scheme va s’intéresser à trouver des algorithmes mettant à constribution
les caractéristiques particulières du langage.
Exploration partielle
Considérons le problème suivant. Nous disposons d’une structure arborescente relativement complexe, T ,
que nous souhaitons explorer au moyen d’un prédicat pred?. Nous nous estimerons satisfaits si l’un des éléments
de l’arbre satisfait le prédicat. Enfin, et surtout, nous voulons interrompre notre exploration de l’arbre dès que
le résultat est obtenu (nous pouvons parler d’exploration partielle de l’arbre). Ce dernier critère n’est pas rempli
par les algoritmes classiques, qui explorent systématiquement, à chaque noeud, l’ensemble des fils.
L’idée générale de l’algorithme est la suivante : Nous allons réaliser une exploration de la structure T , et,
en cas d’échec, exécuter une procédure d’échec. Si notre structure est un arbre, elle comporte un car et un cdr .
Nous voulons éviter d’explorer les cdr si le car nous donne satisfaction ; dans le cas contraire, il faut explorer
aussi le cdr , et, en cas d’échec, exécuter la procédure d’échec. Il suffit de redéfinir notre cas d’échec, pour que,
si l’exploration du car aboutit à l’échec, la procédure d’échec réalise une exploration du cdr , puis seulement
aboutisse à un échec définitif. C’est ce qui est fait dans la procédure suivante, dans laquelle l’exploration
d’une paire consiste à explorer le car de cette paire, une lambda expression définissant une nouvelle procédure
aboutissant, en cas d’échec dans l’exploration du car , à l’exploration du cdr .
(define (one-of T pred?)
(define (explore S fail)
(if (pair? S)
(explore (car S)
(lambda () (explore (cdr S) fail)))
(if (and (number? S) (pred? S))
S
(fail))))
(explore T (lambda () ’())))
6.3. EXERCICES D’APPLICATION
87
Voici une structure d’arbre que nous allons explorer :
(define Tree ’(1 2 3 -9 3 (5 6 -2 8 -7 3) (9 (11 -3 12)
7 11 (13 (15 (17 ((19 20) -3 -7)
-11)))) 6 56 -17 ((((((((3))))) 7 -8)))))
=⇒
TREE
Voici quelques utilisation de notre procédure :
1 ]=> (one-of
;Value: -9
1 ]=> (one-of
;Value: 1
1 ]=> (one-of
;Value: ()
1 ]=> (one-of
;Value: 56
Tree negative?)
Tree odd?)
Tree zero?)
Tree (lambda (u) (= u 56)))
Notons que seul le troisième appel aboutit à une exploration complète de l’arbre.
Cet exemple est abordé à nouveau, au chapitre 9.2.2.2, pour permettre une exploration avec reprise, fournissant un nouveau résultat à chaque appel.
6.3
Exercices d’application
Opérations sur listes
Programmer les opérations suivantes :
– Former toutes les sous-listes possibles d’une liste donnée.
– Former toutes les sous-séquences possibles d’une liste donnée.
– Reconnaitre si une liste est une sous-liste d’une autre liste.
– Reconnaitre si une liste est une sous-séquence d’une autre liste.
Schéma de programmes
Voici deux programmes Scheme, l’un réalisant un produit cartésien de plusieurs listes, l’autre une permutation d’élements.
(define (list:cartesian-product . lists)
(if (null? lists)
(list lists)
(apply append
(map (lambda (from-first)
(map (lambda (from-xrest)
(cons from-first from-xrest))
(apply list:cartesian-product (cdr lists))))
(car lists)))))
(define (list:permutations . elements)
(if (null? elements)
(list elements)
(apply append
(map (lambda (from-first)
(map (lambda (from-xrest)
(cons from-first from-xrest))
(apply list:permutations (remove from-first elements))))
elements))))
88
CHAPITRE 6. STRUCTURES DE DONNÉES EN SCHEME
Ces fonctions font appel à la suppression d’élément dans une liste, remove, dont voici une écriture :
(define (remove x liste)
(cond ((null? liste) ’())
((equal? x (car liste)) (remove x (cdr liste)))
(else (cons (car liste) (remove x (cdr liste))))))
Les similitudes entre ces deux écritures vous suggèrent-elles une forme plus générale pour ces deux fonctions?
Voyez-vous d’autres applications de ce schéma? (Problème proposé par Eric Raible sur Internet.)
Opérations sur séquences
Il serait possible de réaliser des opérations génériques sur séquences, c’est à dire qui se formulent de la même
manière, quelle que soit la représentation effective de la séquence (liste, vecteur ou chaı̂ne). On pourrait ainsi
écrire :
(define (ref seq i)
(cond
((list? seq)
(list-ref seq i))
((vector? seq) (vector-ref seq i))
((string? seq) (string-ref seq i))))
Quelles autres opérations génériques peut-on réaliser de la sorte?
Gestion de matrices
Cet exercice se propose de faire réaliser des opérations gérant des matrices de taille n par p. Les éléments de
la matrice seront représentés linéarisés selon les lignes (c’est à dire tous les éléments de la première ligne, suivis
de ceux de la seconde ligne, etc), et précédés du nombre de colonnes de la matrice. Ainsi, la matrice de deux
lignes et cinq colonnes suivante :
138
5.31
21.3
640
132
36.5
0.07
725
64
258
sera représentée par le vecteur :
#(5 138 21.3 132 0.07 64 5.31 640 36.5 725 258)
Ecrire les procédures suivantes :
– Application d’une opération arithmétique (terme à terme) entre deux matrices.
– Application d’une opération arithmétique entre une matrice et une constante.
– Transposée d’une matrice : la transposée d’une matrice M de taille n par p est une matrice Q de taille p
par q, telle que Qij = Mji .
– Produit externe de deux vecteurs, fournissant une matrice. Le produit externe prend deux vecteurs V et
W , de taille p et q respectivement, ainsi qu’une fonction f , et fournit une matrice M , de taille p par q,
telle que Mij = f (Vi , Wj ).
– Produit interne. Le produit interne s’applique à une matrice P de dimensions n par p et une matrice Q
de dimensions p par q, et fournit une matrice R de dimensions n par q. Le produit interne prend aussi
comme paramètres deux fonctions f et g. Le résultat de :
(inner-product P + * Q)
est le produit matriciel classique de P par Q. Dans le cas général,
(inner-product P f g Q)
l’élément Rij du résultat est obtenu par réduction (cf. fonction reduce du TP 5), par la fonction f , du
résultat de
(map g V W)
expression dans laquelle V est la ligne i de P , et W est la colonne j de Q.
6.4. LA NOTION DE TYPE
6.4
La notion de Type
6.4.1
Introduction
89
Les types représentent une étape significative de l’évolution de l’informatique. Historiquement, la machine,
ou plus exactement, le langage de la machine, ignore la notion de type : c’est l’opération utilisée qui détermine
ce qui va arriver à la donnée. La même séquence de 32 bits en mémoire peut représenter un entier, un flottant,
quatre caractères, une instruction de la machine ou une adresse en mémoire centrale... Une simple erreur dans
une instruction ou une adresse va donc entrainer des résultats imprévisibles dans les programmes.
Le langage Fortran (1954) a introduit la déclaration de type, permettant de vérifier la cohérence des diverses
utilisations d’une même variable. Encore ces types étaient-ils ceux de la machine : entiers ou flottants. Il a fallu
attendre le langage ALGOL (1958, 1960) pour voir apparaı̂tre une notion de type quelque peu indépendante
de la machine, avec ses integer, real et boolean. Les successeurs, COBOL, PL/1, PASCAL, ont étendu cette
notion de types aux tableaux , enregistrements, pointeurs.
La notion de type peut se présenter sous deux formes assez distinctes dans un langage de programmation.
Le type peut être lié aux variables : une variable est “déclarée” d’un type particulier, et le langage n’autorise les
affectations de nouvelles valeurs à cette variable que si ces valeurs sont du type déclaré pour cette variable. On
parle habituellement de typage fort pour décrire ces langages. C’est le cas de la plupart des langages compilés,
tels Ada, C, Fortran, Pascal et bien d’autres.
Au contraire, d’autres langages n’imposent pas de déclarations, et les variables peuvent représenter des
données de types arbitraires. Ce qui est typé, dans ces langages, ce sont les valeurs elles-mêmes, et non pas les
noms utilisés pour les désigner. Les langages interprétés se retrouvent souvent dans cette catégorie : APL, Lisp,
Smalltalk, etc. Nous parlerons de typage latent ou parfois typage faible. Scheme, nous l’avons vu, se range dans
cette dernière catégorie.
6.4.2
Types en Scheme
Nous avons rencontré un certain nombre de types de données gérés par le système Scheme : booléens, nombres,
symboles, paires, caractères, chaı̂nes, vecteurs, procédures. Il en existe quelques autres : ports d’entrée sortie,
tables hashées, environnements, paires faibles, promesses, etc. Ces types, connus a priori par le système, sont
dits types natifs ou types primitifs
6.4.2.1
Caractéristiques des types natifs
Le système a une connaissance intime des types primitifs. Aux objets sont associées des étiquettes qui
permettent de contrôler les opérations qui leur sont appliquées. L’un des aspects des types de données natifs
est donc la protection qu’ils offrent : seules les procédures primitives sont susceptibles de s’appliquer à leurs
représentants (les procédures définies devant faire appel, pour les manipuler, aux procédures primitives).
Ces types natifs représentent des outils de base pour le programmeur. Celui-ci va devoir exprimer son
problème, et en particulier les données qui interviennent dans ce problème, en fonction des types disponibles
sur la machine (ou dans le langage).
Certains de types primitifs sont déjà d’assez bonnes approximations des entités que souhaite utiliser le
programmeur. Le type numérique de Scheme (c’est à dire l’ensemble représenté par les entiers, les rationnels,
les réels et les complexes) constitue, avec les procédures qui s’y attachent, un outil relativement élaboré (meilleur
en particulier que celui qui est offert par nombre d’autres langages, y compris les langages dits “scientifiques”
tels Fortran).
Cependant, un langage ne peut pas tout prévoir, et le programmeur se trouve journellement confronté à la
nécessité de représenter de nouveaux concepts au moyen des types existants. Devant modéliser des situations
réelles, il découvre rapidement l’inadéquation entre les types mis à sa disposition dans le langage, et les objets
du monde réel qu’il souhaite manipuler. Il va donc devoir construire, à partir des objets natifs (et naı̈fs) du
langage des abstractions plus élaborées, ainsi que les opérations qui leur sont relatives.
6.4.3
Utilisation des types
Nous avons montré par différents exemples, tout au long de ce cours, qu’il était en effet possible de construire,
à partir de ces objets élémentaires, des structures plus complexes, mieux adaptées aux problèmes à traiter. Ainsi,
la paire pointée nous permet la création de listes, et les listes, munies d’opérations spécifiques, permettent de
gérer des types comme la pile, la file, l’arbre, etc.
Voici par exemple une implantation d’une structure de queue (une queue est une file d’attente : tout nouvel
élément est introduit en queue de file, tout retrait d’élément s’opère en tête ; la file travaille en FIFO, first-in,
first-out) :
90
CHAPITRE 6. STRUCTURES DE DONNÉES EN SCHEME
(define (make-queue) (cons ’() ’()))
(define (enqueue! q obj)
(set-car! q (cons obj (car q))))
(define (dequeue! q)
(if (null? (cdr q))
(if (null? (car q))
#f
(begin (set-cdr! q (reverse (car q)))
(set-car! q ’()))))
(let ((head (car (cdr q))))
(set-cdr! q (cdr (cdr q)))
head))
Cette implantation gère en fait deux listes : le car de la structure créée par make-queue (une simple paire
pointée) est la liste des entrées : tout nouvel élément est introduit dans cette liste ; le cdr est la liste des sorties :
tout élément retiré de la queue est extrait de cette liste. Si, lorsque l’on doit retirer un élément de la file, la liste
de sortie est vide, les éléments de la file d’entrée sont transférés dans la liste de sortie, en ordre inverse grâce à
reverse, et la file d’entrée est remise à vide.3
6.4.4
Types définis
Certains langages de programmations permettent la création de nouveaux types de données, qui sont dès
lors reconnus par le système au même titre que les types prédéfinis. Pourquoi désirer la création de nouveaux
types de données, quand il est possible de représenter et de gérer tout nouvelle structure de données au moyen
des éléments (types et fonctions) prédéfinis dans le système?
Un type peut être, dans un langage de programmation, une simple convention adoptée par le programmeur :
il peut décider de représenter les mois de l’année par des nombres compris entre 0 et 11. Le mois suivant est
ainsi calculé par :
(define (mois-suivant m) (modulo (+ m 1) 12))
C’est ce qui a été fait pendant des années dans des langages aussi divers que Fortran, Lisp ou APL. Cependant,
cette approche conduit à des programmes fragiles : si le programmeur ajoute (par erreur) 10 à la donnée qui,
dans l’exemple ci-dessus, représente le mois de Septembre, il obtient un nombre qui n’a aucun sens comme
désignation de mois. Le système est bien incapable de détecter cette erreur, car pour lui le programmeur s’est
simplement livré à une opération légale entre nombres entiers.
Il est donc clair qu’il existe un manque sémantique dans ces langages. On peut vouloir distinguer aisément
un nouveau type d’un type déja existant. On peut vouloir s’assurer que, par exemple, enqueue! et dequeue!
opèrent bien sur des objets créés par make-queue, et non sur des listes à la structure quelconque. On peut vouloir
interdire l’utilisation de fonctions autres que enqueue! et dequeue! sur des objets créés par make-queue. Enfin,
on peut vouloir désigner, par des noms identiques (par exemple, parce que la sémantique de l’opération est la
même), des fonctions s’appliquant sur des données de types différents.
Ce genre de considération est pris en compte dans les langages à objets. Certaines implantations de Scheme
proposent des extensions à objets.
Nous ne décrirons pas dans ce chapitre les concepts des langages à objets. Le lecteur curieux trouvera dans
[MNC+ 89] une excellente synthèse du sujet, et un bref tour d’horizon dans [Gir93].
Nous allons cependant mettre en oeuvre des techniques similaires, faisant appel aux traits spécifiques de
Scheme, pour réaliser des implantations fiables de nos divers types de données.
3 Remarque en passant : quel est l’ordre de grandeur de cet algorithme? Contrairement aux apparences, il opère statistiquement
en temps constant pour chaque élément, quelle que soit la taille de la file. Si l’on choisit un exemple dans lequel n éléments sont
insérés, puis retirés de la file, on voit que :
– L’insertion de chaque élément nécessite un temps constant. L’insertion de n éléments est donc en O(n).
– Le retrait du premier élément provoque le transfert, depuis la liste d’entrée vers la liste de sortie, de l’ensemble des éléments.
Ce transfert est réalisé par la fonction primitive reverse, qui opère en O(n).
– Le retrait des autres éléments nécessite un temps constant pour chaque élément. Le retrait de l’ensemble des n éléments est
également en O(n).
On peut vérifier que toute combinaison d’opérations conduisant à l’insertion et au retrait de n éléments peut se ramener au cas
analysé ci-dessus. L’ensemble de nos opérations est donc en O(n) pour n éléments, ce qui justifie notre affirmation relative au
fonctionnement en temps constant pour chaque élément.
6.5. CRÉATION DE TYPES EN SCHEME
6.4.5
91
Protection des types
Prenons comme seul exemple la notion de liste en Scheme. Cette donnée est la base de nombre d’algorithmes ;
on peut la considérer quasiment comme un type primitif. Notons cependant qu’une liste n’est pas un type
primitif. Une liste est l’union de deux types primitifs, la paire pointée et la liste vide.
Nous avons défini une liste comme étant soit la liste vide, soit une paire pointée dont le cdr désigne une
liste. Cette définition par récurrence permet de définir aisément le prédicat caractéristique des listes :
(define (list? l)
(or (null? l)
(and (pair? l) (list? (cdr l)))))
Nombre de fonctions du langage s’appliquent aux listes, fournissant de nouvelles listes : append, reverse,
etc. Cependant, d’autres opérations sont susceptibles de détruire ces caractéristiques, transformant une liste en
un objet qui n’est plus une liste : c’est le cas de set-cdr! ou append!. La structure de liste est donc en fait
fragile, dans la mesure où certaines opérations sont susceptibles d’altérer un objet de ce type, en lui faisant
perdre les propriétés caractéristiques de ce type.
Il est clair que cette fragilité sera aussi le lot des types que nous serons susceptibles de définir à partir des
types primitifs et des données prédéfinies du langage.
6.4.5.1
Types concrets
Nous pourions par exemple réaliser une implantation de nombres rationnels en MIT-Scheme (s’il n’en existait
déja une !). Un nombre rationnel peut ainsi être représenté par un vecteur de trois éléments, le symbole Q
qui permet de repérer un rationnel, ainsi que deux entiers représentant respectivement le numérateur et le
dénominateur du nombre. Le rationnel 2/3 se note ainsi #(Q 2 3). Cependant, tous les vecteurs obéissant à
cette description ne représentent pas nécessairement un rationnel valide : le nombre représentant le dénominateur
doit être strictement positif, les deux nombres doivent être premiers entre eux, etc. Alors même que toutes les
opérations sont soigneusement programmées pour fournir et manipuler des rationnels valides, une faute de
frappe, ou l’application d’une fonction incorrecte sont susceptibles de générer des données qui ne respectent
plus ces propriétés.
Le problème vient donc du fait que les données qui nous servent à modéliser notre nouveau type sont
exposées, visibles de tous, et en particulier accessibles par des procédures autres que celles qui ont été conçues
pour les manipuler en toute sécurité. Nous dirons que ces types de données sont concrets.
6.4.5.2
Types abstraits
Il faut donc, pour éviter ces problèmes, offrir un mécanisme de protection de la représentation des données.
On parle alors d’encapsulation, et de type abstrait .
Un type abstrait fait référence à des valeurs dont la représentation physique n’est pas directement accessible
par le programmeur, et qui ne sont manipulables que par l’intermédiaire de fonctions d’accès spécialisées. Ainsi,
le contenu d’un fichier, d’une base de donnée peuvent être assimilés à des types abstraits car ils ne sont accessibles
que par l’intermédiaire d’un protocole spécialisé.
L’utilisation de types abstraits assure une protection des données par encapsulation. Le programmeur du
type abstrait peut, sans aucune modification des spécifications du type, modifier la représentation physique des
données, ajouter des contrôles d’accès, etc.
Nous allons voir de quelle manière ce paradigme peut être introduit dans le langage scheme.
6.5
Création de types en Scheme
On a vu la fragilité des représentations de nouveaux types au moyen des objets préexistants : quelle que
soit la convention choisie (liste, structures, ou même, pourquoi pas, grand nombre), l’utilisation délibérée ou
accidentelle d’opérations primitives s’appliquant à ces types est une menace envers l’intégrité des données ainsi
représentées.
6.5.1
Implantation par fermetures
En fait, le seul type de données qui ne puisse, en Scheme, être modifié une fois créé est la procédure. Il
se trouve justement que la procédure, grâce à la notion de fermeture, va nous offrir l’encapsulation dont nous
avons besoin pour réaliser des types de données abtraits.
92
CHAPITRE 6. STRUCTURES DE DONNÉES EN SCHEME
Fig. 6.3 - Une implantation de paires pointées par fermetures
(define (x-cons a d)
(lambda (selecteur . pars)
(cond
((eq? selecteur ’pair?) #t)
((eq? selecteur ’car) a)
((eq? selecteur ’cdr) d)
((eq? selecteur ’set-car!) (set! a (car pars)))
((eq? selecteur ’set-cdr!) (set! d (car pars)))
((eq? selecteur ’type) ’cons)
(else #f))))
(define
(define
(define
(define
(define
(x-pair? w) (w ’pair?))
(x-car w) (w ’car))
(x-cdr w) (w ’cdr))
(x-set-car! w v) (w ’set-car! v))
(x-set-cdr! w v) (w ’set-cdr! v))
La figure 6.3 nous propose un exemple de réalisation d’une structure bien connue, la paire pointée. Voici un
exemple d’utilisation :
1 ]=> (define c1 (x-cons 3 5))
;Value: c1
1 ]=> (x-car c1)
;Value: 3
1 ]=> (x-cdr c1)
;Value: 5
1 ]=> (x-set-car! c1 "hello")
1 ]=> (x-car c1)
;Value: "hello"
Il s’agit là d’une approche on ne peut plus traditionnelle ! De très nombreuses extensions à objets de Scheme
ont été réalisées ainsi. On se reportera par exemple au chapitre correspondant (10.1) qui décrit plus en détail le
procédé. Nous nous contenterons ici de donner quelques exemples, qui nous permettront également de parfaire
notre connaissance des structures de pile et de file.
6.5.2
Exemples
Voici quelques implantations “industrielles” (c’est à dire, utilisables) de structures de données, réalisées par
des fermetures.
L’une des méthodes les plus “naturelles”, du moins pour le schemeur convaincu, est l’utilisation de fermetures. La figure 6.4 montre l’utilisation de cette technique pour l’implantation de piles.
Analysons ce programme. La procédure principale, make-stack crée, lors de son exécution, un environnement
dans lequel vont être liées les variables stack, empty?, top, push!, pop! et dispatch. stack reçoit comme valeur
la liste vide, les autres variables reçoivent les valeurs fonctionnelles de procédures liées dans l’environnement. Le
résultat de make-stack est la seule procédure dispatch. Après exécution de make-stack, dispatch est l’unique
procédure à garder des références sur les autres objets qui viennent d’être créés.
Notons, à l’intérieur de la fonction dispatch, l’utilisation d’une forme spéciale particulière, le case. Sa
syntaxe est la suivante :
(case -clef . -clause1 . -clause2 . . . . -clausen .)
Chaque clause est elle-même de la forme :
(-liste. -exp1 . -exp2 . . . . -expp .)
6.5. CRÉATION DE TYPES EN SCHEME
Fig. 6.4 - Une implantation de pile par fermetures
;; A stack implementation using lists.
;; Copyright Juha Heinanen 1988
;; This code may be freely distributed.
;;
(define (make-stack)
(define stack ’())
(define (empty?) (null? stack))
(define (top)
(if (null? stack)
(error "Stack is empty -- TOP"
stack)
(car stack)))
(define (push! object)
(set! stack (cons object stack))
object)
(define (pop!)
(if (null? stack)
(error "Stack underflow -- POP!"
stack)
(let ((object (car stack)))
(set! stack (cdr stack))
object)))
(define (dispatch op . args)
(case op
((empty?) (apply empty? args))
((top) (apply top args))
((push!) (apply push! args))
((pop!) (apply pop! args))
(else
(error "Unknown stack operation --"
" DISPATCH" op))))
dispatch)
93
94
CHAPITRE 6. STRUCTURES DE DONNÉES EN SCHEME
ou encore (“clause else”) :
(else -exp1 . -exp2 . . . . -expp .)
Le fonctionnement de case est le suivant :
– L’expression -clef ., qui peut être quelconque, est évaluée, fournissant une valeur -v ..
– Les -clausei . sont consultées, dans l’ordre, et la valeur -v . est recherchée dans la -liste. de la clause, au
moyen de la fonction memv (le prédicat de comparaison utilisé est donc eqv?).
– Si la valeur -v . est trouvée dans la -liste., les expressions -exp1 . -exp2 . . . . -expp . sont exécutées en
séquence, et le résultat de la dernière est fourni comme résultat de la forme case.
– Si la valeur -v . n’est trouvée dans aucune des -listes., et si une clause else apparaı̂t dans l’instruction
cond, les -expi . de la clause else sont exécutées en séquence, et le résultat de la dernière des -expi . est
fourni comme résultat de la forme case.
– Si la valeur -v . n’est trouvée dans aucune des -listes., et si il n’y a pas de clause else, le résultat de
l’instruction case n’est pas spécifié.
La figure 6.5 montre une autre utilisation de cette technique pour l’implantation de files.
6.6
Analyse de l’approche
Nous avons utilisé les fermetures disponibles dans le langage pour réaliser l’implantation de types de données
abstraits. Ces types de données offrent une grande souplesse à l’implémenteur du type, qui peut à tout moment
en modifier la représentation interne, et une sécurité certaine au programmeur, qui sait que seules les opérations
“officielles” pourront être appliquées sur les représentants du type (c’est à lui, programmeur, de les appliquer à
bon escient).
La représentation choisie (des fermetures, donc des fonctions) est la plus opaque possible. Cependant, la
fonction est un type primitif. Il n’est pas possible de distinguer une fonction “ordinaire” d’une fermeture représentant une donnée ainsi construite. Même si, par convention, on décide de faire en sorte que tous les types
construits acceptent au moins une requête en commun, par exemple la requête ’type, la confusion avec une fonction “ordinaire” est toujours possible, et la plupart de celles-ci, appliquées à ’type, déclencheront effectivement
une erreur.
En outre, le programmeur “futé” peut décider de se passer de x-car, et d’appliquer directement la fonction
représentant la donnée au symbole car, sous le prétexte que cet appel direct est “plus efficace” que l’utilisation
d’une fonction intermédiaire. Mais ce faisant, il transgresse la règle, en manipulant directement les données, au
lieu de le faire par les fonctions prévues par le concepteur du type : si le concepteur modifie l’implantation du
type, en gardant une interface cohérente pour x-car, les programmeurs qui utilisent x-car sont gagnants, ils
n’ont aucune modification à apporter à leurs programmes ; notre programmeur “futé” devra réécrire les siens...
6.6.1
Sécurisation
On peut améliorer légèrement la sécurité du système, par une meilleure encapsulation des différentes fonctions. Reprenons le plus simple des exemples, la construction de paires pointées. La figure 6.6 nous propose une
variante de la technique utilisée plus haut (fig. 6.3).
La méthode employée ici consiste tout d’abord à créer, par des define, les liaisons correspondant aux
différents noms de fonctions que l’on va définir : ici x-cons, x-pair?, x-car, etc. Une forme let permet ensuite
de créer un environnement local, dans lequel on définit cinq valeurs uniques, m-pair?, m-car, etc., qui vont,
comme dans notre première version, servir à sélectionner l’une des opérations gérées par le type. La différence est
que ces valeurs, qui vont servir de messages, ne sont connues que des seules fonctions créées dans l’environnement
du let. Elles sont inaccessibles de l’extérieur, et non reproductibles, puisque ce sont des paires pointées, créées
par la fonction list, et eq? et différentes de toute autre donnée... Aucune autre fonction que x-pair?, x-car,
etc, ne peut donc provoquer de réaction (c’est à dire, lire ou écrire) de la part d’une donnée créée par x-cons.
Enfin, il est possible de “blinder” ces fonctions pour qu’elles vérifient que leur paramètre est bien une fonction
ou une fermeture (par exemple, en utilisant procedure?).
Voici un exemple de session, réalisé avec ces opérations :
1 ]=> (define toto (x-cons 2 3))
;Value: toto
1 ]=> (x-pair? toto)
6.6. ANALYSE DE L’APPROCHE
Fig. 6.5 - Une implantation de file par fermetures
;; A queue implementation using lists.
;; Copyright Juha Heinanen 1988
;; This code may be freely distributed.
(define (make-queue)
; header node abstraction
(define node-front car)
(define node-rear cdr)
(define node-set-front! set-car!)
(define node-set-rear! set-cdr!)
; memory representation
(define queue (cons ’() ’()))
(define queue-length 0)
(define (length) queue-length)
(define (front)
(if (= queue-length 0)
(error "Queue is empty -- FRONT" queue)
(car (node-front queue))))
(define (insert! object)
(let ((new-pair (cons object ’())))
(if (= queue-length 0)
(begin
(node-set-front! queue new-pair)
(node-set-rear! queue new-pair))
(begin
(set-cdr! (node-rear queue) new-pair)
(node-set-rear! queue new-pair))))
(set! queue-length (+ queue-length 1))
object)
(define (remove!)
(if (= queue-length 0)
(error "Queue underflow -- REMOVE!" queue)
(let ((object (front)))
(node-set-front! queue (cdr (node-front queue)))
(set! queue-length (- queue-length 1))
object)))
(define (dispatch op . args)
(case op
((length) (apply length args))
((front) (apply front args))
((insert!) (apply insert! args))
((remove!) (apply remove! args))
(else (error "Unknown queue operation -- DISPATCH" op))))
dispatch)
95
96
CHAPITRE 6. STRUCTURES DE DONNÉES EN SCHEME
(define
(define
(define
(define
(define
(define
Fig. 6.6 - Une implantation “sécurisée” des paires pointées
x-cons #f)
x-pair? #f)
x-car #f)
x-cdr #f)
x-set-car! #f)
x-set-cdr! #f)
(let ()
(define
(define
(define
(define
(define
m-pair?
m-car
m-cdr
m-set-car!
m-set-cdr!
(list
(list
(list
(list
(list
’m-pair?))
’m-car))
’m-cdr))
’m-set-car!))
’m-set-cdr!))
(set! x-cons
(lambda (a d)
(lambda (selecteur . pars)
(cond
((eq? selecteur m-pair?) #t)
((eq? selecteur m-car) a)
((eq? selecteur m-cdr) d)
((eq? selecteur m-set-car!) (set! a (car pars)))
((eq? selecteur m-set-cdr!) (set! d (car pars)))
(else #f)))))
(set! x-pair?
(lambda (w) (w m-pair?)))
(set! x-car
(lambda (w) (w m-car)))
(set! x-cdr
(lambda (w) (w m-cdr)))
(set! x-set-car! (lambda (w v) (w m-set-car! v)))
(set! x-set-cdr! (lambda (w v) (w m-set-cdr! v)))
’OK)
97
6.6. ANALYSE DE L’APPROCHE
;Value: #t
1 ]=> (x-car toto)
;Value: 2
1 ]=> (x-cdr toto)
;Value: 3
1 ]=> (x-set-car! toto "Hello")
1 ]=> (x-car toto)
;Value: "Hello"
6.6.2
Critique de la solution
Cependant, même avec cette solution, le passage d’une fonction “ordinaire” ne représentant pas un objet
construit avec cette technique provoquera une erreur.
D’autre part, chaque nouvelle création d’un objet nécessite la construction d’une structure pour représenter
les valeurs propres à l’objet (ce que désignent les variables a et d), ainsi que la fonction de sélection elle-même.
L’encombrement de la mémoire qui en résulte est important, d’autant plus important d’ailleurs que la fonction
est complexe et reconnait plus d’opérations...
6.6.3
Environnements
Une solution très voisine consiste à utiliser des environnements pour obtenir l’encapsulation que nous recherchons. Nous avons déja abordé cette notion d’environnement. au chapitre 4.3.1. Tout au long de ce cours, nous
avons utilisé le terme de contexte pour désigner l’environnement courant. Les environnements sont en fait des
objets de première classe dans le langage. Ils peuvent donc être affectés à des variables, passés en paramètres à
des fonctions, ou transmis comme résultats à une fonction.
Nous avons vu que certaines constructions du langage définissaient de nouveaux environnements : lambda,
let, let*, etc. Lorsqu’une construction telle que :
(let ((a 3) (b 5))
... )
est exécutée dans un environnement -E1 ., elle crée un nouvel environnement -E2 ., dans le contexte duquel
s’évalue le corps de la forme let. Ce contexte contient les nouvelles liaisons a! 3 et b! 5, mais les liaisons
visibles de l’environnement -E1 . sont encore accessibles. Cet environnement -E1 . est dit environnement parent
de -E2 ..
Dans le cas de MIT-Scheme, deux environnements sont définis au lancement du système : un environnement
système initial, qui contient l’ensemble des liaisons correspondant aux objets prédéfinis du langage, désigné
par la variable system-global-environment, et un environnement utilisateur, user-initial-environment,
initialement vide, qui sera enrichi au cours de la session de objets définis par l’utilisateur. Cet environnement a
pour parent l’environnement système.
MIT-Scheme propose diverses fonctions de manipulation des environnements :
– (environment? -E .) : ce prédicat permet de reconnaı̂tre un environnement.
– (the-environment): cette fonction fournit l’environnement courant. Elle permet en particulier de capturer des environnements lexicaux créés par les formes let, lambda, etc :
(define env (let ((a 3) (b 5)) (the-environment)))
– (environment-parent -E .) et (environment-bindings -E .) prennent comme paramètre un environnement -E ., et fournissent respectivement l’environnement parent de -E . et la liste des liaisons de l’environnement, sous la forme d’une liste associative (cf. § 4.2.2). Pour continuer l’exemple ci-dessus :
=⇒
(environment-bindings env)
((a . 3) (b . 5))
– access est une forme spéciale, qui permet la manipulation des éléments d’un environnement :
– (access -var . -env .) lit la valeur de la variable -var . dans l’environnement -env . ;
– (set! (access -var . -env .) -val .) affecte la valeur -val . à la variable -var . de l’environnement
-env ..
Attention, -var . est un symbole, non une expression. Ex :
(acces b env)
=⇒
5
98
CHAPITRE 6. STRUCTURES DE DONNÉES EN SCHEME
– environment-bound? permet de savoir si un symbole est défini dans un environnement donné. Ex :
(environment-bound? env ’b)
=⇒
#t
Contrairement à access, environment-bound? n’est pas une forme spéciale, mais une procédure.
– eval enfin, est une fonction qui permet d’évaluer une expression dans un environnement donné :
– (eval -exp. -env .) évalue -exp. dans l’environnement -env . ;
– (eval -exp.) évalue -exp. dans l’environnement courant.
Notons bien le fonctionnement de eval sur l’exemple suivant :
(eval (list ’+ 1 (cadr ’(a b))) env)
=⇒
6
eval n’est pas le nom d’une forme spéciale ; les trois éléments de la liste ci-dessus vont donc être évalués
selon la procédure standard :
– eval fournit la valeur eval , fonction primitive ;
– (list ’+ 1 (cadr ’(a b))) construit la liste (+ 1 b) ;
– env enfin a pour valeur l’environnement env construit ci-dessus.
Dans une seconde phase, eval va être appliquée sur ses paramètres, une liste et un environnement. L’algorithme d’eval consiste à évaluer son premier paramètre dans l’environnement défini par le second. La
liste (+ 1 b) est donc évaluée de manière standard : + désigne dans env la fonction d’addition (héritée de
l’environnement initial), et b vaut 5 dans env . Le résultat final est donc la valeur 6 . Il y a donc eu double
évaluation du premier paramètre : une première fois dans l’environnement courant, pour construire une
expression ; une seconde fois dans l’environnement précisé, pour évaluer cette expression.
Nous pouvons enfin proposer une nouvelle version de la définition du type paire pointée (cf. fig. 6.7) qui a de
meilleures propriétés : une paire pointée est maintenant un environnement, qui peut être reconnu à coup sûr (cf.
prédicat x-pair?). Même si l’on transmet un environnement quelconque, ou créé avec une procédure similaire,
la valeur de la variable *-type-* est discriminante.
L’objet est représenté de manière relativement efficace, puisque l’environnement ne contient que les deux
liaisons correspondant aux variables a et d, et la référence à l’environnement parent, partagé par tous les objets
du même type.
Seul demeure le problème de l’accès malveillant à cet environnement au moyen des fonctions s’appliquant
aux environnements. Nous touchons là à l’une des limites de l’approche, due au fait qu’un objet est toujours
représenté par l’une des structures primitives du langage. Pour obtenir une réelle encapsulation des données, il
faut procéder à une extension du langage vers la construction de nouveaux types, qui ne puissent être manipulés
que par les opérations définies par l’utilisateur. C’est le rôle des extensions à objets, qui sont abordées au
chapitre 10.1. Ces extensions sont nombreuses en Scheme ou en Lisp : voir par exemple [Coi83], [Coi88], [Kee89],
[Gre91], [BK88], [DG87], etc.
6.7
Exercices d’application
Nombres Aléatoires
L’informatique fait une grosse consommation de nombres “aléatoires”. Les fonctions suivantes, bien qu’elles
ne fournissent pas des nombres réellement aléatoires, permettent de créer des suites de nombres satisfaisant les
tests de distribution classiques :
;; Random numbers using linear congruential method.
;; Copyright Juha Heinanen 1988
;; This code may be freely distributed.
;;
(define (rand-update x)
(define a 25173)
(define b 13849)
(define m 65536)
(modulo (+ (* a x) b) m))
(define rand
(let ((x 1))
6.7. EXERCICES D’APPLICATION
Fig. 6.7 - Une implantation des paires pointées par des environnements
(define
(define
(define
(define
(define
(define
(define
*-type-* "Environnement Utilisateur")
x-cons #f)
x-pair? #f)
x-car #f)
x-cdr #f)
x-set-car! #f)
x-set-cdr! #f)
(let ((*-type-* (list ’paires)))
(set! x-cons
(lambda (a d)
(the-environment)))
(set! x-pair?
(lambda (w) (and (environment? w)
(eq? (access *-type-* w) *-type-*))))
(set! x-car
(lambda (w) (if (and (environment? w)
(eq? (access *-type-* w) *-type-*))
(access a w)
<erreur>)))
(set! x-cdr
(lambda (w) (if (and (environment? w)
(eq? (access *-type-* w) *-type-*))
(access d w)
<erreur>)))
(set! x-set-car!
(lambda (w v) (if (and (environment? w)
(eq? (access *-type-* w) *-type-*))
(set! (access a w) v)
<erreur>)))
(set! x-set-cdr!
(lambda (w v) (if (and (environment? w)
(eq? (access *-type-* w) *-type-*))
(set! (access d w) v)
<erreur>)))
’OK)
99
100
CHAPITRE 6. STRUCTURES DE DONNÉES EN SCHEME
(lambda ()
(set! x (rand-update x))
x)))
(define (random n)
(modulo (rand) n))
En vous inspirant du code de ces fonctions, ainsi que des exemples des figures 6.4 et 6.5, écrire le gestionnaire
d’une structure de données réalisant de nombres aléatoires.
Piles et Files
Ecrivez de nouvelles versions des opérations sur piles et files (cf. fig. 6.4 et 6.5) en utilisant des environnements
à la place de fermetures.
Gestion de Compte Banquaire
Utilisez la technique des fermetures pour définir une (ou plusieurs) structures de données permettant la
représentation de comptes banquaires. Quelles sont les opérations à prévoir? Envisager les opérations de dépôt
ou de retrait à un guichet automatique. Comment s’assurer de la confidentialité des données? Peut-on envisager
de protéger les transactions par un code secret? Comment faire pour que le banquier puisse, spontanément ou
sur une demande du client, modifier le code confidentiel de celui-ci (cas de l’oubli, de la perte)?
Chapitre 7
Macros
Ce chapitre aborde un aspect particulier du langage Scheme, que l’on ne retrouve guère dans les langages
fonctionnels, celui des macros. On y expose en particulier les avantages et les inconvénients de ces constructions
particulières, à la lumière, en particulier, des récentes propositions apparues dans la norme.
7.1
Retour sur les formes Spéciales
Nous avons déjà rencontré un certain nombre de formes spéciales, dont le traitement des opérandes est
différent de celui des fonctions : certaines n’évaluent pas du tout leurs opérandes (comme quote), d’autres
le font suivant des algorithmes spécifiques (if, and, or, etc). Scheme est un langage extensible, qui offre à
l’utilisateur la possibilité de créer de nouvelles formes spéciales, afin de permettre une plus grande souplesse
dans l’écriture d’applications. De telles formes spéciales sont dites macro-définitions, ou simplement, macros.
Une utilisation classique, en Scheme comme en Lisp, des macros est ainsi de permettre la réalisation d’extensions syntaxiques du langage, pour rendre celui-ci plus agréable et plus adapté aux tâches envisagées.
Quel est le rôle des formes spéciales dans un langage fonctionnel ? Une première justification peut être la
possibilité d’obtenir l’équivalent de l’appel par nom. Cette technique, à condition de nous limiter à un petit
nombre d’utilisations judicieusement choisies, nous permettra de mettre en place diverses extensions propres
aux langages fonctionnels, comme l’évaluation paresseuse, la gestion de flots, etc.
7.1.1
Puissance et Complexité
Les macros sont, traditionnellement, sources de puissance et de complexité dans les langages de programmation. L’une des raisons est qu’elles constituent une abstraction textuelle par dessus la syntaxe : au niveau le plus
bas, les macros peuvent enfreindre les règles syntaxiques ou sémantiques du langage, leur utilisation devenant
alors source d’erreurs complexes et difficiles à comprendre. En voici quelques exemples, en C :
#define TMIN 10
#define START TMIN-1
#define TMAX 100;
...
int i, k = TMAX - 1;
for (i= 2*START; i <
...
/* niveau minimum */
/* pour debut de boucle * /
/* indice max dans TABSYM */
k; i+=2)
Les conflits potentiels entre noms locaux et identificateurs utilisés dans la macro sont à craindre :
#define swap(a,b) {int temp; temp=a; a=b; b=temp;}
...
if (i>j)
swap(i,j); /* On les echange */
else
k++; /* Ordre ok - incremente compteur */
...
swap(val, temp);
Indépendamment de leur complexité inhérente (car raisonner en termes d’opérations construisant des programmes est très différent de raisonner en termes de programmes), les macros s’avèrent être un outil bien délicat
101
102
CHAPITRE 7. MACROS
à manier du fait du non respect, par le macro-processeur, des règles du langage relatives à la syntaxe ou à la
portée des noms.1
7.1.2
Syntaxe et Sémantique des Macros
Il existe plusieurs mécanismes permettant la définition de formes spéciales dans le langage Scheme. L’un
d’entre eux, quasi-normalisé (c.f. [R4R90], § 4.2.6), n’est malheureusement pas disponible dans notre version du
système. Nous présenterons donc d’abord quelques uns de outils de bas niveau utiles à la définition de formes
spéciales, les formes define-macro, quasiquote, unquote et unquote-splicing.
7.1.2.1
La forme define-macro
La syntaxe de déclaration d’une macro est très proche de celle d’une forme define.
(define-macro (-name. . -par-liste.) -corps.)
La macro ainsi définie sera désignée sous le nom -name.. Elle acceptera éventuellement une liste de paramètres, désignée par -par-liste.. Son algorithme d’exécution sera déterminé par -corps.. Ainsi, les déclarations
suivantes:
(define-macro (toto a b c) -....)
(define-macro (titi) -....)
(define-macro (tutu . w) -....)
définissent trois macros, de nom toto, titi et tutu, toto attendant trois paramètres, titi aucun, et tutu un
nombre indéterminé de paramètres, qui seront rassemblés en une liste unique, w.
7.1.2.2
Sémantique des macros
Que se passe-t-il lorsque l’on écrit :
(toto 2 (* 3 5) truc)
Les paramètres de la macro ne sont PAS évalués, mais sont transmis sous forme symbolique à la macro, et
liés aux arguments de celle-ci. Ainsi, a reçoit comme valeur le nombre 2, b la liste (* 3 5), et c le symbole truc.
Le corps de la macro, qui est une expression Scheme quelconque, va s’exécuter, réalisant un certain traitement
sur les variables a, b et c, et, enfin, fournir un résultat. C’est ce résultat qui est alors considéré comme une
expression de substitution pour l’appel initial de la macro, et qui va être utilisé à la place de celle-ci.
Considérons un premier exemple : on souhaı̂te définir une forme spéciale when 2 qui permette l’exécution
d’une suite d’instructions, si une condition est réalisée. Dit plus simplement, when est un if sans partie sinon.
Une expression telle que :
(when -test . -e1 . -e2 . ... -en .)
va être transformée en :
(if -test . (begin -e1 . -e2 . ... -en .) #f)
L’écriture du code ne pose pas de problème particulier :
1 ]=> (define-macro (when expr . corps)
(list ’if expr
(cons ’begin corps)
’#f)))
;Value: when
1 ]=> (when #t (display ’hello) (+ 2 3))
hello
;Value: 5
1 ]=> (when #f (display ’hello) (+ 2 3))
;Value: ()
Notre langage dispose maintenant d’une nouvelle structure de contrôle, qui nous permet (en théorie !) une
écriture plus lisible des programmes.
1A
propos, combien d’erreurs le compilateur C va-t’il détecter dans ces deux exemples? Combien y en a-t’il réellement?
2 Une telle forme existe dans plusieurs systèmes Lisp, tels Common-Lisp, Le Lisp, mais n’est pas disponible de manière standard
en Scheme.
103
7.2. UTILISATIONS DES MACROS
7.1.2.3
Macros et interprétation
A quel moment interviennent les macros lors de l’exécution d’un programme Scheme ou Lisp ? La réponse
dépend des systèmes. Dans le cas de Scheme, la norme du langage précise que les macros sont prises en compte
à la définition des fonctions. Les macros doivent donc être définies avant toute utilisation.
Lors de la définition d’un programme, chaque fois que le système reconnaı̂t l’appel d’une macro, celleci est immédiatement exécutée, et c’est son résultat qui apparaı̂t dans le corps de la fonction. Le travail de
transformation peut donc être relativement complexe, il est effectué, pour chaque utilisation d’une macro, une
fois et une seule.
7.1.2.4
Appel par nom
Considérons la fonction ainsi définie :
f un x 0
f un x val
=1
= xval
Si nous la définissons sous la forme classique :
(define (fun x val)
(if (= val 0) 1
(expt x val)))
les deux paramètres de la fonction seront évalués à chaque appel. Or, nous aimerions exploiter notre connaissance
du comportement de la fonction, et faire en sorte qu’un appel tel que (fun (p) 0) nous rende immédiatement
1, même si l’exécution de la fonction p ne se termine pas ! Il nous faut pour cela renoncer à l’appel par valeur,
et passer à l’appel par nom, ce qui peut se faire en écrivant notre fonction sous la forme d’une macro-fonction.
Une telle définition va s’écrire :
(define-macro (fun x val)
(list ’if
(list ’= val ’0)
’1
(list ’expt x val)))
Notre macro construit donc la forme :
(if (= -val . 0) 1 (expt -x . -val .))
dans laquelle -x . et -val . représentent les deux paramètres de la macro.
1 ]=> (fun (/ 3 0) 0)
;Value: 1
1 ]=> (fun (/ 3 2) 1)
;Value: 3/2
La contrepartie, c’est que notre objet n’est plus une fonction. fun ne peut pas être appliqué par apply, passé
en paramètre à des fonctions, etc.
1 ]=> (fun 3 4)
;Value: 81
1 ]=> (apply fun ’(3 4))
syntactic keyword referenced as variable fun
2 Error->
Il serait possible également d’optimiser la forme au niveau même de la macro, par exemple, en détectant que le
second paramètre est la constante 0, et en fournissant comme résultat de la macro la constante 1.
7.2
Utilisations des macros
les usages classiques des macros consistent à renommer des fonctions primitives pour faciliter la lecture des
programmes, à fournir de nouvelles structures de contôle, etc. Un exemple d’une telle utilisation est donnée
dans le dernier exercice du chapitre.
104
CHAPITRE 7. MACROS
7.2.1
Quelques exemples classiques
Les macros sont traditionnellement utilisées pour fournir des traits impératifs, correspondant à certaines
constructions des langages traditionnels. Par exemple, l’opérateur ++ du langage C permet d’incrémenter une
variable en fournissant sa valeur. Ecrivons une macro réalisant cette opération, telle que, si la valeur de la variable
toto est 3, le résultat de (++ toto) soit 4, la variable toto ayant également cette valeur après l’opération.
On peut décider de transformer (++ toto) en :
(begin (set! toto (+ toto 1)) toto)
c’est à dire une séquence comportant une affectation, puis une référence à la variable. L’une des raisons du choix
d’une instruction de séquencement est que le résultat de la forme set! est, officiellement, indéterminé.3 Notre
macro s’écrit ainsi :
(define-macro (++ var)
(list ’begin
(list ’set var
var)))
7.2.2
(list ’+ var ’1))
La tradition
En programmation Lisp impérative, certaines macros sont pratiquement un must . Programmez celles-ci.
7.2.2.1
prog1 et prog2
La forme prog1 obéit à la syntaxe suivante :
(prog1 -e1 . -e2 . ... -en .)
Les expressions -e1 ., -e2 .... -en . sont exécutées en séquences, puis la forme fournit comme résultat le resultat
de la première. Exemple typique :
(prog1 (car l) (set! l (cdr l)))
La forme prog2 est similaires à prog1, mais rend la valeur de la seconde forme. Exemple :
(prog2 (open "toto") -opération sur toto. (close "toto"))
Dans cette forme, c’est bien sûr le résultat de -opération sur toto. qui nous intéresse...
7.2.2.2
push et pop
Il est simple d’utiliser des listes pour simuler des piles. Nous utiliserons des variables pour désigner ces
“piles”. La forme (push -pile. -valeur .) permet d’ajouter une nouvelle valeur au sommet de la pile. La forme
(pop -pile.) permet de retirer un élément de la pile. Ex :
(define pile ’())
(push pile (* 2 3))
(push pile (cadr ex))
(push pile (+ 2 (pop pile)))
Comment définiriez vous la forme top -pile.), qui fournit la valeur du sommet de la pile sans modifier celle-ci.
7.2.3
Remarque
L’expérience montre la puissance expressive des macros de Lisp (ou Scheme). L’une des raisons (ce n’est
pas la seule) tient au fait que les macros sont programmées dans le même langage que celui qui est utilisé pour
réaliser les programmes. Il y a de multiples avantages à ceci. Ce langage, Lisp, est puissant, et particulièrement
bien adapté au traitement symbolique — il a été conçu pour ça. Ce langage connaı̂t la structure du langage
cible, Lisp, et dispose de toutes les primitives nécessaires pour le manipuler. Ce langage, enfin, est celui-là même
qu’utilise le programmeur pour écrire ses applications. Il n’y pas effort d’apprentissage d’un nouvel outil, mais
au contraı̂re, perfectionnement : programmer des macros permet d’une part d’assimiler toutes les subtilités du
langage, mais aussi de mieux cerner ses propres besoins, ceux de son application.
3 Un
système peut choisir de rendre le nom de la variable affectée, d’autres la nouvelle ou l’ancienne valeur, etc.
7.3. QUASI QUOTE ET SES COMPÈRES
105
Cependant, l’expérience acquise grâce aux macros de Lisp n’est pas nécessairement perdue lorsque l’on passe
à d’autre langage. Quand vous constatez—pour prendre un exemple au hasard—la grande difficulté qu’il y a à
obtenir des paramétrages très subtils au moyen du préprocesseur C,4 pourquoi ne pas envisager l’écriture d’un
programme C spécialisé, qui va lui-même calculer les informations dont vous avez besoin, et générer un fichier
.h que vous pourrez inclure dans votre application ? Voici une idée qui ne vous viendra pas nécessairement à
l’esprit si vous n’avez pas quelque peu pratiqué les macros de Lisp...
7.3
Quasi Quote et ses compères
Comme on l’a compris, le travail d’une macro est essentiellement de construire de nouvelles formes Scheme
pour exécution ultérieure. Ces formes correspondent le plus souvent à des schémas prédéfinis, dans lesquels un
petit nombre d’éléments vont dépendre des paramètres.
7.3.1
Présentation
La forme quasiquote, et ses accompagnatrices unquote et unquote-splicing constituent un outil permettant ce genre d’écriture sans trop de difficultés. La première de ces opérations, la forme quasiquote, prend
comme paramètre un modèle de la structure à construire, et fournit un résultat identique au modèle initial, sauf en ce qui concerne les sous-arbres contenant des formes qui débutent par le mot-clef unquote ou
unquote-splicing. Ces deux marqueurs indiquent une expression à évaluer, qui sera insérée telle quelle, ou
concaténée à l’expression courante. Ainsi, si x a pour valeur 17, et y pour valeur (a b c d), peut-on obtenir :
1 ]=> (quasiquote (1 2 x y 5 z))
;Value: (1 2 x y 5 z)
1 ]=> (quasiquote (1 2 (unquote x) y 5 z))
;Value: (1 2 17 y 5 z)
1 ]=> (quasiquote (1 2 x (unquote y) 5 z))
;Value: (1 2 x (a b c d) 5 z)
1 ]=> (quasiquote (1 2 x (unquote-splicing y) 5 z))
;Value: (1 2 x a b c d 5 z)
1 ]=> (quasiquote (1 2 (unquote (+ 2 (* 3 x))) y 5 z))
;Value: (1 2 53 y 5 z)
1 ]=> (quasiquote (1 2 x (unquote (length y)) 5 z))
;Value: (1 2 x 4 5 z)
Les caractères ‘, , et la séquence ,@ sont reconnus par le système comme constructeurs des formes quasiquote,
unquote et unquote-splicing :
‘-exp.
,-exp.
,@-exp.
⇔
⇔
⇔
(quasiquote -exp.)
(unquote -exp.)
(unquote-splicing -exp.)
Les expressions ci-dessus peuvent donc également s’écrire :
1 ]=> ‘(1 2 x y 5 z)
;Value: (1 2 x y 5 z)
1 ]=> ‘(1 2 ,x y 5 z))
;Value: (1 2 17 y 5 z)
1 ]=> ‘(1 2 x ,y 5 z)
;Value: (1 2 x (a b c d) 5 z)
1 ]=> ‘(1 2 x ,@y 5 z)
;Value: (1 2 x a b c d 5 z)
1 ]=> ‘(1 2 ,(+ 2 (* 3 x)) y 5 z)
;Value: (1 2 53 y 5 z)
1 ]=> ‘(1 2 x ,(length y) 5 z)
;Value: (1 2 x 4 5 z)
4 Un exemple typique consiste à vouloir écrire des programmes C portables et très performants qui se basent sur des informations
du genre sizeof(int), sizeof(short), etc.
106
CHAPITRE 7. MACROS
7.3.2
Sémantique
Il est possible de modéliser le fonctionnement de la forme quasiquote au moyen d’un ensemble de règles de
substitutions. Notons ≺exp5 la transformation de -exp. ainsi définie :
-exp.
forme obtenue
f orm =⇒ (list ‘f orm)
, f orm =⇒ (list f orm)
, @f orm =⇒ f orm
La forme quasiquote tente de reconnaı̂tre les schémas suivants, et leur applique les transformations correspondantes :
‘-item. équivaut à (quote -item.), pour n’importe quelle forme -item. qui n’est pas une liste.5
‘,-item. équivaut à -item., pour n’importe quelle forme -item..
‘,@-item. est une erreur.
‘(-e1 . -e2 . -e3 . ... -en . . -atome.) dans laquelle les -ei . sont des expressions quelconques, et -atome. un
objet qui n’est pas une liste non vide, équivaut à l’expression :
(append ≺e1 5 ≺e2 5 ... ≺en 5 (quote -atome.))
‘(-e1 . -e2 . -e3 . ... -en .) s’écrit aussi ‘(-e1 . -e2 . -e3 . ... -en . . ()), et se ramène donc au cas précédent.
‘(-e1 . -e2 . -e3 . ... -en . . ,-forme.) équivaut à :
(append ≺e1 5 ≺e2 5 ... ≺en 5 -forme.)
‘(-e1 . -e2 . -e3 . ... -en . . ,@-forme.) est une erreur.
La manipulation de la back-quote est un véritable sport en soi. Plusieurs dizaines de pages lui sont dédiées
dans [Ste90], et il n’est pas inutile de consacrer quelques heures à en explorer les possibilités. En particulier,
étudiez les interactions des formes quasiquote, unquote et unquote-splicing lorsqu’elles sont imbriquées, par
exemple quand des macros génèrent des programmes qui doivent eux-même générer des programmes. Notons
que si plusieurs formes quasiquote sont ainsi imbriquées, les substitutions doivent s’effectuer d’abord sur la
plus interne. Attention alors aux formes unquote et unquote-splicing: dans une écriture telle que ,,exp,
équivalente à (unquote (unquote exp)), c’est le unquote externe qui correspond à la quasiquote interne.
Considérons par exemple l’environnement suivant :
(define p ’(q r))
(define (q x) (apply * x))
(define r ’(2 3 5 6))
Quel est le résultat de l’expression :
‘‘(,,p)
En appliquant les règles ci-dessus, nous découvrons que la forme quasiquote génère l’expression :6
(append (list (quote append))
(list (append (list (quote list)) (list p))))
L’évaluation de cette expression fournit :
(append (list (q r)))
5 Un
vecteur reçoit également un traitement particulier, similaire à celui des listes.
6 Cette expression relativement complexe découle de l’application disciplinée des règles énoncées ci-dessus. Tous les systèmes
Scheme ne produisent pas une telle forme. Ils peuvent construire des formes différentes, mais dont l’exécution fournira une valeur
identique. Ils peuvent en particulier comporter des algorithmes de simplification, leur permettant de générer des expressions plus
simples que celle-ci, mais fournissant un résultat identique, telle :
(list ’list p)
Le problème des règles de transformations est qu’elles risquent de ne pas marcher complètement dans tous les cas. Il est (malheureusement trop souvent) facile d’exhiber des exemples mettant en lumière les bugs des implantations de la forme quasiquote - y
compris dans MIT Scheme !
107
7.4. EXERCICES
qui est ce qui va s’imprimer en réponse à ‘‘(,,p).
Une nouvelle exécution de cette expression conduirait à :
(180)
On peut ainsi retenir que ,,p signifie : la valeur de p est une forme ; utiliser la valeur de cette forme.
Comment caractériseriez-vous ,@,p ou ,’,p?
7.4
Exercices
7.4.1
Quelques macros
Débutons par quelques macros simples, pour s’échauffer.
7.4.1.1
while
Une forme répétitive habituelle des systèmes Lisp est le while, dont la syntaxe est :
(while -condition. -suite d’instructions.)
Programmez votre version de la chose (en sachant que le résultat de la forme while nous indiffère totalement).
7.4.2
Compréhension
7.4.2.1
Entrainement
Trouvez des valeurs pour les variables p, q, r et s donnant un sens à l’expression suivante :
(define bar
‘‘(list
‘‘(list
‘‘(list
‘‘(list
‘‘(list
‘‘(list
‘‘(list
‘‘(list
(list
,,p)
,,@q)
,’,r)
,’,@s)
,@,p)
,@,@q)
,@’,r)
,@’,@s)))
Pratiquez le même exercice avec :
(define froboz (list
‘‘‘(list ,,,p)
‘‘‘(list ,,,@q)
‘‘‘(list ,,’,r)
‘‘‘(list ,,’,@s)
‘‘‘(list ,,@,p)
...
‘‘‘(list ,@’,@’,@s)))
(cette dernière expression comportant 32 combinaisons possibles... Exercice proposé par Guy L. Steele Jr.
[Ste90])
7.4.2.2
Forme quasiquote
Ecrivez votre propre version de la forme quasiquote. Faites un premier modèle, suivant les règles exposées
ci-dessus. Tentez ensuite d’introduire quelques optimisations afin de simplifier les formes proposées. Attention,
ce qui semble logique avec unquote a souvent bien des chances de se planter avec unquote-splicing !
7.4.2.3
Macro-expand
Connaissant le texte des macros intervenant dans une expression Scheme, construire le texte correspondant
à cette expression une fois que les macros auront été expansées. Un tel programme, on s’en doute, est d’une
grande utilité pour comprendre et mettre au point des macros...
108
CHAPITRE 7. MACROS
7.5
Une question d’hygiène
7.5.1
Captures
L’utilisation de macros de bas niveau peut entrainer divers désagréments. L’un des moindres n’est pas le
phénomène de capture. Celui-ci intervient lorsque l’expansion de la macro utilise des symboles qui sont redéfinis
dans l’environnement dans lequel opère la macro. Considérons l’exemple suivant, où une macro, consq, permet
de créer une paire pointée à partir de constantes :
1 ]=> (define-macro (consq u v) ‘(cons ’,u ’,v))
;Value: consq
1 ]=> (consq 2 3)
;Value: (2 . 3)
1 ]=> (consq hello world)
;Value: (hello . world)
Cependant, la macro échoue dans l’environnement suivant :
1 ]=> (let ((cons list))
(consq toto titi))
;Value: (toto titi)
La forme consq génère une forme cons, alors que ce symbole, dans cet environnement, a été lié à la fonction
list. Le résultat est donc une liste, et non une paire pointée.
De même, la syntaxe de Scheme fait qu’un mot clef (primitif ou défini) ne peut pas être redéfini comme nom
de fonction, même au sein d’un nouvel environnement :
1 ]=> (let ((quote (lambda (x) (+ x 1))))
(quote 4))
SYNTAX: let: rebinding syntactic keyword quote
2 Error->
(A propos d’erreur, voyez-vous où est le problème dans cet exemple?
1 ]=> (define-macro (quote u v) ‘(cons ’,u ’,v))
;Value: quote
1 ]=> (quote hello)
SYNTAX: quote: incorrect number of subforms 1
2 Error-> (quote toto titi tutu)
SYNTAX: quote: incorrect number of subforms 3
3 Error-> (quote toto titi)
SYNTAX: quote: incorrect number of subforms 1
Notre macro, écrite avec deux paramètres, n’accepte pas les appels avec un seul ou trois, ce qui semble normal.
Mais qu’est-ce qui fait qu’elle ne fonctionne pas non plus avec deux?)
7.5.2
Macros hygiéniques
Bien que le système de macros décrit ci-dessus7 soit plus élaboré et plus puissant que ce qui existe dans la
plupart des autres langages de programmation, il n’en demeure pas moins que les problèmes de capture sont bien
réels. L’une de règles de Scheme est qu’une fonction opère dans l’environnement dans lequel elle a été définie,
et non dans celui où elle s’exécute. Il était souhaı̂table que la même règle s’applique aux macro-définitions.
Pour cette raison, le rapport (c.f. [R4R90], “APPENDIX: MACROS”) propose un système particulier de
macros, dit hygiénique, et référenciellement transparent . Dans ce système, notre exemple de consq devient :
1 ]=> (let ((cons list))
(consq toto titi))
;Value: (toto . titi)
Comment ce résultat est-il atteint ? Le mécanisme de macro-expansion va renommer les variables locales
afin d’éviter les captures de variables liées. L’expression ci-dessus est ainsi transformée en :
1 ]=> (let ((cons.1 list))
(consq toto titi))
;Value: (toto . titi)
7 On
trouve un mécanisme voisin dans tous les systèmes Scheme ou Lisp.
7.5. UNE QUESTION D’HYGIÈNE
109
Le système de macro hygiéniques proposé pour Scheme offre les extensions suivantes :
define-syntax Cette forme permet la définition de macros dans l’environnement courant. Sa syntaxe est :
(define-syntax -nom. -description.)
La description n’est pas elle-même une procédure, mais un ensemble de formes syntaxiques définies par
filtrage au moyen de la forme syntax-rules.
let-syntax Cette forme permet de déclarer des macros dans un environnement local. Sa syntaxe est identique
à celle des formes let :
(let-syntax
((-v1 . -e1 .)
(-v2 . -e2 .) ...
(-vn . -en .))
-exp1 .
-exp2 . ...
-expp .)
dans laquelle les symboles -v1 ., -v2 . ... -vn ., sont associés aux descriptions -e1 ., -e2 . ... -en .. Les expressions
-exp1 ., -exp2 . ... -expp . sont alors exécutées séquentiellement dans ce contexte, le résultat de la dernière
étant fourni comme résultat de la forme let-syntax.
letrec-syntax Cette forme est au letrec ce que le let-syntax est au let, et permet des définitions récursives,
un modèle syntaxique étant décrit en termes d’un autre modèle syntaxique.
syntax-rules Cette forme est utilisée dans define-syntax et let-syntax pour décrire de nouvelles formes
spéciales. Sa syntaxe est :
(syntax-rules -mots-clefs. -règles.)
L’élément -mots-clefs. est une liste, éventuellement vide, qui indique quels sont les mots-clefs qui vont
apparaı̂tre dans les -règles.. Les rêgles sont elles-mêmes des couples
(-forme. -expansion.)
Chaque -forme. précise une syntaxe possible d’utilisation de la macro, l’expression -expansion. fournissant
la forme à générer correspondante. Ces formes sont décrites par filtrage : tout identificateur qui apparait
dans -mots-clefs. sera reconnu textuellement ; tout autre symbole est une variable de filtrage, et sera
instancié avec l’expression correspondante de l’appel de la macro.
L’exemple suivant permet de clarifier ces règles. Il décrit une forme, set!, qui agit, sur un nombre limité de
cas, comme la forme setf de Common-Lisp. L’idée est de pouvoir écrire des affectations :
(set! -emplacement . -valeur .)
dans lesquelles -emplacement . ne désigne pas nécessairement une variable, mais peut faire référence à un emplacement de la mémoire, tel le car ou le cdr d’une paire pointée, un élément d’un vecteur, etc.
(let-syntax
((set! (syntax-rules (car cdr vector-ref)
((set! (car x) y)
(set-car! x y))
((set! (cdr x) y)
(set-cdr! x y))
((set! (vector-ref x i) y) (vector-set! x i y))
((set! x y)
(set! x y)))))
(let* ((jours (list ’lundi ’mercredi ’vendredi))
(jour ’dimanche))
(set! (car jours) ’mardi)
(set! jour (car jours))
jour))
110
CHAPITRE 7. MACROS
La liste (syntax-rules (car cdr vector-ref)... indique que car, cdr et vector-ref sont des mots-clefs, et
non des variables de filtrage. La forme let-syntax définit donc une forme spéciale locale, let! (les redéfinitions
locales de formes spéciales prédéfinies sont donc autorisées), avec quatre syntaxes possibles. La première, par
exemple, décrit le schéma :
(set! (car -x .) -y.)
les variables de filtrage étant mises en évidence vous la forme -x . et -y.. Lorsque cette forme est reconnue, le
modèle généré est :
(set-car! -x . -y.)
Remarquons la dernière règle :
((set! x y) (set! x y))
Il faut se souvenir que cette règle est définie dans une forme let-syntax, similaire à un let, et que, si la partie
gauche de la règle définit effectivement un modèle local, la partie droite fait référence à la signification globale
de la forme, donc au set! standard.
Le résultat de cette exécution est mardi. Les règles de visibilités adoptées permettent ainsi de corriger un
vieux défaut de Common-Lisp qui, dans ce cas précis, ne sait pas générer correctement la macro-expansion.
Il est intéressant de comparer les deux modèles présentés. Voici la forme let, implantée à la manière traditionnelle, avec define-macro, etc :
(define-macro (let defs . exprs)
‘((lambda ,(map car defs) ,@exprs)
,@(map cadr defs)))
et avec define-syntax :
(define-syntax let
(syntax-rules ()
((let (<v1> <e1>) ... ) <exp1> <exp2> ... )
((lambda (<v1> ...) <exp1> <exp2> ... ) <e1>)
)))
L’aspect déclaratif de cette dernière forme apparaı̂t clairement, comparé à l’aspect impératif de la première.
L’une décrit ce que l’on veut obtenir, l’autre ce qu’il faut faire pour celà.
Notons l’usage de l’élipse ... : la notation “-s. ...” représente de 0 à n occurences du schéma -s..
Voici encore un autre exemple, issu de [CR91], qui illustre le fonctionnement correct des macros dans un
contexte où des noms de macros masquent des variables locales, et des variables locales masquent des macros :
(let-syntax
(first (syntax-rules () ((first ?x) (car ?x))))
(second (syntax-rules () ((second ?x) (cdr ?x)))))
(let ((car "Packard"))
(let-syntax ((classic (syntax-rules ()
((classic) car))))
(let ((car "Matchbox"))
(let-syntax ((affordable (syntax-rules ()
((affordable) car))))
(let ((cars (list (classic) (affordable))))
(list (second cars) (first cars))))))))
Le résultat (correct) est ("Matchbox" "Packard").
7.5.3
Niveau bas
Le rapport [R4R90] introduit également des opérations de bas niveau, qui permettent certaines actions qui
ne peuvent être décrites par les fonctions de haut niveau. En particulier, des opérations comme syntax-rules
peuvent être elles-mêmes des macros, décrites en termes d’opérations de bas niveau.
7.5. UNE QUESTION D’HYGIÈNE
111
Ces opérations nécessitent une bonne connaissance des internes du système : environnement, gestion des
symboles, etc.
syntax Cette forme fournit un accès à la liaison (binding) visible de son paramètre, qui peut être un symbole ou
une forme quelconque.8 Dans ce dernier cas, le résultat est une forme contenant les liaisons correspondantes
aux symboles de la forme initiale. Nous noterons ,exp- le résultat de (syntax -exp.).
identifier? Ce prédicat rend vrai si son paramètre représente une liaison, c’est à dire le résultat d’une forme
syntax.
identifier->symbol Cette fonction fournit le symbole original attaché à une liaison.
free-identifier=? Ce prédicat teste si ses deux paramètres représentent la même liaison.
bound-identifier=? Ce prédicat teste si deux liaisons seraient considérées comme équivalentes si les identificateurs correspondants étaient liés par une lambda-expression. Deux liaisons peuvent être identiques au
sens de free-identifier, mais différentes au sens de bound-identifier.
unwrap-syntax Cette fonction fournit l’accès au premier niveau de la structure créée par syntax. Soit toto le
résultat de (syntax (a b c d)). La valeur de toto peut se noter ,(a b c d)-. Le résultat de la forme
(unwrap-syntax toto) peut se noter (,a- . ,(b c d)-), et est, en l’occurence, une paire pointée.
generate-identifier Cette fonction construit une nouvelle liaison. Le nom de l’identificateur de la liaison
peut être précisé explicitement. Tout appel de cette fonction construit un nouvel identificateur, qui sera
reconnu différent (par bound-identifier=?) de tout autre.9
construct-identifier La forme (construct-identifier -ident. -symbole.) construit une nouvelle liaison
de nom -symbole., liaison relative à l’environnement dans lequel la liaison -ident. est définie. Cette forme
est utilisée pour contourner l’hygiénisme dans certains cas...
Détaillons quelques exemples simples de [R4R90].
(let-syntax ((car (lambda (x) (syntax car))))
((car) ’(0)))
Cette forme définit un mot-clef local, car. Le transformateur associé est une fonction à un paramètre, qui rend
(syntax car). Le transformateur étant créé par let-syntax, il est évalué hors de la forme, et (syntax car)
fait donc référence à la liaison globale car, désignant la fonction du système qui fournit le car d’une paire
pointée, que nous noterons ,car -. La forme (car) déclenche l’exécution du transformateur, qui reçoit comme
paramètre x la liste (car) (qu’il n’utilise d’ailleurs pas), et rend la valeur fonctionnelle ,car -. La forme qui va
être compilée, et ultérieurement exécutée, est donc :
(,car - ’(0))
Le résultat est donc le car de la liste (0), c’est à dire 0.
7.5.3.1
Implantation
Une implantation correcte et efficace de ces concepts est assez délicate. Dans la théorie, il est possible
(comme en lambda calcul) de renommer les variables liées des formes, dans la mesure où l’on ne crée pas,
ce faisant, de captures. Un premier problème, lié à l’analyse de la forme externe d’un programme (une liste)
est de distinguer les identificateurs (qui représentent des variables dans les formes exécutables) des symboles,
qui sont des données symboliques, typiquement introduits par une forme quote. Le problème est que d’autres
formes peuvent introduire des symboles (case par exemple, ou des macros définies), alors que quote peut être
lui-même redéfini localement (ou pourquoi pas au niveau global)... Le même nom peut d’ailleurs représenter,
dans un fragment de texte, à la fois une variable et une donnée symbolique.
Une solution consiste à représenter les liaisons par des triplets, comportant le nom externe, une référence
à la liaison, et différents marqueurs qui vont évoluer au cours de l’analyse, et permettront de savoir dans quel
8 J’utilise ici le terme liaison, qui me semble plus significatif, alors que les documents américains utilisent identifier , un identifier
étant une variable liée, par opposition au symbol , qui est une constante symbolique. Notons que les macros hygiéniques reposent sur
la notion de renommage. Le nom utilisé dans un tel identificateur peut éventuellement être différent du nom choisi par l’utilisateur
dans son programme. identifier , dans les différents noms de fonctions, est donc à prendre au sens de liaison.
9 Cette forme est à rapprocher des (gensym) des systèmes Lisp traditionnels. Cependant, alors que (gensym) construit un
symbole nouveau (différent de tout autre symbole du système), (generate-identifier) construit une liaison, protégeant là encore
de phénomènes possibles de captures par des formes construites par programmes et évaluées par eval.
112
CHAPITRE 7. MACROS
contexte une liaison est créée, masquée ou référencée. Ce sont ces marqueurs qui interviennent au niveau de la
subtile différence entre free-identifier=? et bound-identifier=?. Cette technique permet de ne résoudre
qu’au dernier moment le cas des objets qui ressemblent à des liaisons, mais qui sont en fait des symboles. En
revanche, on comprend la nécessité de la forme syntax qui capture le contexte de la liaison au moment de son
exécution (similaire à lambda qui capture l’environnement lors de son exécution). La forme syntax est récursive,
alors que unwrap-syntax est superficielle. Ce choix reflète une volonté de permettre une implantation efficace
des macros, qui n’ont en général pas à analyser en profondeur les formes qui leur sont soumises, mais ne vont
s’intéresser le plus souvent qu’aux tout premiers niveaux. Pour des discussions plus significatives sur le sujet,
voir [R4R90], [Han91a], [CR91], [Cli91b] ou [Cli91a].
7.5.3.2
Note
Quelle est la différence fondamentale entre le système de macros classique, présenté en début de chapitre,
et le système de macros hygiénique ? Considérons les deux définitions suivantes :
(define-macro (one-quote x) (list (quote quote) x))
et
(define-syntax another-quote
(lambda (f) (list (syntax quote) (cadr f))))
La première définition est celle d’une macro opérant, pourrait-on dire, en mode textuel , ou lexical . Le résultat
de la forme (one-quote toto) est la liste (quote toto). Il va ensuite y avoir mise en place des liaisons, la
liste devenant alors la forme exécutable (,quote- toto). Cette mise en place des liaisons s’effectue en même
temps que l’analyse du reste du programme, c’est à dire dans le contexte de la forme compilée. Le contexte
de la définition de la macro n’intervient plus, d’où les risques de captures : dans cet exemple, si la macro est
expansée dans un contexte où quote a été redéfinie, c’est cette nouvelle définition qui va être prise en compte,
non celle qui était en vigueur au moment de la création de la macro one-quote.
Dans le cas des macros syntaxiques, ou hygiéniques, le travail consistant à fournir une forme exécutable,
avec ses liaisons résolues, est à la charge de la macro. Le transformateur associé à la macro another-quote
reçoit comme paramètre la liste (another-quote toto). Il va devoir construire la forme exécutable (quote ...),
et donc créer la liaison ,quote- par (syntax quote). Ce serait une erreur de définir ainsi notre macro :
(define-syntax another-quote
(lambda (f) (list (quote quote) (cadr f))))
La forme construite serait alors (quote toto), dont le premier élément est un symbole, et non pas une liaison,
forme qui n’est donc pas exécutable.
7.5.4
Exercices
7.5.4.1
Macros Standards
Utiliser les macros hygiéniques de haut niveau pour redéfinir les formes suivantes (se référer à [R4R90] pour
la description et la sémantique) : and, or, cond et case.
7.5.4.2
Structures
Il est possible d’implanter, en Scheme, des structures bon marché, mais performantes, au moyen de macros.
Une structure sera représentée par un arbre ; ses champs seront désignés par des noms. Une structure sera définie
par un symbole, nommant son type, et la description de l’agencement de ses champs. Voici un exemple de ce
que l’on souhaı̂te obtenir :
1 ]=> (defstruct cercle (rayon centre-x centre-y
couleur texture))
;Value: cercle
1 ]=> (make-cercle! toto ’(10 100 300 rouge standard))
;Value: toto
1 ]=> (cercle-rayon toto)
;Value: 10
1 ]=> (cercle-centre-y toto)
;Value: 300
1 ]=> (cercle-set-rayon! toto 12)
7.5. UNE QUESTION D’HYGIÈNE
113
;Value: 12
1 ]=> toto
;Value: ((12 . 100) 300 rouge . standard)
Notre macro defstruct a automatiquement construit les macros correspondant aux méthodes de création
(make-cercle!), d’accès (cercle-rayon, cercle-centre-x, etc), et de modification (cercle-set-rayon!, etc)
de la structure. L’impression de la valeur de toto révèle sa structure, qui est un arbre binaire équilibré, formé
de n − 1 paires pointées. Une telle représentation arborescente permet, par rapport à une liste, de minimiser le
temps d’accès aux divers éléments, qui est en O(log n) et non en O(n), comme ce serait le cas si la représentation
choisie était une liste. (On peut également choisir d’intégrer à la représentation de l’objet soit son type, sous
la forme d’un symbole, soit encore une fonction caractéristique, ce qui nous rapproche d’une vision objet de la
structure).
114
CHAPITRE 7. MACROS
Chapitre 8
Flots
Les flots constituent une structure de données permettant de réaliser des opérations sur des collections
de données. Les propriétés des flots sont identiques à celles des listes : l’accès aux premiers éléments d’un
flot est immédiat, l’accès à la suite nécessitant de traverser tout le flot. Le nombre d’éléments dans un flot
est potentiellement infini, les éléments de tête étant consommés alors que la queue du flot n’est pas encore
obligatoirement générée. Par exemple, une session de travail devant une console peut être modélisée par un flot
de caractères échangés entre le producteur humain et la machine consommatrice.1
8.1
Opérations sur flots
Nous ferons appel aux primitives suivantes :
(cons-stream -a. -d .) est la primitive de création d’un élément d’un flot. C’est une opération à deux paramètres.
(stream-car -flot .) fournit l’accès à la tête, c’est à dire le premier élément, d’un flot. Le résultat de cette
opération est une valeur quelconque.
(stream-cdr -flot .) fournit la queue du flot, c’est à dire le flot amputé de son premier élément.
(stream -e1 . -e2 . ... -en .) produit le flot composé des éléments -e1 . -e2 . ... -en .. En particulier, (stream)
fournit le flot vide.
(stream-pair? -objet .) indique si son paramètre est un flot (ou plus exactement, a les caractéristiques d’un
flot).
(stream-null? -flot .) indique si son paramètre est un flot vide.
En première approximation, nous pouvons assimiler le comportement de ces primitives à cons, car, cdr,
list pair? et null? respectivement, sur lesquelles, d’ailleurs, elles sont modélisées.
Nos premiers exemples sont ainsi programmables avec des listes. L’utilisation des flots n’est donc ici qu’une
méthodologie de conception et de programmation.2
8.1.1
Exemples
Un premier mode d’utilisation des flots consiste en une reduplication des opérations sur listes : Ainsi, la
fonction append opérant sur listes :
(define (append u1 u2)
(if (null? u1)
u2
1 Le mode d’utilisation des flots est similaire à celui des pipes du système Unix : un flot de données traverse différents processus
qui les filtrent, les modifient, les composent, les trient, etc.
2 Le langage Scheme utilise le terme de stream, ici flot, pour désigner une certaine structure de données, sur laquelle opèrent
les primitives que l’on vient de décrire. Certains auteurs parlent de delayed lists (listes délayées ??). Le même terme de stream est
utilisé dans d’autres dialectes de Lisp, tel Common-Lisp [Ste90] pour désigner un concept sensiblement différent, le flot d’entréesortie. Les fonctions d’entrée et de sortie de Common-Lisp opèrent sur ces streams, et un nombre respectable d’autres fonctions
permettent de manipuler ces objets. La notion correspondante en Scheme est le port d’entrée-sortie (c.f. [R4R90] § 6.10).
115
116
CHAPITRE 8. FLOTS
(cons (car u1)
(append (cdr u1) u2))))
va devenir :
(define (stream-append u1 u2)
(if (stream-null? u1)
u2
(cons-stream (stream-car u1)
(stream-append (stream-cdr u1) u2))))
La procédure d’énumération en flot des éléments d’un arbre va s’écrire :
(define (tree->stream tree)
(if (pair? tree)
(stream-append (tree->stream (car tree))
(tree->stream (cdr tree)))
(if (null? tree)
(stream)
(stream tree))))
Savoir si l’exploration de deux arbres, éventuellement différents par leurs structures, fournit les mêmes
éléments dans le même ordre peut s’écrire :
(define (compare-tree t1 t2)
(define (stream-equal? s1 s2)
(or
(and (stream-pair? s1)
(stream-pair? s2)
(equal?
(stream-car s1)
(stream-car s2))
(stream-equal? (stream-cdr s1)
(stream-cdr s2)))
(and (stream-null? s1)
(stream-null? s2))))
(stream-equal? (tree->stream t1)(tree->stream t2)))
On trouvera dans [ASS85] nombre d’exemples concernant l’utilisation de flots.
8.1.2
Exercices
Pour s’échauffer, comme on dit...
8.1.2.1
Préparation
Ecrivez une première version des fonctions cons-stream, stream-car, et toutes les autres, opérant sur des
listes simples. Comment réaliser une implantation performante? Peut-on utiliser pour ceci des macros? Discuter
des diverses options.
8.1.2.2
Accumulate
Programmer la fonction (accumulate -op. -init . -flot .), qui combine les éléments du flot -flot . par la
fonction à deux opérandes -op. dont l’élément neutre est -init ..
Comment, avec cette fonction, obtenir la somme ou le produit des éléments d’un flot ? Comment fournir
la liste contenant les éléments d’un flot ? Comment écrire la fonction qui place bout à bout tous les éléments
d’un flot de flots? Comment calculer par la méthode de Horner la valeur d’un polynome défini par le flot de ses
coefficiants pour un x donné.
8.1.2.3
Filter
Ecrire la fonction (filter -predicat . -flot .) qui fournit en sortie un flot dont les éléments sont ceux du
flot d’entrée -flot . qui satisfont -predicat .. Ainsi, (filter odd? -flot .) ne fournira en sortie que les éléments
impairs de -flot ..
8.2. FLOTS INFINIS
8.1.2.4
117
Transversal
Ecrire, en utilisant la technique des flots, une procédure (transversal tree), s’appliquant à un arbre, et
fournissant un flot contenant la liste des éléments de l’arbre obtenue en énumérant ces éléments non pas en
profondeur, mais en largeur. On considérera deux versions de cette procédure : l’une qui s’applique à des arbres
binaires, chaque cons représentant un noeud, l’autre qui s’applique à des arbres quelconques, l’imbrication des
listes représentant les niveaux de l’arbre.
1 ]=> (transversal ’(a (b (c d) e) (f g) h))
;Value: (a h b e f g c d)
1 ]=> (transversal ’(a ((b c (d e))f(g)h)i
(j((k((l)))m)n)o p (q r s) t u v w (x) y z))
;Value: (a i o p t u v w y z f h j n q r
s x b c g m d e k l)
1 ]=> (transversal-bin ’((a . b).((c . d) . e)))
;Value: (a b e c d)
1 ]=> (transversal-bin ’(a (b (c d) e) (f g) h))
;Value: (a b f h c e g d)
8.2
Flots infinis
Lorsque l’on multiplie les (petites) applications écrites grâce à cette technologie, on se rend compte d’une
part de la puissance et de la généralité de celle-ci, d’autre part de certaines de ses limitations. La plus importante
tient au fait que l’on ne manipule pas véritablement un flot de données, mais des listes complètes : le générateur
initial construit une première liste ; celle-ci est ensuite transmise au premier filtre ; le résultat parvient au second
filtre, etc. Certains algorithmes vont calculer des centaines ou des milliers de valeurs dont on ne va peut-être
utiliser que les premières. Nous allons donc voir une autre représentation des flots, faisant appel à un mécanisme
d’évaluation paresseuse.
8.3
Evaluation paresseuse en Scheme
Bien que Scheme soit un langage fonctionnant en ordre applicatif, il fournit deux opérations primitives
permettant d’opérer en ordre normal paresseur.3
8.3.1
Les opérations delay et force
Une modélisation des flots infinis repose sur l’utilisation de deux opérations, delay et force.
L’opération delay accepte un paramètre qui ne va pas être évalué, mais dont le calcul sera retardé. Le
résultat de l’opération est une promesse :
1 ]=> (define toto (delay (* 2 3)))
;Value: toto
1 ]=> toto
;Value: #[promise 3]
Il est possible d’obtenir la valeur de l’expression retardée grâce à la fonction force. Celle-ci évalue une promesse,
et fournit le résultat de l’expression associée :
1 ]=> (force toto)
;Value: 6
La valeur associée à la promesse est alors conservée. Un autre appel de force sur le même objet ne va pas
exécuter à nouveau l’expression, mais simplement fournir la valeur déja calculée :
1 ]=> (define titi (delay
(begin (display "Hello!") (* 7 5))))
;Value: titi
1 ]=> titi
;Value: #[promise 4]
3 Scheme est un langage qui pratique l’évaluation au plus tôt (ou eager evaluation). Les fonctions présentées permettent de
travailler en mode d’évaluation retardée, ou lazy evaluation.
118
CHAPITRE 8. FLOTS
1 ]=> (force titi)
Hello!
;Value: 35
1 ]=> (force titi)
;Value: 35
1 ]=> titi
;Value: #[promise 4]
Voici une implantation possible de ces opérations, sous les noms de delai et forse. Notre macro delai
fournit une paire pointée dont le car indique si la promesse a déjà été calculée. Le cdr est, soit une fonction
sans paramètre, calculant la valeur de la promesse, soit la valeur elle-même :
1 ]=> (define-macro (delai exp)
‘(cons #f (lambda () ,exp)))
;Value: delai
1 ]=> (define (forse dl)
(if (car dl)
(cdr dl)
(begin (set-car! dl #t)
(set-cdr! dl ((cdr dl)))
(cdr dl))))
;Value: forse
La fonction forse utilise un trait impératif nouveau, les opérations (peut-on réellement parler de fonction ?)
set-car! et set-cdr!, qui permettent de modifier respectivement le car et le cdr d’une paire pointée.
1 ]=> (define qq (delai (* 2 3)))
;Value: qq
1 ]=> qq
;Value: (() . #[compound-procedure 2])
1 ]=> (forse qq)
;Value: 6
1 ]=> qq
;Value: (#T . 6)
1 ]=> (forse qq)
;Value: 6
8.3.1.1
Exercice.
Essayer de réaliser votre propre implantation de delay et de force, en utilisant une autre structure qu’une
paire pointée. Peut-on y parvenir sans utilisation de macro? Peut-on y parvenir sans utiliser un trait impératif,
tel que set!, set-car! ou set-cdr!?
8.3.2
Applications
On peut envisager, grâce à cette technique d’évaluation retardée, de rendre disponible dans le langage des
primitives permettant les trois types de passages de paramètres : par valeur, par nom, et par besoin. On peut
représenter de tels paramètres par une paire pointée, de la forme (-type. . -op.), où -type. est un symbole
décrivant la nature du paramètre, -op. est soit la valeur, soit une fonction de calcul de cette valeur.
Voici une première vision de la chose, dans laquelle on utilise explicitement les trois formes value!, name!
et need! pour spécifier les paramètres d’une fonction. Il faut également faire appel à compute! pour obtenir la
valeur d’un paramètre.
1 ]=> (define-macro (value! x) ‘(cons ’value ,x))
;Value: value!
1 ]=> (define-macro (name! x) ‘(cons ’name (lambda () ,x)))
;Value: name!
1 ]=> (define-macro (need! x) ‘(cons ’need (lambda () ,x)))
;Value: need!
1 ]=> (define (compute! x)
(case (car x)
((value) (cdr x))
((name) ((cdr x)))
8.3. EVALUATION PARESSEUSE EN SCHEME
((need)
119
(set-cdr! x ((cdr x)))
(set-car! x ’value)
(cdr x))))
;Value: compute!
1 ]=> (define toto 5)
;Value: toto
1 ]=> (define a (value! toto))
;Value: a
1 ]=> (define b (name! toto))
;Value: b
1 ]=> (define c (need! toto))
;Value: c
1 ]=> a
;Value: (value . 5)
1 ]=> b
;Value: (name . #[compound-procedure 2])
1 ]=> c
;Value: (need . #[compound-procedure 3])
1 ]=> (set! toto (+ toto 1))
;Value: 5
1 ]=> (compute! a)
;Value: 5
1 ]=> (compute! b)
;Value: 6
1 ]=> (compute! c)
;Value: 6
1 ]=> (set! toto (+ toto 1))
;Value: 6
1 ]=> (compute! a)
;Value: 5
1 ]=> (compute! b)
;Value: 7
1 ]=> (compute! c)
;Value: 6
1 ]=> a
;Value: (value . 5)
1 ]=> b
;Value: (name . #[compound-procedure 2])
1 ]=> c
;Value: (value . 6)
8.3.3
Jensen’s device
Le langage ALGOL 60 permettait à la fois l’appel par nom et l’appel par valeur. Le mécanisme de Jensen
(Jensen’s device) en est une illustration typique. Il consiste à modifier la valeur de certains paramètres passés
par nom, afin d’obtenir des effets de bord sur d’autres paramètres, également passés par nom. Voici l’exemple
de la fonction somme :
real procedure somme(x,i,j,k);
value j, k;
real x;
integer i, j, k;
begin
real s;
s := 0.0;
for i:=j step 1 until k do
s:=s + x;
somme := s;
end;
et quelques uns de ses appels typiques :
120
CHAPITRE 8. FLOTS
int w;
real A[1:10];
real M[1:10;1:10];
somme(w,w,1,10);
somme(w*w*w,w,1,10);
somme(A[i],i,1,10);
somme(somme(M[i;j],i,1,10),j,1,10);
Pour comprendre ce qui se passe, imaginons simplement que les paramètres formels passés par nom (ici x
et i) sont remplacés, dans le texte du sous-programme, par l’expression correspondante de la forme d’appel.
Ainsi, le second appel de somme, somme(w*w*w,w,1,10); va correspondre à l’exécution de la boucle :
for w:=j step 1 until k do
s:=s + w*w*w;
Ces quatre expressions calculent donc la somme des dix premiers entiers, des cubes des dix premiers entiers,
des éléments du vecteur A et de la matrice M.
Nous pouvons illustrer ce même mécanisme en Scheme. Voici une nouvelle version de la macro name!, qui
nous fournit également une procedure parmettant de modifier l’argument passé par nom (dans le cas d’un
symbole) :
(define-macro (name! x)
(if (symbol? x)
‘(cons ’name
(cons (lambda () ,x)
(lambda (@-$&*!2Fc) (set! ,x @-$&*!2Fc))))
‘(cons ’name
(cons (lambda () ,x)
(lambda (@-$&*!2Fc) #f)))))
Il est fait appel ici (faute de macros hygiéniques) à un nom barbare pour l’argument de la procedure de
modification, afin d’éviter un conflit éventuel avec le paramètre de la macro.
Nos nouvelles opérations de calcul de valeur et d’affectation deviennent :
(define (compute! x)
(case (car x)
((value) (cdr x))
((name) ((cadr x)))
((need) (set-cdr! x ((cdr x)))
(set-car! x ’value)
(cdr x))))
(define (set-value! x w)
(case (car x)
((value) (set-cdr! x w))
((name) ((cddr x) w))
((need) (set-car! x ’value)
(set-cdr! x w))))
Voici enfin notre propre version de la fonction somme. Nous passerons x et i par nom, j et k par valeur.4
La boucle for de la version Algol est remplacée par une fonction locale sans paramètre, à récursion terminale,
et dont le comportement est donc itératif. Il serait tout aussi simple d’écrire une macro modélisant une telle
construction, ou encore d’utiliser la forme primitive do (c.f. [R4R90], § 4.2.4).
(define (somme x i j k)
(letrec ((s 0)
(loop (lambda ()
(if (<= (compute! i) k)
(begin
(set! s (+ s (compute! x)))
(set-value! i (+ (compute! i) 1))
4 Nous n’utilisons ici, pour de simples raisons de lisibilité, les opérations compute! et set-value! que pour les arguments passés
par nom. Les arguments passés par valeur, dont le comportement ne changerait pas, ne sont pas enrobés dans de telles formes.
8.4. MODÉLISATION DES FLOTS INFINIS
121
(loop))))))
(set-value! i j)
(loop)
s))
Et voici quelques essais :
1 ]=> (define w)
;Value: w
1 ]=> (somme (name! w) (name! w) 1 10)
;Value: 55
1 ]=> (somme (name! (* w w w)) (name! w) 1 10)
;Value: 3025
1 ]=> (somme (name! (/ 1 w)) (name! w) 1 10)
;Value: 7381/2520
1 ]=> (define v #(1 2 5 4 3 0 0 7 6 2))
;Value: v
1 ]=> (+ 1 2 5 4 3 0 0 7 6 2)
;Value: 30
1 ]=> (somme (name! (vector-ref v w)) (name! w) 0 9)
;Value: 30
8.4
Modélisation des flots infinis
8.4.1
Réécriture des fonctions sur flots
Ces nouvelles opérations, delai et forse vont nous permettre de définir nos opérations sur flots : alors que
le car d’un élément d’un flot sera une valeur, son cdr sera une promesse. Convenons d’une valeur particulière
pour représenter le flot vide :
1 ]=> (define *empty-strime* ())
;Value: *empty-strime*
1 ]=> *empty-strime*
;Value: ()
Et voici nos opérations de base :5
1 ]=> (define-macro (cons-strime p q)
‘(cons ,p (delai ,q)))
;Value: cons-strime
1 ]=> (define-macro (strime-car s)
‘(car ,s))
;Value: strime-car
1 ]=> (define-macro (strime-cdr s)
‘(forse (cdr ,s)))
;Value: strime-cdr
1 ]=> (define-macro (empty-strime? s)
‘(eq? *empty-strime* ,s))
;Value: empty-strime?
8.4.1.1
Remarque
Certains auteurs proposent que cons-stream construisent une paire pointée dont le car comme les cdr sont
des expressions retardées. Que pensez-vous de cette option?
5 L’utilisation de macros nous permet ici de privilégier l’aspect “performances”. En revanche, nous renonçons à certains aspects
fonctionnels, comme l’utilisation des fonctions par map, apply ou des fonctionnelles similaires. Notons cependant que le fait que
les paramètres d’un cons-stream ne doivent pas être évalués implique l’utilisation d’une forme spéciale. Certains systèmes Lisp
proposent, à côté des macros et des fonctions “classiques”, dites SUBR, des formes spéciales, dites FSUBR, qui ne diffèrent des
fonctions que par le fait que les paramètres ne sont pas évalués. Il faut recourir, pour obtenir l’évaluation d’une expression, à
la fonction eval. Cependant, eval risque (même lorsque, comme en MIT-Scheme, on peut spécifier l’environnement dans lequel
s’effectue l’évaluation) de provoquer des conflits de noms plus dramatiques encore que ceux qui sont liés à l’utilisation de macros...
Rien n’est parfait en ce bas monde.
122
CHAPITRE 8. FLOTS
8.4.2
Applications
Voici quelques exemples d’utilisation de ces fonctions :
1 ]=> (define (list->strime l)
(if (null? l) *empty-strime*
(cons-strime (car l)
(list->strime (cdr l)))))
;Value: list->strime
1 ]=> (define (strime->list n f)
(if (or (empty-strime? f) (<= n 0))
’()
(cons (strime-car f)
(strime->list (- n 1) (strime-cdr f)))))
;Value: strime->list
1 ]=> (define li ’(a b c d e f g h i))
;Value: li
1 ]=> (define st (list->strime li))
;Value: st
1 ]=> st
;Value: (a () . #[compound-procedure 3])
1 ]=> (strime->list 5 st)
;Value: (a b c d e)
1 ]=> st
;Value: (a #T b #T c #T d #T e #T f () .
#[compound-procedure 4])
8.4.3
Exercices
8.4.3.1
Au cas où...
Vérifiez que tous les algorithmes écrits avec la première version de nos flots sont encore opérationnels avec
la seconde.
8.4.3.2
Alternate Streams
Ecrivez la fonction (alternate-strime f1 f2) qui fournit un flot contenant la liste alternée des valeurs
des flots f1 et f2.
1 ]=> (define st1 (list->strime ’(a b c d e f g)))
;Value: st1
1 ]=> (define st2 (list->strime ’(1 2 3 4 5)))
;Value: st2
1 ]=> (define st3 (alternate-strime st1 st2))
;Value: st3
1 ]=> st1
;Value: (a () . #[compound-procedure 5])
1 ]=> st2
;Value: (1 () . #[compound-procedure 6])
1 ]=> st3
;Value: (a () . #[compound-procedure 7])
1 ]=> (strime->list 6 st3)
;Value: (a 1 b 2 c 3)
1 ]=> (strime->list 100 st3)
;Value: (a 1 b 2 c 3 d 4 e 5 f g)
8.4.3.3
Merge Streams
Ecrivez la fonction (merge-strime f1 f2) qui fournit un flot contenant la liste triée des valeurs des flots
f1 et f2 (on suppose ceux-ci triés par ordre croissant).
8.4. MODÉLISATION DES FLOTS INFINIS
8.4.3.4
123
Streams
Nous n’avons pas redéfini l’opération stream, qui est le pendant de list pour des flots. Comment feriezvous, en vous souvenant que les paramètres ne doivent être évalués que lors de l’utilisation du flot, non lors de
sa création?
8.4.4
Flots infinis
Une utilisation typique des opérations sur flots est la création et la manipulation de flots infinis. Voici par
exemple une fonction permettant la création du flot des entiers à partir d’un certain ordre :
1 ]=> (define (entiers-from n)
(cons-strime n (entiers-from (+ n 1))))
;Value: entiers-from
1 ]=> (define l (entiers-from 100))
;Value: l
1 ]=> l
;Value: (100 () . #[compound-procedure 16])
1 ]=> (strime->list 15 l)
;Value: (100 101 102 103 104 105 106 107 108 109 110
111 112 113 114)
8.4.5
Exercices
8.4.5.1
nth-stream
Ecrire une fonction fournissant le neme élément d’un flot : (nth-stream -n. -flot .).
8.4.5.2
Multiples
Ecrivez une fonction founissant un flot contenant la liste croissante de tous les multiples d’un entier donné.
Fournir le flot contenant la liste croissante de tous les nombres qui sont des multiples de 5, 7 et 13. Imprimer
les 1000 premiers éléments de ce flot.
8.4.5.3
Fibonacci
Ecrire une fonction construisant le flot de Fibonacci, c’est à dire le flot contenant les valeurs des éléments
de la suite de Fibonacci. Quelle est la valeur du 1000eme élément de ce flot?
8.4.5.4
Nombres premiers
Ecrire une fonction fournissant le flot des nombres premiers. Quelle est la valeur du 1000eme élément de ce
flot?
8.4.5.5
Applicateur
Ecrire la fonction map-stream qui prend comme paramètres une fonction et un flot, et fournit le flot des
applications de la fonction sur chaque élément du flot. Généraliser à un nombre arbitraire de flots. En déduire
l’écriture d’une fonction fournissant le flot des factorielles des entiers naturels.
8.4.5.6
Constructeur
Ecrire la fonction build-stream qui prend comme paramètres une fonction f et une valeur initiale n, et
construit le flot des applications (ou itérations) de f , (n, f (n), f (f (n))...).
Ecrire de même la fonction build-stream2, qui prend comme paramètres une fonction f et deux valeurs n
et p, et construit le flot des applications successives (n, p, f (n, p), f (p, f (n, p)), f (f (n, p), f (p, f (n, p))), etc.).
En déduire (encore) une formulation du flot de Fibonacci.
8.4.6
Flots de caractères
Nous avons quelque peu délaissé les caractères jusqu’à présent. Se reporter à [R4R90], § 6.6, 6.7 et 6.10 pour
tout savoir des caractères, des chaı̂nes de caractères et des entrées sorties.
Nous allons écrire quelques outils permettant le traitement de fichiers de type texte, en utilisant la technique
des streams.
124
8.4.6.1
CHAPITRE 8. FLOTS
Entrées et sorties
Ecrire les fonctions file->stream et stream->file qui permettent de réaliser des lectures et écritures sur
fichiers séquentiels ASCII en termes de streams.
8.4.6.2
wc
Ecrire en Scheme l’utilitaire Unix wc qui permet le comptage des caractères, mots et lignes d’un fichier
ASCII.
8.4.6.3
zap-gremlins
Ecrire en Scheme l’utilitaire zap-gremlins qui permet de “nettoyer” un fichier ASCII : suppression des
caractères non imprimables, des blancs de fin de lignes, remplacement des tabulations par des espaces, etc.
Imaginer quelques autres options qui vous seraient utiles (ou auraient pu vous être utiles un jour...).
Chapitre 9
Continuations
9.1
Introduction
L’évaluation de toute expression, en Scheme comme en Lambda Calcul, peut être considérée comme une
suite de transformations (ou réductions) appliquées à l’expression initiale. Le résultat est une expression mise
(si possible) sous sa forme normale. L’algorithme d’évaluation consiste à détecter, dans l’expression, les tranformations applicables, à sélecter l’une d’entre elles, et à l’appliquer. Chaque transformation concerne donc
une portion de l’expression courante, le calcul actif , le reste de l’expression étant mis en attente. Ainsi, dans
l’expression :
(sqrt (+ (* 3 3) (* 5 8)))
l’évaluateur va t-il choisir une première transformation à appliquer, par exemple le calcul (* 5 8). La partie
en attente peut alors se représenter par :
(sqrt (+ (* 3 3) "))
expression dans laquelle " symbolise le calcul en cours, et que l’on peut qualifier de contexte de cette exécution.
9.2
Contexte
Cet “avenir” du calcul courant peut être matérialisé sous la forme d’une fonction, dont le paramètre serait
la valeur attendue, résultat du calcul (* 5 8) :
(lambda (") (sqrt (+ (* 3 3) ")))
Notre expression initiale est ainsi équivalente à :
((lambda (") (sqrt (+ (* 3 3) "))) (* 5 8))
Le langage Scheme nous permet d’exprimer effectivement ce contexte du calcul courant au moyen d’une
lambda expression :
(lambda (w) (sqrt (+ (* 3 3) w)))
Cette expression représente la continuation du calcul (* 5 8) dans l’expression (sqrt (+ (* 3 3) (* 5 8))).
9.2.1
Un premier exemple
Reprenons l’exemple de la fonction factorielle :
(define (fact n)
(if
(= 0 n)
1
(* n (fact (- n 1)))))
l’évaluation d’une expression telle que (display (fact 4)) peut se matérialiser par les enchaı̂nements
suivants :
(display (fact 4))
(display (if (= 0 4) 1 (* 4 (fact (- 4 1)))))
125
126
CHAPITRE 9. CONTINUATIONS
(display
(display
(display
(display
(display
(display
(display
(display
(display
(display
(display
(display
(display
(display
(display
(display
(display
(* 4
(* 4
(* 4
(* 4
(* 4
(* 4
(* 4
(* 4
(* 4
(* 4
(* 4
(* 4
(* 4
(* 4
(* 4
(* 4
24)
(fact (- 4 1))))
(fact 3)))
(if (= 0 3) 1 (* 3 (fact (- 3 1))))))
(* 3 (fact (- 3 1)))))
(* 3 (fact 2))))
(* 3 (if (= 0 2) 1 (* 2 (fact (- 2 1)))))))
(* 3 (* 2 (fact (- 2 1))))))
(* 3 (* 2 (fact 1)))))
(* 3 (* 2 (if (= 0 1) 1 (* 1 (fact (- 1 1))))))))
(* 3 (* 2 (* 1 (fact (- 1 1)))))))
(* 3 (* 2 (* 1 (fact 0))))))
(* 3 (* 2 (* 1 (if (= 0 0) 1 (* 0 (fact (- 0 1)))))))))
(* 3 (* 2 (* 1 1)))))
(* 3 (* 2 1))))
(* 3 2)))
6))
Si l’on s’intéresse en particulier aux appels récursifs de la fonction fact, on peut mettre en évidence la suite
de contextes :
(*
(*
(*
(*
4
4
4
4
")
(* 3 "))
(* 3 (* 2 ")))
(* 3 (* 2 (* 1 "))))
qui ont cette structure :
(* n ")
Soit k la fonction suivante :
(lambda (") (* 4 (* 3 ")))
qui correspond au contexte de la seconde dérivation. Notre calcul peut s’écrire :
(k (fact 2))
Les différentes lignes sont donc de la forme :
(kn (fact n))
dans laquelle kn représente une certaine lambda expression :
(display (fact 4))
⇔
(k4 (fact 4))
avec
k4
⇔
(lambda (x) (display x))
De même,
(* 4 (fact 3))
⇔
(k3 (fact 3))
avec
k3
⇔
(lambda (x) (* 4 x))
et ainsi de suite :
(fact 3)
⇔ (* 3 (fact 2))
k2
⇔ (lambda (x) (* 3 x))
...
⇔
(k2 (fact 2))
127
9.2. CONTEXTE
Les continuations successives mises en évidence sont donc :
(lambda
(lambda
(lambda
(lambda
(lambda
(x)
(x)
(x)
(x)
(x)
(display x))
(* 4 x))
(* 3 x))
(* 2 x))
(* 1 x))
On voit apparaitre le schéma suivant pour notre factorielle :
(define (fact n)
(if
(= 0 n)
1
(-k . (fact (- n 1)))))
dans lequel -k . représente la continuation idoine.
Il est possible de transformer notre schéma applicatif, qui est de la forme f (g(x)), en g(x, f ), en modifiant
notre fonction la plus interne g pour qu’elle applique son paramètre f sur le résultat de son calcul. Nous
introduisons donc un paramètre supplémentaire, qui représente explicitement la continuation :
(define (fact n k)
(if
(= 0 n)
(k 1)
(fact (- n 1) -k .)))
Notons en particulier l’introduction de l’application de k sur la valeur finale 1 en fin de récursivité. L’expression
initiale, (display (fact 4)) devient ainsi :
(fact 4 display)
Il nous reste à construire l’expression -k .. On sait que k est la fonction qui attend comme paramètre la valeur f act(n). Le calcul de (fact (- n 1)) va nous fournir la valeur f act(n − 1), qui va être transmise à la
continuation -k .. La fonction -k . doit appliquer k sur le résultat du produit de n par f act(n − 1). Elle s’écrit
donc :
(lambda (x) (k (* n x)))
La fonction factorielle devient ainsi :
(define (fact n k)
(if
(= 0 n)
(k 1)
(fact (- n 1) (lambda (x) (k (* n x))))))
9.2.2
Le style CPS
Nous venons de de faire apparaı̂tre la continuation comme paramètre explicite de notre fonction. Cette
transformation nous a fourni la forme CPS (pour Continuation Passing Style) de notre programme. Quels sont
les avantages et les inconvénients de cette forme? Tous d’abord, notre programme n’utilise plus que des appels
récursifs terminaux. Comme nous l’avons vu, il va donc être interprété sous forme itérative par le système. Mais
surtout, nous gagnons une grande souplesse d’exécution. Nous allons donner quelques exemples montrant, pour
différentes classes de problèmes, l’intérêt de la programmation CPS .
9.2.2.1
Erreurs et échappements
Imaginons une fonctions f qui fait appel à une fonction g. La fonction g est susceptible de détecter des
erreurs, par exemple parce que ses paramètres ne sont pas conformes au modèle attendu. En programmation
traditionnelle, la fonction f va devoir tester le code de retour de g, et éventuellement, transmettre elle-même
128
CHAPITRE 9. CONTINUATIONS
un code en retour à la fonction qui a fait appel à elle.1 A petites causes, grands effets : on voit se profiler une
multitude de tests à chaque niveaux, afin de tenir compte des particularités de chaque fonction élémentaire au
niveau de chacune des strates de notre logiciel...
Considérons la fonction primitive sqrt. Cette fonction fournit un nombre complexe lorsque son paramètre
est négatif :
1 ]=> (sqrt -4)
;Value: +2i
Si nous voulons au contraire signaler une erreur, nous pouvons écrire :
1 ]=> (define (sq x)
(if (< x 0)
"sq Error"
(sqrt x)))
;Value: sq
1 ]=> (sq 10)
;Value: 3.1622776601683795
1 ]=> (display (+ (sq 0.5) (sq 0.7)))
1.543766807720623
;No value
Malheureusement, ce genre de construction est très sensible aux erreurs :
1 ]=> (display (+ (sq -1.23) (sq 0.7)))
Illegal datum in first argument position "sq Error"
within procedure #[primitive-procedure integer->flonum]
There is no environment available;
using the current REPL environment
;Package: (user)
Idéalement, il faudrait tester dans + le compte-rendu de sq, etc.
Une meilleure solution est de programmer en CPS , en utilisant une fonction sq à laquelle on transmet la
continuation, sq pouvant abandonner éventuellement cette continuation en cas d’erreur :
1 ]=> (define (sq x k)
(if (< x 0)
"sq Error"
(k (sqrt x))))
;Value: sq
1 ]=> (sq 10 display)
3.1622776601683795
;No value
1 ]=> (sq -5 display)
;Value: "sq Error"
1 ]=> (sq 0.5 (lambda (u) (sq 0.7
(lambda (v) (display (+ u v))))))
1.543766807720623
;No value
1 ]=> (sq -1.23 (lambda (u) (sq 0.7
(lambda (v) (display (+ u v))))))
;Value: "sq Error"
9.2.2.2
Introduction aux Coroutines
Nous venons de voir que le style CPS permet très facilement d’abandonner le traitement d’un problème.
Associé au mécanisme des fermetures, montrons qu’il permet également de reprendre aisément un traitement
interrompu.
1 Ce qui nous pose un problème philosophique : une fonction h, écrite en tenant compte des particularités de f, va se tirer
honorablement de la situation. Mais que faire si j’écris (display (f x)), et que f détecte — par exemple dans g — une erreur et la
signale à display? On ne peut pas s’attendre à ce qu’une fonction extrêmement générale comme display soit capable d’interpréter
les comptes-rendus de n’importe quelle fonction.
9.2. CONTEXTE
129
Revenons au problème d’exploration d’arbre, décrit au chapitre 6.2.3.1 Nous disposons d’une structure
arborescente relativement complexe, T , que nous souhaı̂tons explorer au moyen d’un prédicat pred?. Nous
aimerions écrire une fonction avec les caractéristiques suivantes :
– Elle doit fournir un résultat à la fois.
– Chaque appel doit fournir le résultat “suivant”.
– Notre arbre T est très grand. Il est hors de question de le recopier, de le linéariser, ou de le transformer.
– On ne veut pas non plus calculer tous les résultats à la fois, quitte à les fournir les uns après les autres.
D’une part, la liste peut être elle-même très grande, d’autre part il est possible que l’on ne les demande
pas tous.
Nous allons pour cela utiliser la technique des continuations, mais cette fois, au lieu d’abandonner purement
et simplement le calcul lorsque une certaine condition est remplie, nous allons conserver l’état de ce calcul, afin
de pouvoir le reprendre éventuellement. Voici la fonction make-walker qui va construire un explorateur d’arbre
(tree walker ), fournissant ses résultats selon un certain critère, représenté ici par un prédicat.
1 ]=> (define (make-walker T pred?)
(define (explore S cont!)
(if (pair? S)
(explore (car S)
(lambda () (explore (cdr S) cont!)))
(if (and (number? S) (pred? S))
(begin
(set! glob-cont! cont!)
S)
(cont!))))
(define (glob-cont!)
(explore T (lambda () ())))
(lambda () (glob-cont!)))
;Value: make-walker
La fonction comporte deux définitions internes. glob-cont! est un simple appel d’initialisation de l’autre fonction, explore. Cette dernière prend comme paramètre une structure à explorer, et une continuation, décrivant le
travail à accomplir en cas d’échec sur l’objet courant. On introduit une opération supplémentaire, qui consiste à
affecter à l’opération globale glob-cont! la continuation courante en cas de succès permettant ainsi une reprise
du traitement. Enfin, la continuation globale initiale est la fonction (lambda () ()) qui rend nil, fournissant
une valeur par défaut lorsque l’exploration de l’arbre est terminée.
Voici deux exemples d’exploration d’un arbre sur deux critères différents :
1 ]=> (define Tree ’(1 2 3 -9 3 (5 6 -2 8 -7 3) (9
(11 -3 12) 7 11 (13 (15 (17 ((19 20) -3 -7)
-11)))) 6 56 -17 ((((((((3))))) 7 -8)))))
;Value: tree
1 ]=> (define toto (make-walker Tree negative?))
;Value: toto
1 ]=> (toto) ;Value: -9
1 ]=> (toto) ;Value: -2
1 ]=> (toto) ;Value: -7
1 ]=> (toto) ;Value: -3
1 ]=> (toto) ;Value: -3
1 ]=> (toto) ;Value: -7
1 ]=> (toto) ;Value: -11
1 ]=> (toto) ;Value: -17
1 ]=> (toto) ;Value: -8
1 ]=> (toto) ;Value: ()
1 ]=> (toto) ;Value: ()
1 ]=> (define titi (make-walker Tree odd?))
;Value: titi
1 ]=> (titi) ;Value: 1
1 ]=> (titi) ;Value: 3
130
CHAPITRE 9. CONTINUATIONS
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
]=>
]=>
]=>
]=>
]=>
]=>
]=>
]=>
]=>
]=>
]=>
]=>
]=>
]=>
]=>
]=>
]=>
]=>
]=>
]=>
]=>
]=>
]=>
]=>
(titi)
(titi)
(titi)
(titi)
(titi)
(titi)
(titi)
(titi)
(titi)
(titi)
(titi)
(titi)
(titi)
(titi)
(titi)
(titi)
(titi)
(titi)
(titi)
(titi)
(titi)
(titi)
(titi)
(titi)
;Value:
;Value:
;Value:
;Value:
;Value:
;Value:
;Value:
;Value:
;Value:
;Value:
;Value:
;Value:
;Value:
;Value:
;Value:
;Value:
;Value:
;Value:
;Value:
;Value:
;Value:
;Value:
;Value:
;Value:
-9
3
5
-7
3
9
11
-3
7
11
13
15
17
19
-3
-7
-11
-17
3
7
()
()
()
()
Bien entendu, chaque appel de make-walker comporte son propre environnement local, ici, les objets T,
glob-cont!, pred?, etc, et va donc générer un marcheur indépendant des autres : tout marche correctement
comme on peut l’espérer...
Nous avons en fait mis en place un mécanisme qui, dans ce cas particulier, s’apparente aux coroutines. Nous
verrons ultérieurement que la généralisation de la transformation CPS, ainsi que l’utilisation du call/cc permet
l’écriture de telles coroutines sous une forme beaucoup plus simple.
9.2.2.3
Valeurs multiples
Il est parfois nécessaire à un sous-programme de transmettre plusieurs résultats différents à l’appelant.
Scheme nous offre, comme la plupart des langages de programmations traditionnels, plusieurs solutions : utilisation de variables globales ou de variables de l’environnement, passage par nom, tel que nous venons de le voir
au chapitre précédent, ou encore formation d’une structure, contenant les différents résultats (liste, vecteur), ou
pourquoi pas, création d’une fermeture, susceptible de fournir ces valeurs.
Certains dialectes de Lisp, comme Common Lisp [Ste90], proposent un mécanisme plus simple et plus efficace
que ces différentes techniques, qui est l’utilisation de valeurs multiples. Techniquement, une fonction peut rendre
une valeur principale, et des résultats annexes. La valeur principale est la seule utilisée par les opérations
habituelles du langage, des constructions spéciales permettant l’accès aux autres résultats. Ainsi, Common Lisp
fait appel pour cette manipulation à un ensemble de fonctions et formes spéciales, telles multiple-value-list,
multiple-value-bind, values, etc.
La technique des continuations, qui permet de remplacer un retour d’une fonction par un appel d’une
continuation, peut être généralisée au passage de valeurs multiples à la continuation : il suffit d’appliquer celle-ci
sur plusieurs valeurs au lieu d’une seule.
Voici (faute d’un meilleur exemple) une fonction qui calcule la longueur et le dernier élément de son paramètre, une liste, valeurs bien entendu calculées en une seule traversée de la liste. Les deux résultats sont
transmis à la continuation, qui peut dans certains cas n’en utiliser qu’un :
1 ]=> (define (length&last li k)
(define (l&l n li k)
(if (null? (cdr li))
(k n li)
(l&l (+ n 1) (cdr li) k)))
(if (pair? li)
(l&l 1 li k)
(k 0 li)))
;Value: length&last
9.3. TRANSFORMATION CPS
1 ]=> (length&last
;Value: (7 (g))
1 ]=> (length&last
;Value: (0 toto)
1 ]=> (length&last
;Value: (1 (a))
1 ]=> (length&last
;Value: (7 g)
1 ]=> (length&last
;Value: 7
1 ]=> (length&last
;Value: (g)
9.3
131
’(a b c d e f g) list)
’toto list)
’(a) list)
’(a b c d e f g) cons)
’(a b c d e f g) (lambda (x y) x))
’(a b c d e f g) (lambda (x y) y))
Transformation CPS
Ce mécanisme de transformation d’un programme fonctionnel en un programme exprimé dans le style CPS
revêt une importance toute particulière dans le domaine de la compilation des programmes fonctionnels. Sans
aller jusqu’à la construction d’un compilateur complet, tel celui décrit dans [FWH89], nous allons donner les
règles de transformations s’appliquant aux programmes Scheme, et aborder quelques exemples.
9.3.1
Définitions préliminaires
Précisons tout d’abord quelques uns des termes que nous utiliserons dans notre exposé.
9.3.1.1
Formes
Nous supposerons que nous nous intéressons à un sous-ensemble de Scheme, comportant des opérations
primitives, des formes spéciales, des constantes, variables, etc. Nous caractériserons en particulier les formes
Scheme, c’est à dire les expressions de la forme
(e1 e2 ... en)
par leur ordre d’exécution des sous-expressions ei. Ces sous-expressions peuvent être initiales (I ), finales (F ) ou
indéterminées (E ). Il peut y avoir plusieurs expressions initiales, qui sont exécutées dans un ordre indéterminé.
Il n’y a qu’une seule expression finale, qui est exécutée après toutes les expressions initiales, et qui est le résultat
de la forme. Enfin, des expressions indéterminées peuvent intervenir dans certaines formes. Nous préciserons
ultérieurement cette notion.
9.3.1.2
Opérations primitives
Nos opérations primitives ont les caractéristiques suivantes :
– Elles ne sont pas implantées par des appels de procédures.
– Elles ne font pas appel aux procédures qui peuvent lui être passées comme arguments.
– Ce ne sont pas des objets de première classe.
Ces restrictions impliquent par exemple que l’on ne peut pas définir une fonction telle que :
(define (fun f) (f 0 0))
et faire des appels à fun sous la forme (fun +) ou (fun cons) : la fonction f est supposée être une opération
définie. Ces restrictions sont simplement liées au fait que l’appel (f 0 0) va être modifié par la transformation
CPS. D’un autre côté, une telle limitation n’est pas significative, car l’effet de (fun +) peut être obtenu par
(fun (lambda (u v) (+ u v))), forme qui respecte nos conventions. Ceci exclut également des formes comme
map ou apply, qui peuvent elles aussi être simulées au moyen de fonctions définies.
Nos opérations primitives sont donc celles (telles car, cons, etc.) qui sont implantées au coeur du système
en langage machine. On supposera que ces opérations existent en nombre fini, et que l’on est capable de les
reconnaı̂tre dans le texte des programmes. Leur schéma d’exécution est de la forme :
(op I I I ... I )
132
CHAPITRE 9. CONTINUATIONS
c’est à dire qu’elles comportent un nombre variables d’expressions initiales, qui sont toutes exécutées dans un
ordre indéterminé, puis qu’une certaine opération est appliquée sur les résultats de ces expressions.
Notons qu’une application fonctionnelle (dont le premier élément est une fonction définie) est de la forme :
(I I I ... I )
Là également, toutes les expressions initiales sont exécutées dans un ordre arbitraire, le résultat de la première
fournissant la fonction à appliquer.
9.3.1.3
Formes spéciales
Le formes spéciales du langage se ramènent toutes au schéma d’exécution suivant : toutes les expressions
“initiales” sont évaluées ; suivant un algorithme propre à la fonction, l’une des expressions terminales est alors
évaluée, et devient le résultat de la forme. Voici les schémas d’exécution des formes spéciales de base :
(if I F F )
(set! v I )
(let ((v I ) ...) F )
(begin I F )
(lambda (v ...) E )
Les autres formes spéciales se déduisent celles-ci. Par exemple, (begin e1 e2 e3) équivaut à (begin e1
(begin e2 e3)), (and e1 e2) à (if e1 e2 #f), etc. Notons aussi qu’une forme lambda immédiatement appliquée peut être transformée en une forme let équivalente :
((lambda (v1 v2 ... vn) E) p1 p2 ... pn)
⇔ (let ((v1 p1) (v2 p2) ... (vn pn)) E)
Notons le cas particulier des lambda expressions utilisées en paramètres d’autres fonctions. Les expressions
contenues dans ces lambda expressions ne participent pas à l’évaluation de la forme contenant ces lambda
expressions, mais seront (éventuellement) utilisées ultérieurement : elles ne sont ni initiales, ni finales, mais
“suspendues”, ou indisponibles.
9.3.1.4
Autres définitions
Il est nécessaire de caractériser certains cas particuliers pour notre algorithme de transformation. Nous
utiliserons les concepts suivants :
expression simple Nous dirons qu’une expression est simple si elle ne peut pas entraı̂ner un comportement
récursif. C’est le cas en particulier des applications de fonctions primitives, et de toute application de
fonction primitive sur des expressions simples. C’est le cas également (avec nos restrictions) d’une lambda
expression. Nous adopterons un point de vue conservateur en supposant que seules les fonctions primitives
ne sont pas susceptibles d’être récursives.
forme finale Nous dirons qu’une expression est en forme finale si toutes ses sous-expressions initiales sont
simples.
Une expression peut ainsi être simple, mais non en forme finale, en forme finale, mais non simple, etc. Voici
quelques exemples :
(cons (car x) y)
(f (cdr x))
(cdr (f x))
(lambda(x) (car (cdr x)))
(lambda(x) (car (f x)))
(if p (car x) (cons (car x) (cddr x)))
(if p (f x) (cons (car x) (cddr x)))
(if (f x) x (cons (car x) (cddr x)))
9.3.1.5
simple, finale
non simple, finale
non simple, non finale
simple, finale
simple, non finale
simple, finale
non simple, finale
non simple, non finale
Exercice
Comment caractériseriez-vous les expressions suivantes :
(if (car x) (f (cdr x)) (g (cons x (f x))))
(let ((x (car x)) (y (f (cdr x)))) (cons x (cadr y)))
(and (null? (cddr x)) (eq? (car x) ’lambda) (rec (cdr x)))
133
9.3. TRANSFORMATION CPS
9.3.2
Conversion en forme CPS
Nous allons maintenant aborder le mécanisme effectif de conversion en forme CPS . Le but de cette conversion
est de transformer une fonction en une fonction équivalente, exprimée au moyen de formes terminales.2 Cette
conversion consiste à transformer toute lambda expression, afin qu’elle accepte un paramètre supplémentaire,
sa continuation, et tout appel de fonction définie pour lui transmettre cette continuation comme paramètre
supplémentaire. Ce paramètre, que nous appelerons k par convention, sera ajouté après les autres paramètres
de la fonction.
9.3.2.1
Lambda expression
La transformation d’une lambda expression s’écrit donc ainsi :
(lambda (x1 x2 ... xn ) E)
=⇒ (lambda (x1 x2 ... xn k) (k E))
Exemple :
(lambda (x) (+ x 2))
=⇒ (lambda (x k) (k (+ x 2)))
(lambda (x) (f (car x)))
=⇒ (lambda (x k) (k (f (car x))))
Nous supposerons que toutes les transformations en style CPS s’opèrent simultanément. Le dernier exemple,
qui contient l’application d’une fonction définie, doit donc également être modifié. Nous allons analyser maintenant les autres règles de transformation.
9.3.2.2
Formes simples
Dans le cas d’une forme simple (constante, variable, ou expression simple), la transformation ne peut pas
être poussée plus avant. Ainsi, des expressions comme (k 0) ou (k (cdr (car x))) sont déjà sous forme finale.
9.3.2.3
Appel de fonction
L’appel de fonction définie ne pose pas plus de problèmes lorsque toutes les sous-expressions de la forme
d’appel sont simples. Puisque toutes les fonctions sont simultanément converties en CPS , la transformation
consiste simplement à ajouter à la forme d’appel la continuation comme dernier paramètre :
(k (f (car x)))
=⇒
(fk (car x) k)
(Nous noterons fk la transformée CPS de la fonction f).
9.3.2.4
Formes non finales
La transformation des formes non finales va nécessiter la mise en évidence, dans l’expression, d’une application la plus interne possible, et de son contexte. Ainsi, dans :
(k (* n (fact (- n 1))))
il existe une seule application (non primitive), (fact (- n 1)). Le contexte de cette application est :
(k (* n "))
Notons k’ la fonction associée à ce contexte :
(lambda (v) (k (* n v)))
Notre expression initiale devient :
(k’ (fact (- n 1)))
2 Cette transformation n’implique pas nécessairement que la fonction n’aie plus un comportement récursif. Celui-ci va par exemple
se manifester par une consommation de mémoire pour la représentation des continuations créées par la fonction.
134
CHAPITRE 9. CONTINUATIONS
qui se transforme en :
(factk (- n 1) k’)
autrement dit :
(factk (- n 1) (lambda (v) (k (* n v))))
Il peut éventuellement être nécessaire d’appliquer la transformation CPS au corps de la lambda expression
ainsi obtenue. Par exemple, une forme telle que :
(k (+ (f x) (g y)))
comporte deux applications de fonctions définies. Il faut choisir l’une d’elles comme pivot, par exemple (f x).
On obtient alors :
(fk x (lambda (v) (k (+ v (g y)))))
Le corps de la lambda expression va lui-même être transformé :
(k (+ v (g y)))
=⇒
(gk y (lambda (u) (k (+ v u))))
d’où la forme finale :
(fk x (lambda (v) (gk y (lambda (u) (k (+ v u))))))
Dans ce cas particulier, nous avions deux expressions initiales, qui pouvaient toutes deux être choisies comme
pivot. Le choix de (g y) comme première expression est tout aussi valide, ce qui fournit une transformée
différente, mais équivalente.
(gk y (lambda (u) (fk x (lambda (v) (k (+ v u))))))
Considérons maintenant l’expression :
(k (null? (f (g x))))
Cette expression comporte deux applications, (f (g x)) et (g x), mais dans la première, la sous-expression
n’est pas simple. C’est donc la seconde qui doit obligatoirement être choisie comme pivot. On obtient alors :
(gk x (lambda (v) (k (null? (f v)))))
l’expression interne de la lambda expression devenant à son tour :
(fk v (lambda (u) (k (null? u))))
d’où la transformée finale :
(gk x (lambda (v) (fk v (lambda (u) (k (null? u))))))
9.3.2.5
Formes spéciales
Les formes spéciales sont composées de formes initiales, toutes évaluées dans un ordre arbitraire, et de formes
finales, dont une seule va être évaluée et fournir le résultat de la forme spéciale.
Nous distinguerons deux cas, suivant que toutes les formes initiales sont simples ou non.
Lorsque les formes initiales sont toutes simples, la transformation CPS consiste à appliquer la continuation
à toutes les formes finales :
(k (spec I I I F F F ))
=⇒
(spec I I I (k F ) (k F ) (k F ))
Ainsi,
(k (if p (f x) (cons (car x) (cddr (g x)))))
=⇒
(if p (k (f x)) (k (cons (car x) (cddr (g x)))))
=⇒
(if p (fk x k) (gk x (lambda (v) (k (cons (car x) (cddr v))))))
La transformation CPS a été ici appliquée aussi loin que possible.
Lorsque les formes initiales ne sont pas toutes simples, on va choisir l’une d’entre elles comme pivot :
(k (if (null? (f x)) x (g (cdr x))))
9.3. TRANSFORMATION CPS
135
Ici, (f x) est la seule application initiale candidate. Son environnement k’ est :
(k (if (null? ") x (g (cdr x))))
La première transformation fournit donc :
(fk x (lambda (v) (k (if (null? v) x (g (cdr x))))))
Il faut maintenant appliquer la transformation CPS au corps de la lambda expression. Toutes les expressions
initiales de la forme if sont maintenant simples. On est donc ramené au cas précédent :
(k (if (null? v) x (g (cdr x))))
=⇒
(gk (cdr x) (lambda (u) (k (if (null? v) x u))))
La forme CPS de notre expression initiale est ainsi :
(fk x (lambda (v) (gk (cdr x)
(lambda (u) (k (if (null? v) x u))))))
Considérons un autre exemple :
(k (let ((y 4)) (f (+ y 2) x)))
Il existe une seule expression initiale, 4, qui est simple. Notre forme transformée devient ainsi :
(let ((y 4)) (k (f (+ y 2) x)))
qui est à son tour modifiée en :
(let ((y 4)) (fk (+ y 2) x k))
Etudions maintenant cet autre exemple :
(k (* y (let ((y 4)) (f (+ y 2) x))))
On peut considérer le contexte k’ de la forme let,
(k (* y "))
et se ramener comme ci-dessus à :
(let ((y 4)) (fk (+ y 2) x k’))
donc :
(let ((y 4)) (fk (+ y 2) x (lambda (v) (* y v))))
mais, ce faisant, nous avons créé un conflit de nom : la première occurrence de l’identificateur y était libre dans
l’expression initiale, et est devenue liée dans notre nouvelle forme. Il est clair qu’il faut appliquer une alphaconversion pour renommer la variable locale y de la forme let, par exemple en y.1, ce qui nous donne la forme
correcte suivante :
(let ((y.1 4)) (fk (+ y.1 2) x (lambda (v) (* y v))))
9.3.3
Exemples
Attaquons nous à quelques transformations de fonctions. Voici (à nouveau) la fonction iota générant la liste
des n premiers entiers :
(define (iota n)
(if (= n 0)
’()
(cons n (iota (- n 1)))))
Les étapes de transformation CPS sont les suivantes. Intégrons d’abord la continuation dans le corps de la
fonction :
(define (iotak n k)
(k (if (= n 0)
’()
(cons n (iota (- n 1))))))
136
CHAPITRE 9. CONTINUATIONS
La forme initiale du if est simple. On peut donc appliquer la continuation aux formes finales du if :
(define (iotak n k)
(if (= n 0)
(k ’())
(k (cons n (iota (- n 1))))))
La seconde forme du if est non simple, non finale. Elle devient :
(define (iotak n k)
(if (= n 0)
(k ’())
(iotak (- n 1) (lambda (u) (k (cons n u))))))
Considérons cet autre exemple :
(define remove
(lambda (s los)
(letrec
((loop
(lambda (los)
(if (null? los)
’()
(if (eq? s (car los))
(loop (cdr los))
(cons (car los) (loop (cdr los))))))))
(loop los))))
Une première transformation fait apparaı̂tre une continuation dans chaque lambda expression :
(define removek
(lambda (s los k)
(k (letrec
((loopk
(lambda (los k)
(k (if (null? los)
’()
(if (eq? s (car los))
(loop (cdr los))
(cons (car los) (loop (cdr los)))))))))
(loop los)))))
La forme initiale du letrec (qui est, pour la transformation CPS , assimilable au let) est une lambda expression,
donc simple. La continuation peut être directement appliquée au corps de la forme, c’est à dire l’appel de loop :
(define removek
(lambda (s los k)
(letrec
((loopk
(lambda (los k)
(k (if (null? los)
’()
(if (eq? s (car los))
(loop (cdr los))
(cons (car los) (loop (cdr los)))))))))
(k (loop los)))))
Ce corps devient donc :
(loop los k)
9.3. TRANSFORMATION CPS
137
Nous pouvons maintenant nous intéresser à la forme lambda interne. L’expression initiale du if auquel la
continuation est appliquée est simple. Il suffit donc d’appliquer cette continuation aux deux formes finales du
if :
(define removek
(lambda (s los k)
(letrec
((loopk
(lambda (los k)
(if (null? los)
(k ’())
(k (if (eq? s (car los))
(loop (cdr los))
(cons (car los) (loop (cdr los)))))))))
(loopk los k))))
Là encore, la continuation peut être appliquée aux deux formes finale du if interne :
(define removek
(lambda (s los k)
(letrec
((loopk
(lambda (los k)
(if (null? los)
(k ’())
(if (eq? s (car los))
(k (loop (cdr los)))
(k (cons (car los) (loop (cdr los)))))))))
(loopk los k))))
La transformation de la première forme est évidente : il suffit d’intégrer la continuation à l’appel de la forme loop.
La seconde forme nécessite de faire apparaı̂tre le contexte sous la forme d’une lambda expression. L’écriture
finale de notre fonction est ainsi :
(define removek
(lambda (s los k)
(letrec
((loopk
(lambda (los
(if (null?
(k ’())
(if (eq?
(loopk
(loopk
k)
los)
s (car los))
(cdr los) k)
(cdr los) (lambda (v)
(k (cons (car los) v)))))))))
(loopk los k))))
9.3.4
Conflits de noms
La transformation CPS est une transformation textuelle. La création de nouveaux identificateurs, dans
les lambda expressions représentant les continuations, ou le déplacement d’expressions à l’intérieur du corps
d’autres formes qui déclarent des variables locales sont susceptibles de créer des conflits de noms. Une solution
consiste à renommer systématiquement les variables locales, par exemple par création explicite de nouveaux
identificateurs jamais utilisés dans le programme, par une forme telle que gensym ou assimilée.
9.3.5
Formes simples ou itératives
Par ailleurs, notre définition des formes simples est particulièrement restrictive. De nombreuses fonctions
définies n’utilisent pas de récursion, implicite ou explicite, ou pratiquent des appels récursifs terminaux, qui leur
confèrent un comportement itératif. Il n’est en fait pas nécessaire d’appliquer la transformation CPS à de telles
138
CHAPITRE 9. CONTINUATIONS
fonctions, qui ne peuvent en aucune façon créer un comportement récursif des programmes dans lesquels elles
apparaissent.
Une analyse du graphe des appels permet de détecter les procédures (ou les ensembles de procédures) qui ne
peuvent pas créer de comportement récursif, afin d’éviter de leur appliquer la conversion CPS : ces procédures
sont traı̂tées comme les fonctions primitives.
9.3.5.1
Exemple
Considérons l’expression suivante :
(letrec ((p1 (lambda (n)
(list (p3 n))))
(p2 (lambda (n)
(- n 1)))
(p3 (lambda (n)
(if (p4 n) 1 (* n (p5 n)))))
(p4 (lambda (n)
(zero? n)))
(p5 (lambda (n)
(p3 (p2 n)))))
(p1 5))
Convertissez-la en forme CPS par l’algorithme général. Tentez ensuite de détecter les fonctions qui ne peuvent
en rien introduire un comportement récursif, et appliquer l’algorithme de conversion CPS , en considérant cette
fois ces fonctions particulières comme des primitives.
9.3.6
Evaluateurs à passage de continuation
Les techniques que nous avons évoquées ci-dessus sont utilisées de manière effective dans les interprètes et
les compilateurs à passage de continuation. Des optimisations classiques consistent à restreindre le nombre de
cas dans lesquels il est nécessaire de construire la continuation, ou à représenter celles-ci par des structures
de données particulières. Le sujet est développé plus en détail dans [FWH89]. Voir aussi certains articles de
recherche, comme [Sen89], [FM90].
9.4
Echappement
Nous allons maintenant introduire une nouvelle notion, celle d’échappement . Un échappement est une procédure à un paramètre, qui fournit la valeur de ce paramètre, en oubliant son contexte. Soit escape une telle
procédure :
(escape 3)
=⇒ 3
(escape (+ 2 (* 3 5)))
=⇒
17
Dans ces deux premiers exemples, le contexte de l’appel de escape était assimilable à la procédure identité.
Voici un autre exemple, dans lequel le contexte est (* 3 ") :
(* 3 (escape (+ 2 (* 3 5))))
=⇒
17
Le résultat est le même que ci-dessus : le contexte a été purement et simplement ignoré.
Notre procédure d’échappement, toute simple qu’elle soit, nous permet de créer de nouvelles procédures
d’échappement. Ainsi, make-escape prend comme paramètre une fonction, et applique cette fonction sur la ou
les valeurs transmises à l’échappement :
(define (make-escape f)
(lambda x (escape (apply f x))))
=⇒ make-escape
(define (fun x) (* 3 x))
=⇒ fun
(define titi (make-escape fun))
=⇒ titi
(+ 2 (titi (* 2 2)))
=⇒ 12
(define toto (make-escape *))
=⇒ toto
(+ 3 (toto 2 2 2))
=⇒ 8
9.5. LA FORME CALL-WITH-CURRENT-CONTINUATION
139
Les systèmes Lisp, et Scheme, proposent des fonctions comme error, permettant d’interrompre l’exécution
en cours, et de signaler une erreur.3 Voici une telle fonction :
(define erreur (make-escape
(lambda x (display "Erreur") x )))
=⇒ erreur
(+ 2 (erreur "test"))
=⇒ Erreur("test")
(list 4 (display "toto")
(erreur "essai") (display "titi"))
=⇒ titiErreur("essai")
(Ce dernier exemple donne une indication sur l’ordre d’évaluation des expressions : titi est imprimé, l’exécution
se fait donc de droite à gauche ; toto n’est pas imprimé, notre procédure a donc bien interrompu le traitement
de la forme list.)
9.5
La forme call-with-current-continuation
La combinaison des deux notions de contexte et d’échappement nous fournit un outil puissant. En effet, la
transformation CPS permet de transformer une fonction quelconque en fonction à récursivité terminale, dans
laquelle la continuation apparaı̂t explicitement. Obtenir une procédure d’échappement dans un tel système
consiste à introduire simplement une nouvelle forme spéciale, escape, dont la transformation CPS est définie
par :
(k (escape E))
=⇒
E
Si la procédure identiték peut se noter :
identiték
⇔
(lambda (v k) (k v))
la procédure d’échappement escapek peut, elle, s’écrire :
escapek
⇔
(lambda (v k) v)
Cet outil n’est pas lié spécifiquement au langage Scheme. Tout langage fonctionnel dans lequel les fonctions
sont de première classe autorise la programmation en style CPS , et l’utilisation de procédures d’échappement.
Cependant, la programmation explicite dans ce style particulier est assez lourde. Même s’il est possible
d’automatiser la transformation CPS , il est plus simple de rendre les continuations directement accessibles dans
le langage.
Nous pouvons maintenant introduire call-with-current-continuation, la fonction clef des opérations sur
continuations, dont le nom est souvent abrégé en call/cc.
La forme call-with-current-continuation est une procédure à un argument. Cet argument est lui-même
une procedure à un argument. L’expression :
(call-with-current-continuation fun)
est équivalente à :
(fun -cont .)
dans laquelle -cont . est la continuation de la forme call/cc. Cette continuation est équivalente à l’application
de notre forme make-escape sur le contexte de la forme call/cc.
Considérons l’expression suivante :
(cons 2 (* 3 (call/cc fun)))
3 La fonction error fournie par MIT-Scheme est peu standard, car elle repose sur l’utilisation d’extensions propres à ce dialecte,
en particulier le système de traitement des erreurs. L’un des intérêts d’un tel système va être de permettre la prise en compte des
erreurs signalées par appel de error, mais aussi des erreurs détectées par les procédures primitives. Une fonction définie qui détecte
une situation anormale peut ainsi se contenter de signaler l’erreur par appel à error, sans qu’il soit nécessaire de générer un code
de retour particulier, ni, dans la fonction appelante, de tester systématiquement un tel code, qui ne doit être rendu que dans une
situation exceptionnelle. C’est dans le gestionnaire d’erreur que l’on prendra en charge ces traitements, et, après analyse éventuelle
de l’environnement et de l’instruction erronnée, que l’on décidera ou non de poursuivre l’exécution en cours, en fournissant un
résultat approprié. Prenons un exemple concret : dans une certaine application, il est nécessaire que car et cdr, appliqués à la liste
vide, fournissent un résultat, et non une erreur. Au lieu d’utiliser des fonctions de remplacement de car et cdr, qui testeraient ce
cas particulier avant d’appliquer l’algorithme du système, nous utiliserons directement les fonctions natives, mais en prévoyant,
dans le gestionnaire d’erreurs, de rendre la valeur appropriée au lieu de signaler l’erreur dans ces cas particuliers.
140
CHAPITRE 9. CONTINUATIONS
Le contexte de la forme call/cc est une procédure, équivalente à celle produite par :
(lambda (") (cons 2 (* 3 ")))
L’appel est donc équivalent à :
(cons 2 (* 3 (fun (make-escape
(lambda (v) (cons 2 (* 3 v)))))))
Si notre fonction fun est de la forme :
(lambda (k) 7)
le résultat de notre expression sera :
(cons 2 (* 3 ((lambda (k) 7) (make-escape
(lambda (v) (cons 2 (* 3 v)))))))
soit encore :
(cons 2 (* 3 7))
c’est à dire la paire pointée (2 . 21). Lorsque la continuation n’est pas utilisée par la procédure appelée par
le call/cc, le résultat fourni par cette procédure devient le résultat de la forme call/cc.
Si notre fonction fun est de la forme :
(lambda (k) (+ 4 (k 2)))
nous obtenons cette fois :
(cons 2 (* 3 ((lambda (k) (+ 4 (k 2))) (make-escape
(lambda (v) (cons 2 (* 3 v)))))))
La procédure k construite par make-escape et transmise à la lambda expression est maintenant appliquée sur
la valeur 2. Le contexte courant :
(cons 2 (* 3 (+ 4 ")))
est donc abandoné, et le résultat est celui de :
((lambda (v) (cons 2 (* 3 v))) 2)
c’est à dire la paire pointée (2 . 6). En particulier, il n’y a pas eu exécution de la forme (+ 4 "). Tout se
passe comme si l’expression toute entière avait été remplacée par le calcul de
((lambda (v) (cons 2 (* 3 v))) 2)
Lorsque la continuation créée par un call/cc est utilisée, quel que soit le contexte, le calcul courant est
abandonné et la continuation exécutée à sa place.
Les continuations créées par call/cc sont des objets de première classe. Elles peuvent être affectées à des
variables, passées en paramètres à des fonctions, etc. Comme tout objet Scheme, leur persistance est illimitée.
Elles sont en pratique assimilables à des fonctions.
Notons bien que ce qui est sauvegardé par une continuation est le contexte, pas le reste du monde : les
variables globales, l’état du système restent inchangés. En voici un exemple :
1 ]=> (define xxx ’(a b c d e f g))
;Value: xxx
1 ]=> xxx
;Value: (a b c d e f g)
1 ]=> (begin
(set! xxx (cdr xxx))
(call-with-current-continuation
(lambda (w) (set! toto w)))
(set! xxx (cdr xxx))
’ok)
;Value: ok
1 ]=> xxx
;Value: (c d e f g)
9.5. LA FORME CALL-WITH-CURRENT-CONTINUATION
141
L’expression a affecté à xxx la valeur (cdr xxx), obtenu la continuation, sauvé celle-ci dans la variable toto,
affecté à nouveau à xxx la valeur de (cdr xxx), et enfin fourni comme résultat le symbole ok. La continuation
créée est équivalente à :
(lambda (") (begin " (set! xxx (cdr xxx)) ’ok))
Utilisons à présent cette continuation :
1 ]=> (toto #t)
;Value: ok
1 ]=> xxx
;Value: (d e f g)
1 ]=> (cons ’coucou (toto 5))
;Value: ok
1 ]=> xxx
;Value: (e f g)
Notons à nouveau qu’une continuation ainsi créée est un échappement. Dans le dernier exemple, le cons n’est
donc pas exécuté, et le résultat imprimé est simplement le symbole ok.
On trouve dans la litérature de multiples exemples, plus ou moins complexes, d’utilisations des continuations :
[HFW84], etc. Nous allons ici en donner quelques uns.
9.5.1
Quelques exemples
Le call/cc va nous permettre de définir un certain nombre de formes de contrôle secondaires. Pour simplifier
la discussion, posons les définitions suivantes :
receveur Le receveur est l’argument de la forme call/cc. C’est toujours une fonction à un paramètre.
continuation Le paramètre reçu par le receveur est une continuation. La continuation est une procédure d’échappement à un paramètre, qui applique à ce dernier la continuation de la forme call/cc initiale.
9.5.1.1
La forme escape
La procédure d’échappement escape décrite ci-dessus peut ainsi être définie par :
(define escape)
(call-with-current-continuation
(lambda (k) (set! escape k)))
L’utilisation de la forme lambda nous permet de matérialiser la continuation, qui est affectée par set! à la
variable globale escape définie antérieurement. La continuation ainsi capturée est le top-level , c’est à dire
la boucle d’évaluation de l’interprète. La fonction escape nous permet ainsi de sortir d’une suite quelconque
d’appels imbriqués de fonctions, en fournissant un résultat (ou un message d’erreur). Comme souvent, le receveur
est une fonction anonyme dont le seul but est de conserver la continuation pour un usage ultérieur.
9.5.1.2
Les sorties exceptionnelles
Une autre utilisation typique du call/cc est la sortie exceptionnelle de plusieurs blocs imbriqués.
Le programme suivant calcule le produit des nombres d’une liste :
(define (product-of list)
(if (pair? list)
(* (car list) (product-of (cdr list)))
1))
(product-of ’(2 3 5 7 8 4 3 9 3 2 2 4 3 2 1 3 1))
156764160
(product-of ’(2 3 5 7 8 4 3 9 0 1 3 2 2 4 3 2 3))
0
Cependant, ce programme n’est pas très satisfaisant en terme d’efficacité générale, car lorsqu’un nombre égal à 0
est rencontré, il continue malgré tout d’explorer la liste en profondeur, alors que le résultat est bien évidemment
nul. La version suivante s’interrompt dès qu’un zéro est rencontré :
(define (product-of list)
142
CHAPITRE 9. CONTINUATIONS
(if (pair? list)
(if (zero? (car list))
0
(* (car list) (product-of (cdr list))))
1))
Cependant, même s’il y a cette fois interruption, on peut se convaincre qu’il va quand même y avoir multiplication
des premiers nombres par 0 ! Optimiser plus avant nécessite un appel à la forme d’échappement :
(define (product-of list)
(define (pr list k)
(if (pair? list)
(if (zero? (car list))
(k 0)
(* (car list) (pr (cdr list) k)))
1))
(call-with-current-continuation
(lambda (k) (pr list k))))
De même, la recherche du premier nombre négatif d’un arbre quelconque peut s’écrire :
1 ]=> (define (1st-neg T)
(call-with-current-continuation (lambda (k)
(letrec ((search (lambda (s)
(if (pair? s)
(begin (search (car s))
(search (cdr s))
#f)
(if (and (number? s) (negative? s))
(k s)
#f)))))
(search T)))))
;Value: 1st-neg
1 ]=> (define tree ’(2 3 (5 6 (7 8)) 9 (10 -7 11)
12 (13 -14 (((17))) -8)))
;Value: tree
1 ]=> ( 1st-neg tree)
;Value: -7
9.6
Utilisation avancée des continuations
Nous allons maintenant aborder quelques exemples moins élémentaires d’utilisation des continuations.
9.6.1
Exemples
9.6.1.1
Catch et Throw
Le call/cc permet de définir des formes de contrôle équivalentes à celles des autres langages. Ainsi, CommonLisp ne dispose pas des continuations, mais de différentes formes de contrôle, dont l’une est le catch/throw . La
forme spéciale :
(catch -nom. -e1 . -e2 . ... -en .)
exécute séquentiellement les expressions -ei .. Le résultat de la forme est le résultat de la dernière expression
exécutée, -en .. Cependant, si au cours de l’exécution de l’une des -ei ., une forme throw est exécutée, l’exécution
du reste des expressions -ei . est abandonné, et le résultat de la forme throw est fourni comme résultat de la
forme catch de même nom (le nom des formes est le symbole qui apparaı̂t comme premier opérande de la
forme). La forme throw a elle-même pour syntaxe :
(throw -nom. -e1 . -e2 . ... -ep .)
Les expressions -ei . sont exécutées en séquence, et le résultat de -ep . est fourni comme résultat de la forme
throw — et donc de la forme catch.
9.6. UTILISATION AVANCÉE DES CONTINUATIONS
143
Voici une implantation possible des formes catch et throw.
1 ]=> (define-macro (catch nom . formes)
‘(call-with-current-continuation
(lambda (,nom) ,@formes)))
;Value: catch
1 ]=> (define-macro (throw nom . formes)
‘(,nom (begin ,@formes)))
;Value: throw
1 ]=> (begin
(display "message 1")(newline)
(catch toto
(display "message 2")(newline)
(catch titi
(display "message 3")(newline)
(throw toto
(display "message 4")(newline)
1)
(display "message 5")(newline)
2)
(display "message 6")(newline)
3))
message 1
message 2
message 3
message 4
;Value: 1
L’exemple montre que le throw a effectivement interrompu le traitement, en fournissant comme résultat la valeur
de la dernière expression du throw , 1.
Les formes catch et throw ainsi définies ont une portée lexicale : un throw n’est reconnu que lorsqu’il est
employé dans le contexte d’un catch de nom correspondant :
1 ]=> (define (essai) (display "essai") (newline)
(throw toto 7777))
;Value: essai
1 ]=> (catch toto
(display "message 1")(newline)
(essai)
(display "message 2")(newline))
message 1
essai
Unbound variable toto
2 Error->
Du fait de la portée lexicale, le nom toto utilisé à l’extérieur du catch fait référence à une liaison différente de
celle qui est créée dans le catch.
Pour obtenir l’effet escompté dans ce cas précis, il faut modifier la rêgle de portée des noms de ces formes.
Une solution consiste à utiliser le fluid-let pour permettre la manipulation de noms à portée universelle. Une
nouvelle version de la macro catch devient alors :4
1 ]=> (define-macro (catch nom . formes)
‘(call-with-current-continuation
(lambda (y2&%*@!3g)
(fluid-let ((,nom y2&%*@!3g))
,@formes))))
;Value: catch
1 ]=> (begin
4 On notera d’une part le nom de la variable locale de la forme lambda, y2&%*@!3g, astucieusement choisi pour éviter tout
conflit de nom, d’autre part que puisqu’il est fait usage d’une forme fluid-let, les variables concernées (ici toto et titi) doivent
préalablement posséder une valeur globale.
144
CHAPITRE 9. CONTINUATIONS
(display "message 1")(newline)
(catch toto
(display "message 2")(newline)
(catch titi
(display "message 3")(newline)
(throw toto
(display "message 4")(newline)
1)
(display "message 5")(newline)
2)
(display "message 6")(newline)
3))
message 1
message 2
message 3
message 4
;Value: 1
et l’on obtient cette fois le fonctionnement attendu :
1 ]=> (define (essai) (display "essai") (newline)
(throw toto 7777))
;Value: essai
1 ]=> (catch toto
(display "message 1")(newline)
(essai)
(display "message 2")(newline))
message 1
essai
;Value: 7777
9.6.1.2
While et Exit
Voici encore un exemple d’instruction de contrôle, la forme while.
1 ]=> (define-macro (while test . body)
‘(call-with-current-continuation
(lambda (*the-continuation*)
(letrec ((*the-while-body*
(lambda ()
(if ,test
(begin ,@body
(*the-while-body*))
#f))))
(*the-while-body*)))))
;Value: while
Un exemple de son utilisation :
1 ]=> (define truc 7.23)
;Value: truc
1 ]=> (while (> truc 1)
(set! truc (- truc (* 3 (/ truc 7))))
(display truc) (newline))
4.131428571428572
2.360816326530612
1.3490379008746354
.7708788004997916
;Value: ()
L’utilisation d’une continuation n’est pas utile dans ce cas précis. Elle n’est d’ailleurs pas invoquée en fonctionnement normal. Mais elle va nous permettre d’écrire une instruction de sortie exceptionnelle :
9.6. UTILISATION AVANCÉE DES CONTINUATIONS
145
1 ]=> (define-macro (exit)
’(*the-continuation* #f))
;Value: exit
1 ]=> (define truc 103.45)
;Value: truc
1 ]=> (while (> truc 1)
(if (and (> truc 50) (< truc 64)) (exit))
(set! truc (- truc (* 3 (/ truc 7))))
(display truc) (newline))
59.114285714285714
;Value: ()
9.6.1.3
Exercice : boucles nommées
Comment écririez-vous les formes named-while et exit-from, permettant d’identifier des boucles imbriquées
(le nom de la boucle suivant le mot-clef), et donc de sortir de plusieurs boucles imbriquées?
9.6.1.4
Exercice : make-walker revisité
Transformer la fonction make-walker pour qu’elle fournisse des fonctions travaillant grâce aux continuations,
par usage explicite de call/cc.
9.6.1.5
Quiz
Prenons aussi conscience que la manipulation des continuations n’est pas aussi simple qu’il y paraı̂t. En fait,
si la manipulation de macros est un véritable sport en soi, celle des continuations s’apparente carrément au
travail sans filet.
Considérons l’exemple suivant (on supposera que l’on a défini antérieurement :
1 ]=> (define call/cc call-with-current-continuation)
;Value: call/cc
afin de raccourcir les programmes...)
1 ]=> (define (restart cont) (cont cont))
;Value: restart
1 ]=> (define (test cont)
(display "debut") (newline)
(call/cc cont)
(display "suite") (newline)
(call/cc cont)
(display "fin") (newline))
;Value: test
Quel est le résultat de :
(test (call/cc restart))
Réfléchissez quelques minutes avant de vous jeter sur les explications. Non ! J’avais dit quelques minutes !
Enfin... Suivons les choses dans l’ordre. Cet appel crée une première continuation, -k1 ., équivalente à :
-k1 .
⇔
(lambda (") (test "))
La fonction restart reçoit cette continuation, et l’applique sur elle-même. La fonction test est donc exécutée, et son paramètre cont a comme valeur -k1 .. L’exécution se poursuit, et il y a impression de la chaı̂ne
"debut". L’expression suivante crée une nouvelle continuation, -k2 ., correspondant à :
-k2 .
⇔ (lambda (")
(display "suite") (newline)
(call/cc cont)
(display "fin") (newline))
146
CHAPITRE 9. CONTINUATIONS
puis cont, ici -k1 ., est appelée avec -k2 . comme paramètre. La fonction test est donc exécutée à nouveau, son
paramètre cont ayant cette fois comme valeur -k2 .. Il y à nouveau impression de la chaı̂ne "debut". L’expression
suivante crée une nouvelle continuation, -k3 ., correspondant elle aussi à :
-k3 .
⇔ (lambda (")
(display "suite") (newline)
(call/cc cont)
(display "fin") (newline))
puis cont, maintenant -k2 ., est appelée avec -k3 . comme paramètre. L’exécution de poursuit donc dans -k2 .,
avec l’impression de la chaı̂ne "suite", puis création d’une nouvelle continuation, -k4 ., correspondant à :
-k4 .
⇔ (lambda (")
(display "fin") (newline))
et appel de cont avec -k4 . comme paramètre. Mais de quel cont est-il question ? Nous sommes dans -k2 ., et
dans cet environnement, la variable locale cont a pour valeur -k1 .. Il y a donc à nouveau exécution de test,
avec cette fois -k4 . comme valeur affectée à cont. Cette exécution de test débute par l’impression de la chaı̂ne
"debut", puis la création d’une continuation -k5 ., correspondant à :
-k5 .
⇔ (lambda (")
(display "suite") (newline)
(call/cc cont)
(display "fin") (newline))
Il y a appel de cont avec -k5 . comme paramètre. Quel cont est-ce? C’est bien sûr -k4 . ! Celle-ci écrit "fin",
puis interrompt le cycle infernal. Pour résumer, on voit donc apparaı̂tre à l’écran :
1 ]=> (test (call/cc restart))
debut
debut
suite
debut
fin
;No value
9.6.1.6
Exercice : coroutines
On veut créer grâce aux continuations un mécanisme de gestion de coroutines. L’idée est simple : on capture
à un endroit d’un programme sa continuation, que l’on sauve dans une liste de ”processus” à exécuter. Deux
ou trois fonctions ou macros auxiliaires, et le tour est joué.
Joué?
Vraiment?
Examinons nos programmes.
Une première macro, init crée simplement une forme qui définit la variable globale *pqueue* qui représentera notre liste de processus en attente :
1 ]=> (define-macro (init)
’(begin
(define *pqueue* ’())))
;Value: init
Un dispatcher nous permet d’ajouter un nouveau ”process” dans la queue, et de donner la main au process qui
est en tête de liste. Le système s’interrompt quand la liste est vide. Le paramètre est soit une continuation (cas
”normal”), soit une fonction à un paramètre (initialisation d’un nouveau processus), soit encore la liste vide,
qui nous permettra, en fin de processus, de donner la main pour l’exécution des autres processus :
1 ]=> (define (dispatch proc)
(if (not (null? proc))
(set! *pqueue* (append *pqueue* (list proc))))
(if (not (null? *pqueue*))
9.6. UTILISATION AVANCÉE DES CONTINUATIONS
147
(let ((hdr (car *pqueue*)))
(set! *pqueue* (cdr *pqueue*))
(hdr #f))
(display "No more process\n")))
;Value: dispatch
Passer la main au processus suivant s’écrit tout simplement :
1 ]=> (define-macro (resume)
’(call-with-current-continuation dispatch))
;Value: resume
Créer un processus est tout aussi simple : on se contente de créer une fonction, contenant l’expression initiale,
et de l’introduire au bout de la liste des processus :
1 ]=> (define-macro (create . body)
‘(set! *pqueue* (append
*pqueue*
(list (lambda (*unused*)
,@body
(dispatch ’()))))))
;Value: create
Notons que l’on ajoute, à la fin de l’appel initial, l’appel (dispatch ’()) qui permettra, après l’achèvement
du processus, de relancer les autres processus encore en file d’attente.
Et enfin, pour faire joli, une interface ”friendly” pour lancer le tout :
1 ]=> (define (run) (dispatch ’()))
;Value: run
Allons-y :
1 ]=> (init)
;No value
1 ]=> *pqueue*
;Value: ()
1 ]=> (run)
No more process
;No value
1 ]=> *pqueue*
;Value: ()
Jusqu’ici ça, va. Essayons un processus :
1 ]=> (define (print x) (display x)(newline))
;Value: print
1 ]=> (define (test1)
(print "test1 - debut")
(resume)
(print "test1 - suite-1")
(resume)
(print "test1 - suite-2")
(resume)
(print "test1 - suite-3")
(resume)
(print "test1 - fin"))
;Value: test1
1 ]=> (create (test1))
;Value: ()
1 ]=> (run)
test1 - debut
test1 - suite-1
test1 - suite-2
test1 - suite-3
148
CHAPITRE 9. CONTINUATIONS
test1 - fin
No more process
;No value
Il semblerait que ceci fonctionne... Passons en grandeur réelle...
1 ]=> (define (test2)
(print "test2 - debut")
(resume)
(print "test2 - suite")
(resume)
(print "test2 - fin"))
;Value: test2
1 ]=> (define (test3)
(print "test3 - debut")
(resume)
(print "test3 - suite")
(resume)
(print "test3 - fin"))
;Value: test3
1 ]=> (create (test3))
;Value: ()
1 ]=> (create (test2))
;Value: (#[compound-procedure 2])
1 ]=> (create (test1))
;Value: (#[compound-procedure 2] #[compound-procedure 3])
Rien d’anormal : la forme set! fournit, dans MIT-Scheme, la valeur précédente de l’objet, ici *pqueue* ; on a
donc maintenant trois processus en attente du signal de départ :
1 ]=> *pqueue*
;Value: (#[compound-procedure 2] #[compound-procedure 3]
#[compound-procedure 4])
Allons-y :
1 ]=> (run)
test3 - debut
test2 - debut
test1 - debut
test3 - suite
test2 - suite
test1 - suite-1
test3 - fin
test2 - fin
test1 - suite-2
test1 - suite-3
test1 - fin
No more process
test2 - suite
test2 - fin
No more process
test3 - suite
test3 - fin
No more process
;No value
1 ]=> *pqueue*
;Value: ()
Ciel ! Que s’est-il passé?
Oui ! Que s’est-il passé? C’est là l’exercice. Comme quoi tout n’est pas simple dans la vie...
Conseil amical : le fait que l’on utilise des macros n’intervient en rien dans le phénomène. Si vous ne comprenez
pas “sur le papier”, tester l’exemple sur machine. Simplifier test1, test2, test3, pour mettre en évidence la
chose sur un cas moins compliqué. Combien faut-il créer de processus pour mettre en évidence le bug? Comment
corriger la fonction coupable pour obtenir ce que l’on souhaı̂tait?
Chapitre 10
Objets en Scheme
10.1
Introduction
Assez paradoxalement, le langage Lisp, et, a fortiori, le langage Scheme apparaissent peu fréquemment au
premier plan lorsqu’il est question de langages à objets. Pourtant, l’évolution de la programmation par objets
semble assez indissociable de celle du langage Lisp. Ainsi, le langage SmallTalk, premier langage objet “officiel”,
est-il né d’une volonté de mêler les concepts de Lisp et de Simula 67. De même, Scheme a d’abord été pour ses
auteurs, Guy L. Steele Jr. et Gerald Sussman, un outil d’étude de la notion d’acteur . Les premiers langages à
objets industriellement utilisés n’ont pas été SmallTalk ou C++, mais bien des extensions de Lisp, comme les
Flavours.
Nous ne décrirons pas dans ce chapitre les concepts des langages à objets. Le lecteur curieux trouvera dans
[MNC+ 89] une excellente synthèse du sujet. Nous montrerons tout d’abord comment réaliser rapidement une
petite extension du langage. Nous aborderons ensuite deux modèles objets et leur implantation en Scheme : le
modèle ObjVlisp, dû à Pierre Cointe, et le modèle CLOS, qui est l’extension objet de Common Lisp [Ste90].
Ces deux applications correspondent à des programmes Scheme non triviaux, qui nous permettront également
d’approfondir notre connaissance du langage et du système. Nous terminerons en passant en revue quelques
extensions à objets “industrielles” de Lisp ou Scheme, telles celles proposées par les langages Oaklisp ([PL92a],
[PL92b]), ou Dylan ([Sha92]).
10.2
Des classes en Scheme
Grâce aux caractéristiques assez exceptionnelles du langage (portée lexicale, persistance illimité, fonctions
de première classe), implanter une extension à objets en Scheme est trivial. Cela a été fait des centaines de fois
depuis la conception du langage. Allons y de la notre. Nous allons définir des classes, décrivant le comportement
de nos objets (variables d’instances, méthodes), classes permettant de construire des objets répondant à des
messages.
Notre langage n’utilisera pas d’héritage, il n’y aura pas de variables de classe, et nous utiliserons des formes
particulières pour créer les classes et envoyer des messages à des objets. En revanche, la conception, la réalisation
et la mise au point n’ont nécessité qu’une petite heure ! En voici les spécifications.
(newclass -nom. -variables. -methodes.)
permet la création d’une nouvelle classe. Le premier paramètre (qui n’est là qu’à titre indicatif) représente
le nom de la classe. -variables. est la liste, éventuellement vide, des variables d’instances. Enfin -methodes.
est la liste des méthodes de la classe. Chaque méthode est représentée par un couple nom/algorithme placé
entre parenthèses. Le nom de chaque méthode est arbitraire, et le corps des méthodes peut faire référence aux
variables d’instances. On désignera par *par* la liste des paramètres (éventuels) de la méthode, afin de pouvoir
y faire référence à l’intérieur de celle-ci.
Le résultat de newclass est une fonction, qui génère à chaque appel un nouveau représentant de la classe.
Les paramètres de cette fonction sont les valeurs initiales des variables d’instance. Ces valeurs sont à fournir
dans l’ordre des variables d’instances.
Une seconde opération, send, nous permettra, dans la grande tradition des extensions à objets de Lisp,
d’envoyer un message à un objet. La syntaxe sera :
(send -objet . -opération. -paramètres. ... )
149
150
CHAPITRE 10. OBJETS EN SCHEME
10.2.1
Un exemple d’utilisation
Nous allons montrer tout d’abord le fonctionnement de l’extension, avant d’aborder sa réalisation pratique.
Nous allons définir une classe dite “rectangle”, avec trois variables d’instance, les dimensions (longueur et
largeur), et la couleur, ainsi qu’un certain nombre de méthodes : calcul du périmètre, lecture et modification de
la couleur, etc. Voici un exemple de session :
1 ]=> (define rectangle
(newclass rectangle (longueur largeur couleur)
( (perimetre (* 2 (+ longueur largeur)))
(couleur couleur)
(repeindre (set! couleur (car *par*)))
(infos (list longueur largeur couleur)))))
;Value: rectangle
1 ]=> (define toto (rectangle 10 4 ’rouge))
;Value: toto
1 ]=> (send toto ’couleur)
;Value: rouge
1 ]=> (send toto ’perimetre)
;Value: 28
1 ]=> (send toto ’repeindre ’bleu)
;Value: rouge
1 ]=> (send toto ’classe)
;Value: rectangle
1 ]=> (send toto ’infos)
;Value: (10 4 bleu)
1 ]=> (send toto ’coucou)
;Value: (rectangle does-not-understand coucou)
La classe nouvellement créée est affectée à la variable rectangle. Cette classe est une fonction, capable
de créer de nouveaux objets qui ont leurs propres variables d’instance, mais partagent les mêmes méthodes.
L’exemple montre la création d’un rectangle, affecté à la variable toto, et l’envoi de quelques messages à cet
objet.
10.2.2
L’implantation
Il existe, on s’en doute, des dizaines1 de techniques permettant d’implanter des objets en Scheme. Nous
utiliserons celle qui est probablement la plus naturelle pour ce langage, la création des objets sous forme de
fermetures2 . Un objet sera donc une fonction, constituée d’une sélection (instruction case) permettant de choisir
la méthode à exécuter. Cette fonction sera créée à l’intérieur d’une forme permettant la définition de variables
locales (forme let ou lambda), cette forme étant elle-même construite à l’intérieur de la fonction représentant
la classe. Une implantation possible de la chose est la suivante :
1 ]=> (define-macro (newclass nom iv meths)
‘(lambda init
(apply
(lambda ,iv
(lambda (*method* . *par*)
(case *method*
,@(map (lambda (x) (cons (list (car x))
(cdr x)))
meths)
((classe) ’,nom)
(else (list ’,nom ’does-not-understand
*method*)))))
init)))
;Value: newclass
1 Ou
2 Il
en tout cas, des unités.
existe d’ailleurs un dicton qui affirme “Objects are poor man’s closures”.
10.3. LE MODÈLE OBJVLISP
151
Notons que l’on ajoute une méthode par défaut, classe, qui fournit le nom de la classe de l’objet, et enfin,
lorsque le message n’est pas prévu, que la fonction renvoie une liste représentant une erreur.
L’opération send est, dans ces conditions, réduite à sa plus simple expression : (send toto ’hello) est
équivalent à (toto ’hello).
1 ]=> (define (send obj . msg)
(apply obj msg))
;Value: send
10.2.3
Critique du modèle proposé
On peut adresser de nombreuses critiques à ce premier exemple, tant sur le fond que sur la forme. S’il
permet en effet la création de classes et d’instances de ces classes, il ignore tout de l’héritage, qu’il soit simple
ou multiple ; il ne cherche pas à intégrer ou unifier partiellement les objets définis et les entités primitives du
langage ; enfin, il manque de souplesse en ce qui concerne la redéfinitions des classes ou des méthodes. Plus grave
peut-être, les classes sont créées par une macro, et non par une fonction, ce qui interdit la définition dynamique
de nouvelles classes (les macros sont en effet exécutées à la compilation des instructions dans lesquelles elles
apparaissent). Enfin, une fois définies, classes et entités sont figées : il n’est plus possible d’introduire de nouvelles
variables d’instance, ou de créer ou de modifier les méthodes. La technique utilisée rend cette extension aussi
peu souple qu’Eiffel ou C++, ce qui n’est pas peu dire !
Aller beaucoup plus loin dans cette direction est technologiquement difficile. Les objets créés sont de taille
relativement importante : chaque instance contient une copie de toutes les méthodes de la classe. Si l’on veut
ajouter un héritage, ce qui est possible, il faut conserver une copie des expressions symboliques décrivant
les variables d’instances et les méthodes de chaque classe, et les concaténer aux variables d’instances et aux
méthodes des sous-classes. Là encore, une instance va contenir une copie des méthodes de ses classes et de toutes
ses super-classes ! Cette méthodologie de création d’objets se prête en fait bien mieux au modèle acteur qu’au
modèle objet classique.
Enfin, le modèle choisi n’est pas très satisfaisant : les classes sont statiques, les instances figées. Nous allons
pouvoir juger de l’importance d’une bonne conception du modèle sur les exemples qui vont suivre : ObjVlisp et
CLOS.
10.2.3.1
Exercice
Quelle modification simple peut-on introduire, pour que l’on puisse faire référence à l’objet à l’intérieur de
lui-même (par exemple sous le non self)? Peut-on transformer send en une macro?
10.3
Le modèle ObjVlisp
Dû à Pierre Cointe [Coi83], le modèle ObjVlisp unifie les concepts d’instance, de classe et de métaclasse :
tout objet est instance d’une classe. Afin d’éviter une régression infinie, la classe CLASS est instance d’elle-même,
et engendre les autres classes. Ce modèle est donc à la fois plus formel que celui de SmallTalk 80, dans lequel
les classes et leurs méta-classes sont des entités un peu à part, et en même temps plus souple.
Nous décrirons les différentes notions du modèle ObjVlisp au moyen du texte source d’une implantation en
Scheme, dûe à Pierre Cointe lui-même, et communiquée à l’auteur en 1986. On pourra la comparer avec la
version Le Lisp, fournie en annexe de [MNC+ 89].
10.3.1
Notions Initiales
Décrivons brièvement certaines caractéristiques du système MIT-Scheme, ici mises à profit, les notions
d’environnement et d’évaluation, dont la combinaison va être utilisée pour la réalisation de l’implantation.
10.3.1.1
Environnement
Un environnement est une matérialisation des liaisons (couples nom-valeurs) en effet à un instant précis de
l’exécution. Un environnement est un objet de première classe, qui contient de telles liaisons, associé éventuellement à un environnement parent.
MIT-Scheme connaı̂t initialement deux environnements : l’environnement système, dans lequel sont prédéfinies les fonctions et formes spéciales de l’interprète, et l’environnement utilisateur, initialement vide, dont
l’environnement système est un parent. Une variable ne peut être utilisée que si une liaison correspondante
est visible dans l’environnement courant, ou dans un environnement parent. La forme define crée (ou recrée)
152
CHAPITRE 10. OBJETS EN SCHEME
une liaison dans l’environnement courant. La forme set! modifie une liaison déja existante, et visible dans
l’environnement courant ou dans un environnement parent. Ex :
(define toto ’(1 2 3 4 5))
(define car cadr)
(car toto)
2
mais ce car, qui est actuellement le seul visible, est créé par define dans l’environnement utilisateur. Le car
de l’environnement système existe toujours, et peut même être référencé par la formule magique :
(environment-lookup system-global-environment car)
Différentes fonctions permettent de manipuler les environnements, d’explorer leur contenu, etc. Celles qui
nous intéressent sont ici :
(the-environment)
Cette fonction matérialise l’environnement courant sous la forme d’un objet de première classe.
(make-environment -e1 . -e2 . ... -en.)
est équivalente à :
(let () -e1 . -e2 . ... -en. (the-environment))
Un environnement local est créé par let, dans lequel les formes -e1 . -e2 . ... -en. sont évaluées, ce qui crée
éventuellement de nouvelles liaisons, puis l’environnement ainsi élaboré est capturé et fourni en résultat de la
forme.
On se reportera à [Han91b], chapitre 13, pages 149–152 pour plus de détails convernant les environnement.
10.3.1.2
Evaluation
L’autre caractéristique du langage qui est beaucoup mise à contribution dans l’implantation de Cointe est
l’évaluation d’expressions. Ce travail est réalisé par la forme eval, dont la syntaxe est :
(eval -expression. -environnement .)
et dont le but est d’évaluer la forme -expression. dans l’environnement spécifié.
Précisons la sémantique. eval est une fonction, c’est à dire que ses paramètres sont évalués dans l’environnement courant selon les algorithmes habituels, -expression. et -environnement . pouvant être des formes
Scheme quelconques. L’évaluation du premier paramètre doit fournir comme résultat une expression symbolique,
donc une liste représentant une expression Scheme. Le second paramètre doit fournir, par son évaluation, un
environnement. eval effectue alors l’évaluation de l’expression symbolique dans l’environnement spécifié.
Notons que la forme eval est excessivement coûteuse, car elle fait intervenir le mécanisme de compilation
sur l’expression à évaluer. Il est souvent possible de remplacer eval par, selon les cas, l’utilisation de apply, de
macros, ou de primitives, comme access permettant la manipulation d’environnements.
10.3.2
Présentation du modèle
CLASS est la méta-classe de toutes les classes du système, incluant elle-même et la classe OBJECT. OBJECT est
une superclasse de toutes les classes du système, sauf bien sûr elle-même.
Tout comme dans notre premier exemple, l’envoi de message s’effectue par la fonction send :
(send -objet . -opération. -paramètres. ... )
le paramètre -opération. est un symbole, les autres paramètres éventuels sont transmis à la méthode. Un objet
est également représenté par une fonction, et cette forme send est strictement équivalente à :
(-objet . -opération. -paramètres. ... )
Les champs d’une instance (variables d’instance) sont figés : ils sont construits par héritage à la création de
la classe et leur nombre ou leurs noms ne peuvent plus être modifiés. Par contre, les méthodes peuvent être
définies (ou redéfinies) dynamiquement, et leur héritage est dynamique. Une instance d’une classe bénéficie donc
immédiatement d’une méthode nouvellement implantée dans cette classe, ou dans une super classe de celle-ci.
10.3. LE MODÈLE OBJVLISP
10.3.3
153
Exemples d’utilisation
Chargeons tout d’abord le système :
1 ]=> (load "/pim/girardot/cours/objscheme.scm")
Loading "/pim/girardot/cours/objscheme.scm"
les-dictionnaires
le-germe
new
pour-tester-la-chose
pie
fini
-- done
;No value
Le chargement du système réalise la définition de deux classes, CLASS et OBJECT, et de leurs méthodes associées.
La création d’une classe s’effectue par l’envoi du message new à la classe CLASS :
1 ]=> (define LINK (send CLASS ’new ’OBJECT
’(head tail)
’( car
(lambda () (access head env))
cdr
(lambda () (access tail env))
see
(lambda () self)
print
(lambda () (cons (access head env)
(access tail env)))
test
(lambda () (send self ’car))
set-car (lambda (nv)
(send self ’rplaca nv)
(send self ’car))
set-cdr (lambda (nv)
(send self ’rplacd nv)
(send self ’cdr))
rplaca
(lambda (nv) (eval
(list ’set! ’head nv) env))
rplacd
(lambda (nv) (eval
(list ’set! ’tail nv) env))
)))
;Value: link
La création d’une nouvelle classe nécessite trois paramètres : le nom de la superclasse, ici OBJECT,3 la liste des
variables d’instance, et celle des méthodes.
La classe LINK ainsi définie construit l’équivalent des paires pointées. Les deux variables d’instance, head
et tail, représentent les deux champs de la cellule. Les méthodes quant à elles sont définies par des lambda
expressions, qui vont être créées dans l’environnement de la classe. Notons l’usage de la variable self, qui est
(ou sera, à l’exécution) liée à l’instance recevant le message. Cette variable permet en particulier l’autoréférence
d’une instance : la méthode set-car par exemple est réalisée au sein de l’instance par envoi de deux messages
à elle-même.
L’implantation choisie par Cointe impose un mode particulier d’accès aux variables d’instance. La lecture
de la variable d’instance toto s’effectue par la forme (access toto env), l’affectation à toto de la valeur de
l’expression exp se réalise par (set! (access toto env) exp). La forme access, primitive de bas niveau,
permet, comme son nom l’indique, l’accès à la valeur d’une variable dans un environnement : le premier paramètre n’est pas évalué, et désigne effectivement le symbole dont la valeur associée est recherchée. Cointe utilise
pour l’affectation l’expression (eval (list ’set! ’toto exp) env), sensiblement équivalente, mais beaucoup
moins efficace. Dans ces écritures, env représente un environnement.
Nous pouvons maintenant créer une instance de cette nouvelle classe et lui envoyer quelques messages :
1 ]=> (define pie (send LINK ’new 1 2))
;Value: pie
1 ]=> (send pie ’car)
;Value: 1
3 Nous utilisons ici des majuscules pour mettre en relief les noms des classes comme CLASS, OBJECT, bien que le système représente
en interne tous les symboles en lettres minuscules.
154
CHAPITRE 10. OBJETS EN SCHEME
1 ]=> (send pie ’see)
;Value: #[compound-procedure 2 self]
1 ]=> (send pie ’print)
;Value: (1 . 2)
1 ]=> (send pie ’test)
;Value: 1
1 ]=> (send pie ’cdr)
;Value: 2
1 ]=> (send pie ’set-car 35)
;Value: 35
1 ]=> (send pie ’print)
;Value: (35 . 2)
1 ]=> (send pie ’is)
;Value: #[compound-procedure 3 self]
1 ]=> (send pie ’hello)
Unbound variable hello
2 Error->
Le dernier exemple montre l’envoi d’un message non implanté, qui provoque une erreur. Par contre, le sélecteur
is n’a pas été défini dans la classe, mais est quand même reconnu : il correspond à une méthode héritée de la
classe OBJECT. Voici encore un exemple d’utilisation de méthode héritée :
1 ]=> (send pie ’inspect)
;Value: ((class #[compound-procedure 3 self])
(head 35) (tail 2))
1 ]=> (send LINK ’inspect)
;Value: ((class class) (super object)
(fields (head tail)) (methods #[environment 4]))
1 ]=> (send CLASS ’inspect)
;Value: ((class class) (super object)
(fields (super fields methods))
(methods #[environment 5]))
1 ]=> (send OBJECT ’inspect)
;Value: ((class class) (super ()) (fields ())
(methods #[environment 6]))
La méthode inspect permet de connaı̂tre les noms et les valeurs des différentes variables d’instance d’un objet.
Ainsi, tous les objets ont-ils une variable class qui désigne la classe dont ils sont instances. Cette classe est
soit un identificateur symbolique (cas de CLASS), soit la valeur de la classe elle-même, un objet, donc une
fonction. Les classes sont toutes instances de CLASS. Elles comportent donc les mêmes variables d’instances,
class qui désigne leur méta-classe, super, la super-classe, fields, la liste des variables d’instance des instances
de la classe, et methods, qui est l’environnement d’exécution associé à l’objet. Les noms des méthodes sont
eux-mêmes accessibles par le message menu envoyé à la classe :
1 ]=> (send CLASS ’menu)
;Value: (new methodfor)
1 ]=> (send OBJECT ’menu)
;Value: (is ? ?<- auto inspect pretty menu)
1 ]=> (send LINK ’menu)
;Value: (car cdr see print test set-car
set-cdr rplaca rplacd)
Montrons enfin le travail de l’héritage, en créant une nouvelle classe héritant de LINK :
1 ]=> (define SLINK (send CLASS ’new ’LINK ’(rest)
’(rest
(lambda () (access rest env)))))
;Value: slink
1 ]=> (define jfp (SLINK ’new 1 2 3))
;Value: jfp
1 ]=> (send jfp ’car)
;Value: 1
1 ]=> (send jfp ’rest)
;Value: 3
10.3. LE MODÈLE OBJVLISP
155
1 ]=> (send pie ’rest)
Unbound variable rest
2 Error->
La classe SLINK dispose d’une variable d’instance supplémentaire, rest, et d’une méthode supplémentaire
éponyme. Bien entendu, pie, qui est une instance de LINK, et non SLINK, ne bénéficie pas de cette nouvelle
méthode !
1 ]=> (send SLINK ’inspect)
;Value: ((class class) (super link)
(fields (head tail rest))
(methods #[environment 7]))
En revanche, SLINK a pour superclasse LINK, et son inspection montre que les instances de cette classe disposent
en effet des variables héritées, head et tail. Naturellement, l’envoi du message print provoque cette sortie :
1 ]=> (send jfp ’print)
;Value: (1 . 2)
puisque la méthode print n’a pas été redéfinie dans la sous-classe, et ne connaı̂t que head et tail.
10.3.4
L’implantation
Nous allons maintenant analyser rapidement l’implantation effective de ObjVlisp, objscheme.scm.
La macro make-environment-in permet la création d’un nouvel environnement, sans liaisons initiales, dont
parent est l’ancètre immédiat.
1 ]=> (define-macro (make-environment-in parent)
‘(eval ’(make-environment ()) ,parent))
;Value: make-environment-in
La variable script va être utilisée à différents endroits. Son exécution (par eval) va créer une fonction,
de nom self, dans l’environnement désiré. Cette fonction prendra comme premier argument, sel, un symbole,
qui désignera la méthode à appliquer. La valeur de cette méthode va être obtenue par évaluation du symbole
dans l’environnement contenant les méthodes, par la forme (eval sel dico-methods). Le reste des arguments
de self est rassemblé en une liste, args. La méthode obtenue sera immédiatement appliquée sur une liste
contenant comme premier élément la valeur de la fonction elle-même, comme second élément l’environnement
contenant les variables de l’instance courante, environnement disponible sous le nom dico-fields, et comme
autres éléments les arguments attendus par la méthode, et contenus dans args.4 Enfin, cette fonction, créée par
la forme letrec, afin de permettre les références récursive, est fournie comme résultat de la forme. L
1 ]=> (define script
’(letrec ((self (lambda (sel . args)
(apply (eval sel dico-methods)
(cons* self dico-fields args)))))
self))
;Value: script
La fonction make-instance est à usage “interne”. Elle assure la création de la fonction définie par script
dans un environnement de travail contenant les objets dico-fields et dico-methods.
1 ]=> (define (make-instance class-name
methods field vfield)
(eval script
(make-environment
(define dico-fields
(make-dico class-name field vfield))
(define dico-methods methods))))
;Value: make-instance
4 La fonction cons* est un cons multiple, (cons* #e1 $ #e2 $ ... #en-1 $ #en$) étant équivalent à (cons #e1 $ (cons #e2 $ (cons
... (cons #en-1 $ #en$)))...).
156
CHAPITRE 10. OBJETS EN SCHEME
Les fonctions suivantes assurent la création d’un “dictionnaire” (terme hérité de Smalltalk) de variables
d’instances :
1 ]=> (define (make-dico name f vf)
(eval
‘(make-environment ,@(make-dico1
(cons ’class f)
(cons name vf)))
()))
;Value: make-dico
1 ]=> (define (make-dico1 names values)
(cond
((null? names) ())
(else (cons ‘(define ,(car names) ’,(car values))
(make-dico1 (cdr names) (cdr values))))))
;Value: make-dico1
Un dictionnaire est donc un environnement. La fonction auxiliaire make-dico1 transforme une liste de noms
et de valeurs initiales en formes define, la première étant la variable class, systématiquement rajoutée, et
make-dico construit l’environnement correspondant.
La fonction scan-methods agit à la manière de map pour définir une liste couples noms/méthodes par des
appels à defmethod.
1 ]=> (define (scan-methods pkg pl)
; pl = (sel (lambda () ..) )
(cond
((null? pl) ())
(else (defmethod pkg (car pl) (cdr (cadr pl)))
(scan-methods pkg (cddr pl)))))
;Value: scan-methods
La fonction defmethod permet la création d’une méthode, dans l’environnement d’une classe. Les paramètres
désignent respectivement l’environnement de la classe, le sélecteur de la méthode, et le corps de la méthode
précédé de la liste des paramètres. La méthode est construite sous la forme d’une fonction, qui recevra, en
plus des paramètres éventuels de la méthode, la valeur de l’instance réceptrice, self, et de l’environnement de
travail de celle-ci, env. On a vu ci-dessus, lors de la description de la variable script, comment la méthode allait
effectivement être exécutée. On doit donc prévoir, dans le corps de la méthode, d’aller explicitement rechercher,
dans l’environnement spécifié, les valeurs des instances. Par contre, la méthode est définie une seule fois, dans
l’environnement des méthodes de la classe.
defmethod est appelée lors de la création d’une nouvelle classe, pour établir les méthodes correspondantes,
mais peut aussi être utilisée à tout moment pour ajouter de nouvelles méthodes à une classe déja existante.
1 ]=> (define (defmethod class sel valfn)
(eval
‘(define ,sel (lambda (self env ,@(car valfn))
,@(cdr valfn)))
class))
;Value: defmethod
La création des environnements initiaux des classes CLASS et OBJECT s’effectue “à la main”. L’environnement
de la classe OBJECT contient les méthodes prédéfinies, celui de CLASS étant initialement vide, mais héritant du
premier :
1 ]=> (define env-object
(make-environment
(define (is self env) (access class env))
(define (? self env field) (eval field env))
(define (?<- self env field nv)
(eval ‘(set! ,field ,nv) env))
(define (auto self env) self)
(define (inspect self env)
(environment-bindings env))
(define (pretty self env) (pp self))
10.3. LE MODÈLE OBJVLISP
157
(define (menu self env)
(if (environment-bound? env ’methods)
(map car (environment-bindings
(access methods env)))
’()))
))
;Value: env-object
1 ]=> (define env-class
(make-environment-in env-object))
;Value: env-class
1 ]=> env-class
;Value: #[environment 5]
1 ]=> env-object
;Value: #[environment 6]
Remarquons que les méthodes de la classe OBJECT sont créées “à la main”, et non par defmethod. Il est donc
nécessaire de prévoir explicitement self et env comme premiers paramètres. La méthode menu utilise deux
fonctions primitives d’accès à l’environnement : environment-bound? permet de savoir si une liaison correspondant à un symbole donné existe dans un environnement, environment-bindings fournit l’ensemble des liaisons
d’un environnement sous la forme d’une liste de couples noms/valeurs. Remarquons encore que l’environnement
des méthodes de la classe CLASS, env-class, est initialement vide, mais créé avec l’environnement env-object
comme parent. Les instances de CLASS héritent donc bien des méthodes de OBJECT.
La variable fields-class est utilisée ultérieurement lors de la création des méthodes. Elle contient la liste
des variables d’instance décrivant une classe, la variable class, automatiquement ajoutée, contenant la classe
de l’objet.
1 ]=> (define fields-class ’(super fields methods))
;Value: fields-class
Les deux classes initiales peuvent elle-mêmes être créées :
1 ]=> (define CLASS (make-instance ’CLASS
env-class
fields-class
‘(OBJECT ,fields-class ,env-class)))
;Value: class
1 ]=> (define OBJECT (make-instance ’CLASS
env-class
fields-class
‘(() () ,env-object)))
;Value: object
1 ]=> CLASS
;Value: #[compound-procedure 8 self]
1 ]=> OBJECT
;Value: #[compound-procedure 9 self]
Remarquons que le second paramètre de make-instance est env-class dans les deux cas : CLASS et OBJECT
sont des classes, et sont donc construites sur le modèle de CLASS. Leurs méthodes doivent travailler dans
l’environnement des méthodes de classe. Les caractéristiques des classes ainsi créée sont différentes : OBJECT n’a
pas de variables d’instances, et n’hérite d’aucune classe.
Il reste à définir la méthode effective de création d’un nouvel objet, travail que make-instance n’effectue
que partiellement :
(defmethod env-class ’new ’(values
; values -> (super fields methods)
(cond
((eq? self class)
(make-class
(eval ’(make-environment ())
(methods (eval (car values)
(the-environment))))
self
(car values)
158
CHAPITRE 10. OBJETS EN SCHEME
(append
(fields (eval (car values)
(the-environment)))
(cadr values))
(caddr values)))
(else
(make-instance self (self ’? ’methods)
(self ’? ’fields) values)))))
Enfin, la fonction auxiliaire make-class, utilisée par new pour la création d’objets qui sont instances de
CLASS elle-même :
(define (make-class env self super fields methods)
(scan-methods env methods)
(eval script
(make-environment
(define dico-fields
(make-dico ’class fields-class
(list super fields env)))
(define dico-methods (self ’? ’methods)))))
Terminons par quelques fonctions de service utilisées par les précédentes :
(define (fields name) (name ’? ’fields))
(define (supers name) (name ’? ’super))
(define (methods name) (name ’? ’methods))
(define (send fonc . args) (apply fonc args))
(defmethod env-class ’methodfor ’((method)
(pp (eval method (self ’? ’methods)))))
La dernière expression montre la création dynamique d’une nouvelle méthode pour CLASS qui utilise pp, le
pretty printer . La méthode est immédiatement accessible aux sous-classes :
1 ]=> (send CLASS ’methodfor ’menu)
(define (menu self env)
(and (environment-bound? env ’methods)
(map car (environment-bindings
(access methods env)))))
;No value
1 ]=> (send LINK ’methodfor ’set-cdr)
(define (set-cdr self env nv)
(self ’rplacd nv) (self ’cdr))
;No value
(Notons au passage l’optimisation de l’interprète, remplaçant la forme if de la méthode menu par une forme
and plus compacte.)
10.4
Le modèle CLOS
CLOS (Common Lisp Object System) est une intégration forte d’un mécanisme de programmation par
objets au sein du langage Common Lisp. CLOS ajoute à CL (Common Lisp) les concepts de classe et de
fonction générique.
Les notions relatives à CLOS sont développées dans un grand nombre de documents aisément accessibles.
[Ste90] contient un chapitre complet (près d’une centaine de pages) consacré à CLOS. L’utilisation de CLOS est
elle-même décrite dans [Kee89]. Citons aussi l’excellent polycopié de J.C. Royer [Roy91], la courte description
située dans [MNC+ 89], The Art of Metaobject Protocol [Gre91], dont les chapitres 5 et 6 sont accessibles par
ftp anonyme sur arisia.xerox.com, dans /pcl/mop, et enfin les nombreux articles disponibles dans la presse
spécialisée, tels [DG87], [BK88]. P. Dussud [Dus88] décrit comment le modèle CLOS a influencé la conception
matérielle de l’Explorer II de Texas Instruments. P. Cointe [Coi88] décrit une méthodologie de réalisation de
CLOS à partir d’ObjVlisp.
10.4. LE MODÈLE CLOS
10.4.1
159
Classes
Une classe détermine la structure et le comportement d’un ensemble d’objets, dits instances de cette classe.
Toute entité en CL est instance d’une classe. Les classes sont donc elles-même instances d’autres classes. La
classe de la classe d’un objet est dite métaclasse de cet objet. La métaclasse détermine les propriétés d’héritage
ainsi que la représentation des instances qui dépendent d’elle.
Les classes de CLOS se fondent harmonieusement avec les types de CL : toutes les classes sont des types,
et il existe, pour la plupart des types primitifs de CL, des classes associées. La notion de type, peu usitée dans
le langage, prend donc tout son sens lorsqu’elle est complétée par la notion de classe. Une classe peut avoir
plusieurs super-classes directes : l’héritage est multiple. La hiérarchie des classes (et des types) de CL forme un
graphe acyclique.
A chaque classe est associé une liste de précédences de classes, qui est un ensemble ordonné contenant la
classe et toutes ses superclasses, directes ou non, de la plus spécialisée à la moins spécialisée. Cet ordre intervient
lors de l’héritage et de la sélection des fonctions génériques
10.4.2
Fonction générique
Une fonction générique est une fonction dont le comportement dépend des classes des entités qui lui sont
transmises en paramètres. La notion de fonction générique recouvre les notions de fonction classique, et d’envoi
de message.
10.4.3
Objets de Common Lisp
CLOS est un ajoût réalisé après coup au langage Common Lisp. Il a donc été nécessaire de trouver, a
posteriori, un cadre cohérent pour les différents types d’entités qui coexistent en CL : objets primitifs, séquences
(listes et tableaux), tables, structures, et enfin classes et objets définis. CLOS a été voulu compatible, mais
aussi efficace, puissant, universel, souple, d’une grande malléabilité. Son protocole de méta-objets est donc
relativement complexe.
10.4.3.1
CLOS et les types primitifs
Le langage Common Lisp définissait un certain nombre d’objets primitifs, tels les entiers, les flottants, les
symboles, les chaı̂nes, les cons, etc. Dans le cadre de CLOS, des classes ont été associées à ces types primitifs,
et donc des listes de précédences, expliquant comment chaque classe hérite de ses superclasses. La table 10.1
décrit la liste de précédence des classes associées aux types prédéfinis.
Notons encore que le type nil est un sous-type de tous les autres. Il n’existe pas d’objet de type nil (mais
nil qui représente la liste vide est lui l’unique représentant du type null ). Enfin, nous avons signalé que certains
types de CL n’avaient pas de classe associée. Il est possible en effet de définir des types dérivés par union,
intersection ou spécialisation de types primitifs. Ainsi, (or integer float) est une spécification de type,
désignant l’ensemble des nombres qui sont soit entiers, soit flottants. De même, (vector float 100) désigne le
type des vecteurs de 100 nombres flottants. Enfin, (and integer (satisfies -test .)) est le type des entiers
qui satisfont au prédicat -test .. Cependant, des telles spécification de types (applicables également à des classes)
peuvent être utilisées pour spécialiser des méthodes de fonctions génériques.
Common Lisp disposait aussi d’un autre concept, celui de structure, correspondant (en gros) aux structures de
C ou aux enregistrements de Pascal. Les structures, et les constructeurs de structures, présentent de nombreuses
analogies avec les instances et les classes. Les structures ont donc été intégrées dans le modèle CLOS, en
bénéficiant d’un protocole particulier.
10.4.3.2
Hiérarchie des classes
Le tableau 10.2 décrit le sommet de la hiérarchie des classes. La classe t définit le comportement par défaut de
toutes les entités de CL. La classe standard-object, dont héritent toutes les entités créées par standard-class
fournit un comportement par défaut pour ces entités. De même, structure-class régit la création des objets de
type structure, et standard-structure leur comportement. Enfin, built-in-class décrit les objets primitifs.
Ces trois classes ont une super-classe commune, class.
Notons enfin que t est une instance de built-in-class, et que toutes les autres classes sont des instances
de standard-class.
10.4.4
Le protocole de méta-objets
L’implantation de CLOS est réalisée de manière effectives par les classes et les méthodes (qui sont décrites
en détail dans les différents documents précités). Cette accessibilité des méta-classes permet à l’utilisateur de
160
CHAPITRE 10. OBJETS EN SCHEME
Tab. 10.1 - Liste de Précédence des types prédéfinis
Type prédéfini
t
character
stream
readtable
function
pathname
package
hash-table
random-state
number
array
sequence
symbol
vector
list
bit-vector
string
cons
null
complex
float
rational
ratio
integer
Liste de Précédence
(t)
(character t)
(stream t)
(readtable t)
(function t)
(pathname t)
(package t)
(hash-table t)
(random-state t)
(number t)
(array t)
(sequence t)
(symbol t)
(vector array sequence t)
(list sequence t)
(bit-vector vector array sequence t)
(string vector array sequence t)
(cons list sequence t)
(null list sequence t)
(complex number t)
(float number t)
(rational number t)
(ratio rational number t)
(integer rational number t)
Tab. 10.2 - Hiérarchie des classes
t
standard-structure
standard-object
class
built-in-class
structure-class
standard-class
crééer de nouvelles méta-classes dans le système, tout en conservant le comportement correct d’objets existants,
et ceci sans perte d’efficacité des parties qui reposent sur le protocole prédéfini.
CLOS peut ainsi être décrit à trois niveaux :
– le premier correspond aux opérations permettant la création de classes et d’instances. Ces fonctions sont
suffisantes dans la plupart des cas pour réaliser une programmation par objet effective et efficace en CL.
– Le second niveau correspond aux opérations plus élémentaires qui implémentent effctivement les opérations
du premier niveau. Ces opérations de base agissent sur des entités anonymes. Il est possible de définir de
nouvelles fonctions, ou de nouvelles syntaxes pour la manipulation des classes et des objets.
– Le dernier niveau est celui du protocole des méta objets. Il permet un contrôle fin du comportement des
classes et de leurs instances. Il devient possible de modifier le comportement de l’héritage des caractéristiques des objets ou des méthodes.
161
10.4. LE MODÈLE CLOS
10.4.5
Niveau de base
10.4.5.1
Classes et Slots
Le coeur du noyeau de CLOS est la classe standard-class, définie sur le schéma suivant :
(defclass standard-class
(class)
;
;
(direct-superclasses ;
direct-slot
;
class-options
;
name))
;
une superclasse unique
4 slots
les super-classes directes
la liste des slots directs
la liste des options de classe
le nom de la classe
Les slots correspondent aux variables d’instance et de classe, en terminologie objet classique. Leur comportement
est lui-même décrit par une classe :
(defclass standard-slot-description
(slot-description)
; une superclasse unique
; 4 slots
(name
; le nom
initform
; l’expression d’initialisation
type
; les restrictions sur le type
allocation))
; le type d’allocation
Le champ allocation peut prendre les valeurs :instance ou :class. Dans le premier cas, le slot correspond à
une variable d’instance, propre à l’objet ; dans le second cas, c’est une variable de classe, partagée entre toutes
les instances. La classe hérite de slot-description, classe virtuelle qui rassemble les comportements communs
aux slots de classes et de structures. Elle définit d’autres propriétés d’un slot :
– :reader définit automatiquement une fonction générique de lecture de la valeur associée au slot.
– :writer définit automatiquement une fonction générique de modification de la valeur associée au slot.
– :accessor définit automatiquement une fonction de lecture de la valeur du slot, et d’écriture par
(setf (-accesseur . -instance.) valeur)
du slot.
– :initarg définit un paramètre d’initialisation du slot pour la fonction générique initialize-instance.
– :documentation est explicite.
Les options de classe permettent en particulier de spécifier pour la nouvelle classe une métaclasse différente
de standard-class.
Notons que les slots d’une instance, que des fonctions de lecture ou d’écriture génériques aient été déclarées ou non, sont toujours accessibles en lecture et en écriture par (slot-value -instance. -slot .) et (setf
(slot-value -instance. -slot .) -valeur .), où -slot . est une désignation de slot. Ceci peut apparaı̂tre a priori
comme une entorse au principe d’encapsulation des informations. Cependant, il existe des techniques pour rendre
inaccessibles les désignations de slots, et donc interdire la consultation ou la modification de leurs valeurs par
des programmes non autorisés.
10.4.5.2
Algorithme d’héritage
L’héritage étant multiple, CLOS spécifie explicitement un algorithme permettant d’obtenir la liste de précédence des classes. Lors de la création d’une nouvelle classe C héritant, dans l’ordre, des classes C1 , C2 ... Cn ,
on considère d’une part l’ensemble des relations :
C ≺ C1 , C1 ≺ C2 ..., Cn−1 ≺ Cn ,
dans lesquelles A ≺ B indique que la classe A doit apparaı̂tre avant la classe B dans la liste des précédences de
classes, d’autre part l’ensemble des relations Ci ≺ Cj issues du graphe d’héritage, dans lesquelles les Ci (et les
Cj ) sont des superclasses, directes ou non, de C. Soit R l’union de ces deux ensembles.
162
CHAPITRE 10. OBJETS EN SCHEME
Le principe consiste à sélectionner les classes les unes après les autres, en choisissant à chaque fois une classe
Cj , telle qu’il n’existe aucune Ci telle que Ci ≺ Cj dans R. La première classe sélectionnée selon cet algorithme
est C selon toute vraisemblance.
La classe sélectionnée est ajoutée au bout de la liste des précédences, initialement vide. On retire ensuite de
R toutes les relations Cj ≺ Ck dans lesquelles Cj est la classe qui vient d’être sélectionnée.
Si plusieurs classes sont candidates (plusieurs n’ont aucune sous-classes référencées dans notre ensemble de
relations), on choisi la classe qui a une sous-classe directe la plus à droite dans la liste.
Enfin, s’il n’est pas possible de sélectionner une classe, alors qu’il reste des relations, c’est qu’il y a un cycle
dans le graphe : l’héritage est imposible.
Considérons cet exemple traditionnel ([Ste90]) :
(defclass
(defclass
(defclass
(defclass
(defclass
(defclass
pie (apple cinnamon) ())
apple (fruit) ())
cinnamon (spice) ())
fruit (food) ())
spice (food) ())
food () ())
L’ensemble des classes à ordonner est :
S
⇔
{pie, apple, cinnamon, fruit, spice, food, standard-object, t}
Notre ensemble de relations est :
R
⇔
{pie≺apple, apple≺cinnamon,
cinnamon≺standard-object, apple≺fruit,
fruit≺standard-object, cinnamon≺spice,
spice≺standard-object, fruit≺food,
food≺standard-object, spice≺food, standard-object≺t}
La classe pie est la seule à ne pas avoir de sous-classe dans l’ensemble R. Elle est donc sélectionnée, ce qui nous
donne un premier élément de la liste des précédences :
L
⇔
{pie}
On obtient alors, en supprimant toutes les références à pie :
S
⇔ {apple, cinnamon, fruit, spice, food,
standard-object, t}
R
⇔
{apple≺cinnamon, cinnamon≺standard-object,
apple≺fruit, fruit≺standard-object,
cinnamon≺spice, spice≺standard-object,
fruit≺food, food≺standard-object,
spice≺food, standard-object≺t}
La classe apple est la seule à ne pas apparaı̂tre à droite d’une relation. C’est donc la suivante sélectionnée dans
la liste :
L
⇔ {pie, apple}
S
⇔ {cinnamon, fruit, spice, food,
standard-object, t}
R
⇔
{cinnamon≺standard-object,
fruit≺standard-object, cinnamon≺spice,
spice≺standard-object, fruit≺food,
food≺standard-object, spice≺food,
standard-object≺t}
Il y a cette fois deux classes candidates : cinnamon et fruit. Dans la liste déja ordonnée, pie est sous-classe
directe de cinnamon, et fruit est sous-classe directe de apple. Cette dernière apparaı̂t le plus à droite dans la
liste : fruit est donc sélectionnée.
L
S
⇔
⇔
{pie, apple, fruit}
{cinnamon, spice, food, standard-object, t}
10.4. LE MODÈLE CLOS
163
R
⇔
{cinnamon≺standard-object, cinnamon≺spice,
spice≺standard-object, food≺standard-object,
spice≺food, standard-object≺t}
En continuant de même, sous obtenons la liste finale :
L
⇔ {pie, apple,fruit, cinnamon, spice, food,
standard-object, t}
Notons que les ordonnancements obtenus peuvent être contradictoires entre différentes classes. Ainsi, à la
nouvelle classe pastry ainsi définie :
(defclass pastry (cinnamon apple) ())
correspond la liste :
(pastry cinnamon spice apple fruit food
standard-object t)
Par contre, il n’est pas possible de construire de nouvelle classe qui hérite à la fois de pie et de pastry.
10.4.5.3
Fonctions génériques
Les fonctions génériques se comportent comme des fonctions Lisp traditionnelles. Elles peuvent être appliquées, passées en paramètres, rendues comme résultat. Cependant, alors qu’une fonction ordinaire contient
un segment de code unique, qui est toujours exécuté lorsque la fonction est appelée, à une fonction générique
peuvent être associées plusieurs méthodes, dont le choix va être fait dynamiquement, en fonction des caractéristiques des paramètres d’appel de la fonction. Ce qui est effectivement exécuté, lors de l’appel d’une fonction
générique, est en fait une combinaison des méthodes applicables.
Une fonction générique peut être globale (créée par la forme defgeneric) ou locale (construite par une
forme spécialisée comme generic-flet ou generic-labels, similaires à let et letrec respectivement).
La syntaxe de defgeneric est la suivante :
(defgeneric -nom. -liste-de-paramètres.
-options.* -méthodes.*)
-nom. doit être un symbole, ou encore une liste (setf -nom.) (ce qui crée une fonction d’accès pour la forme
setf).. La -liste-de-paramètres. est classique, au sens de CL : il est possible de spécifier des paramètres optionnels
ou à mot-clef.
Les options permettent de modifier l’ordre de précédence des arguments (par défaut, de gauche à droite), la
combinaison des méthodes, les méta-classes à utiliser pour la fonction générique et les méthodes elles-même, ou
encore de documenter la fonction. Les méthodes peuvent être spécifiées directement dans la forme defgeneric,
et/ou ajoutées ultérieurement par defmethod.
Une méthode est définie par une liste de paramètres et un corps. Les paramètres peuvent comporter des
formes les spécialisant : un paramètre est soit un identificateur, -symbole., soit une liste (-symbole. -spécialisation.).
Le second élément, -spécialisation., peut être un nom de classe, ou encore une forme (eql -expression.), qui
indique que le paramètre doit être égal au sens de eql (sensiblement équivalent à eqv? en Scheme) au résultat
de -expression. pour que la méthode puisse être appliquée. Dans le premier cas, la méthode est appliquable si le
paramètre appartient à la classe spécifiée, ou à une sous-classe de celle-ci. Notons que -symbole. est équivalent
à la spécialisation (-symbole. t).
Lorsqu’une fonction générique est appliquée à un ensemble d’arguments, il y a tout d’abord sélection de
l’ensemble des méthodes applicables, c’est à dire des méthodes dont les spécialisations des paramètres formels
sont compatibles avec les types des paramètres effectifs. Les méthodes applicables sont ensuites classées, de la
plus spécifique à la moins spécifique. Ce classement tient compte de l’ordre de précédence des arguments et de
l’ordre total existant entre une classe et ses superclasses. Enfin, suivant le mode de combinaison des méthodes,
c’est la première méthode de la liste (méthode primaire), ou une certaine combinaison des méthodes, qui est
appliquée. Dans tous les cas, il est possible à une méthode de déclancher l’exécution de la méthode suivante de
la liste (un peu moins spécialisée, donc) par la fonction call-next-method.
164
CHAPITRE 10. OBJETS EN SCHEME
10.4.5.4
Instances
Les instances, enfin, sont créées par la fonction générique :
(make-instance -classe. -arguments.
Les arguments de la fonction sont utilisés pour initialiser les slots de l’instance ainsi créée, selon le protocole
propre à la méta-classe de l’instance. Il y a peu à dire sur la fonction elle-même, car les choix possibles dépendent
d’options liées à la classe.
Notons qu’il est possible de définir des méthodes permettant de réactualiser des instances existantes lorsque
les classes dont elles dépendent sont modifiées. Ces méthodes sont exécutées automatiquement lors de l’utilisation
d’une instance dont la classe génératrice a été modifiée.
10.5
CLOS en Scheme
Les principales notions de CLOS sont accessibles en Scheme grâce à une implantation, Tiny-CLOS , réalisée par Gregor Kiczales à Xerox PARC, placée dans le domaine publique, et accessible par ftp anonyme sur
parcftp.xerox.com, dans /pub/mops.
Tiny-CLOS propose les traits suivants :
– Classes, avec slots d’instance, mais pas d’options de création de slots.
– Héritage multiple.
– Fonctions génériques, avec méthodes multiples, mais spécialisation sur classe seulement.
– Pas de combinaison de méthodes : méthodes primaires et call-next-method.
– Classes, fonctions génériques et méthodes sont des entités de première classe.
Voici un exemple de session réalisé avec Tiny-CLOS :
kiwi.emse.fr% scheme
Scheme Microcode Version 11.59
MIT Scheme running under SunOS
Type ‘^C’ (control-C) followed by ‘H’ to obtain
information about interrupts.
Scheme saved on Tuesday December 11, 1990 at 7:04:22 PM
Release 7.1.0 (beta)
Microcode 11.59
Runtime 14.104
SF 4.15
1 ]=> (load "/pim/girardot/cours/tiny-clos.scm")
Loading "/pim/girardot/cours/tiny-clos.scm"
Loading "/pim/girardot/cours/support.scm"
-- done
-- done
;Value: tiny-clos-up-and-running
1 ]=> (define <pos>)
;Value: <pos>
1 ]=> (define pos-x (make-generic))
;Value: pos-x
1 ]=> (define pos-y (make-generic))
;Value: pos-y
1 ]=> (define move (make-generic))
;Value: move
1 ]=> (let ((x (vector ’x))
(y (vector ’y)))
(set! <pos> (make <class>
’direct-supers (list <object>)
’direct-slots (list x y)))
(add-method pos-x
(make-method (list <pos>)
10.6. AUTRES EXTENSIONS À OBJETS
165
(lambda (call-next-method pos)
(slot-ref pos x))))
(add-method pos-y
(make-method (list <pos>)
(lambda (call-next-method pos)
(slot-ref pos y))))
(add-method move
(make-method (list <pos>)
(lambda (call-next-method pos new-x new-y)
(slot-set! pos x new-x)
(slot-set! pos y new-y))))
(add-method initialize
(make-method (list <pos>)
(lambda (call-next-method pos initargs)
(move pos (getl initargs ’x 0)
(getl initargs ’y 0)))))
)
;No value
1 ]=> (define p3 (make <pos> ’x 1 ’y 2))
;Value: p3
1 ]=> (define p4 (make <pos> ’x 3 ’y 5))
;Value: p4
1 ]=> (pos-x p3)
;Value: 1
1 ]=> (pos-x p4)
;Value: 3
1 ]=> (pos-x 25)
Illegal datum in first argument position ()
within procedure #[primitive-procedure cdr]
There is no environment available;
using the current REPL environment
;Package: (user)
2 Error-> ^G
Quit!
1 ]=> (move p4 100 125)
;No value
1 ]=> (pos-x p4)
;Value: 100
1 ]=>
End of input stream reached
Moriturus te saluto.
kiwi.emse.fr%
On définit ici une classe, <pos>, et trois fonctions génériques pos-x, pos-y et move. La création de la classe
elle-même s’effectue dans un environnement local, dans lequel x et y désignent deux vecteurs. Le but est ici de
créer des liaisons uniques pour x et y, les valeurs x et y désignant les slots des instances de <pos>.5
Une méthode est construite par make-method, qui prend comme paramètre une liste de “spécialiseurs” (ici
<pos> dans tous les cas) et une fonction, et fournit une méthode (un objet de la classe method . Cette méthode
est utilisée comme paramètre de add-method, qui va l’intégrer à la fonction générique spécifiée comme premier
paramètre.
Notons qu’une méthode reçoit comme premier paramètre la méthode “suivante” qui sera utilisable sous la
forme (call-next-method), et comme autres paramètres ceux de la fonction générique.
10.6
Autres extensions à Objets
Nous avons fait allusion à d’autres extension à objets de Scheme. Voici quelques références.
5 En CLOS, les désignations de slots ne sont pas nécessairement des symboles, mais peuvent être n’importe quelle valeur ; c’est
ce que cet exemple veut démontrer.
166
10.6.1
CHAPITRE 10. OBJETS EN SCHEME
Oaklisp
Le langage Oaklisp ([PL92a], [PL92b]) est un dialecte de Scheme. Il propose une vision proche de celle de
CLOS, par les notions de classe et de fonction générique.
10.6.2
Dylan
Le langage Dylan [Sha92] a été conçu chez Apple Computer comme outil interne de développement et de
test de logiciels. Dylan reprend avec de petites variations les principes de Scheme, en adaptant et en simplifiant
les concepts de CLOS. Dylan est construit autour des concepts d’objects et de fonctions. Toutes les entités du
langage Dylan sont des objets, instances de classes. Il existe une hiérarchie entre les différentes classes, toutes
étant sous-classes, directes ou indirectes, de la classe <object>. Enfin, l’héritage est multiple.
Dylan distingue trois catégories de classes :
Classe Abstraite (Abstract Class) : Elle permet de définir des protocoles partagés par des sous-classes, mais ne
peut pas elle-même créer des instances.
Classe Instanciable (Instanciable Class) : Elle peut être utilisée pour créer des instances (par la fonction make),
et d’autres classes peuvent hériter de ses caractéristiques.
Classe Verrouillée (Sealed Class) : Elle permet de créer des instances, mais ne peut être utilisée comme superclasse pour d’autres classes.
Les Fonctions recouvrent les notions de fonctions, procédures, méthodes et messages des autres langages.
Dylan supporte deux types de fonctions : les fonctions génériques et les méthodes. Cette conception est très
voisine de celle des fonctions génériques de CLOS, le terme de méthode recouvrant ici les fonctions traditionnelles
d’un système Lisp.
Notons enfin que Dylan devrait être proposée avec un syntaxe infixe, proche d’Algol ou de Pascal, dans le
but avoué de raccoler les utiliseurs familiarisés avec ce type de langage.
Chapitre 11
Quelques aspects des systèmes Scheme
Scheme : un petit catalogue des concepts
Nous n’avons malheureusement fait qu’effleurer, dans ce cours, les principaux aspects du langage. Nous
allons consacrer ce dernier chapitre à analyser quelques aspects des systèmes Scheme.
11.1
Opérations d’entrées-sorties
Nous nous sommes peu intéressés jusqu’à présent aux opérations d’entrées-sorties. L’une des raisons est que
les systèmes Scheme proposent la possibilité d’introduire en mode conversationnel des expressions du langage,
et d’obtenir un résultat imprimé sous une forme lisible. En outre, les opérations d’entrées-sorties n’ont rien
de fonctionnel, puisqu’elles se caractérisent justement par un effet de bord, consistant à lire ou écrire sur un
support physique.
Les opérations d’entrées-sorties mettent en jeu un type de donnée particulier, le port d’entrée-sortie, qui
désigne le médium particulier sur lequel s’effectuera la lecture ou l’écriture. Cf. [R4R90], § 6.10.
11.1.1
Ports d’entrée-sortie
Un port est un objet susceptible d’accepter ou de fournir des caractères. Un port peut-être utilisé pour des
entrées ou des sorties. Voici les opérations relatives aux ports :
input-port? Prédicat qui indique si son paramètre est un port utilisable pour réaliser des lectures.
output-port? Prédicat qui indique si son paramètre est un port utilisable pour réaliser des écritures.
current-input-port Fournit le port courant utilisé en lecture par le système Scheme.
current-output-port Fournit le port courant utilisé en écriture par le système Scheme.
open-input-file Accepte comme paramètre une chaı̂ne de caractères, qui représente le nom d’un fichier, et
fournit un port susceptible de délivrer les caractères contenus dans ce fichier.
open-output-file Accepte comme paramètre une chaı̂ne de caractères, qui représente le nom d’un fichier qui
va être créé, et fournit un port susceptible de recevoir des caractères, qui vont être écrits dans le fichier.
close-input-port Ferme le fichier associé au port fourni en paramètre.
close-output-port Ferme le fichier associé au port fourni en paramètre.
Notons qu’un port n’est pas obligatoirement associé à un fichier : la plupart des implantations du langage
permettent d’associer des ports à des terminaux interactifs, des chaı̂nes de caractères, etc.
11.1.2
Lecture et Ecriture
Ces ports vont être utilisés pour réaliser des lectures ou des écritures. Ces opérations vont concerner des
caractères individuels, ou des objets Scheme. Les primitives qui opèrent sur des caractères sont les suivantes :
read-char Cette fonction réalise la lecture d’un caractère sur le port courant, ou, si un paramètre est précisé,
sur le port correspondant.
167
168
CHAPITRE 11. QUELQUES ASPECTS DES SYSTÈMES SCHEME
peek-char Cette fonction réalise la lecture d’un caractère sur le port courant, ou, si un paramètre est précisé,
sur le port correspondant. La différence avec read-char est qu’une nouvelle lecture sur le même port
fournira le même caractère : on peut considérer que le caractère est consulté, mais non consommé.
char-ready? Ce prédicat indique si un caractère est disponible sur le port courant, ou, si un paramètre est
précisé, sur le port correspondant. Cette primitive n’est pas bloquante sur un port interactif.
write-char Le premier paramètre doit être un caractère. Le second paramètre, optionnel, désigne le port sur
lequel s’effectuera l’opération, par défaut le port courant.
D’autres opérations font références à des objets Scheme, qui sont alors lus ou écrits sous une forme externe.
Les primitives standard sont :
read Cette procédure lit un objet Scheme sur le port précisé par son argument, ou, par défaut, sur le port
courant.
write Cette procédure écrit son premier paramètre, un objet Scheme, sur le port précisé par son second argument, ou, par défaut, sur le port courant. L’objet est écrit sous une forme susceptible d’être relue par la
procédure read.
display Cette procédure écrit son premier paramètre, un objet Scheme, sur le port précisé par son second
argument, ou, par défaut, sur le port courant. L’impression obtenue ne fait apparaitre les doubles quotes,
qui délimitent les chaı̂nes de caractères, les back-slashs, etc. Cette forme ne permet pas habituellement
une relecture par la fonction rerad.
newline La fonction permet le passage à la ligne suivante sur le port précisé par son argument, ou, par défaut,
sur le port courant.
11.1.3
Formatage
Scheme ne propose pas de manière standard des instructions de formatage de résultats (c’est à dire des outils
permettant de créer la représentation textuelle d’objets Scheme sous une forme plus élaborée que ce qui peut
être obtenu avec write ou display).
Cependant, il existe presque toujours une instruction format dans les systèmes Scheme, permettant l’édition
de résultat sous forme formattée. Cette fonction se retrouve également dans le langage Common-Lisp. Sa syntaxe
est la suivante :
(format -flot . -specif . -par1 . -par2 . ...)
Le premier paramètre, -flot ., précise le flot de sortie. Le second paramètre, -specif ., est une chaı̂ne de caractères
fournissant des indications sur les sorties à effectuer et sur la représentation des paramètres. Enfin, les paramètres
suivants, facultatifs, sont édités suivant les spécifications fournies par -specif ..
Le mode de travail de format est le suivant : les caractères successifs de -specif . sont imprimés (au sens
générique d’envoyés, générés) sur le flot de sortie, jusqu’à rencontre du caractère d’échappement tilde, ~. Celuici introduit une commande ou une spécification de format. La syntaxe générale d’une spécification est :
~[n{,n}∗ ][:][@]S
Les n représentent des indications optionnelles (on parlera de modificateurs), qui peuvent être des nombres
entiers (“4”, “-12”) ou des caractères notés ’c (“’$”, “’*”), séparés les uns des autres par des virgules. L’ordre
et le rang de ces indications sont significatifs. Si par exemple la commande attend trois modificateurs, et que
l’on ne souhaı̂te préciser que le troisième, celui-ci doit être précédé de deux virgules. Le caractère S précise la
commande, et peut éventuellement être précédé de “:” et/ou “@”. Les commandes représentées par une lettre
sont acceptées indifféremment en majuscule ou en minuscule. La signification des modificateurs de commande
dépend de la commande elle-même.
Les commandes typiquement reconnues par format sont les suivantes :
~A
: impression d’une expression symbolique en utilisant le même algorithme que display. La forme complète
de cette commande est :
~mincol,colinc,minpad,’padchar:@A
Les valeurs par défaut sont 0 , 1 , 0 et blanc respectivement. L’expression est imprimée sur une largeur
minimale de mincol caractères, cadrée à gauche (ou à droite si @ est spécifié), en utilisant padchar comme
caractère de remplissage pour obtenir la largeur désirée. Si minpad est différent de 0 , au moins minpad
caractères de remplissage seront ajoutés. Si colinc est différent de zero, la taille de l’ensemble sera ajustée
11.2. ASPECTS SYNTAXIQUES
169
pour être égale à un multiple de colinc. Enfin, si : est précisé, nil sera imprimé sous la forme () plutôt
que nil. Ex :
(format nil "~,5,,’$@A" ’(hello world))
=⇒
"$$$(helloworld)"
~S
: impression d’une expression symbolique en utilisant le même algorithme que write. La forme complète
de cette commande est :
~mincol,colinc,minpad,’padchar:@S
Les valeurs par défaut sont 0, 1, 0 et blanc respectivement. Hormis l’algorithme utilisé, le comportement
de la commande est strictement identique à celui de la spécification A à laquelle on se reportera.
~~
: imprime un ~ (autrement dit, un ~ doit être doublé).
~%
: effectue un passage à la ligne (par impression d’un "\n").
~-fin-de-ligne. : si le caractère ~ est suivi d’une fin de ligne (caractère "\n"), cette fin de ligne, ainsi que tous
les blancs qui peuvent se trouver à la suite sont ignorés. Ceci permet d’obtenir une présentation agréable
dans un fichier texte d’une instruction format comportant une description nécessitant plusieurs lignes.
11.2
Aspects syntaxiques
La syntaxe du langage Scheme est relativement simpliste. De nombreuses études ont été faites autour des
extensions syntaxiques possibles du langage. Nous avons déjà abordé les macros au § 7. En fait, la notion même
de macro définition, ou plus simplement macro, est relativement complexe. Mal comprise dans les langages de
programmation traditionnels (quelques essais avec le préprocesseur C suffisent à s’en convaincre), elle est, même
en Scheme, difficile à mettre en oeuvre.
Une partie de la difficulté est la nécessité de définir formellement la sémantique de cette notion de macro.
Pour ne citer qu’un aspect de la chose, considérons les environnements mis en oeuvre lors d’un usage de macro :
la définition de la macro est réalisée dans un premier contexte. La macro elle-même est utilisée dans un second
contexte, lors de la compilation de l’expression dans laquelle elle est utilisée. Cette utilisation de la macro
conduit à la construction d’une forme de remplacement, construction qui a lieu dans le contexte de la fonction
d’expansion de la macro, donc dans un troisième contexte. Enfin, l’évaluation de la forme ainsi générée se produit
ultérieurement, dans un quatrième contexte.
Les systèmes Scheme proposent parfois plusieurs systèmes différents de macros : un système traditionnel
(qui, en MIT-Scheme, est représenté par la forme define-macro), que l’on peut qualifier de bas niveau, et un
système de plus haut niveau, qui répond, au moins partiellement, à certains impératifs de sécurité.
11.2.1
Macros
Les macros incarnées par la forme define-macro correspondent aux macros des systèmes Lisp traditionnels.
Le principe consiste à reconnaı̂tre qu’une forme est spéciale (ceci peut être fait à la “lecture” d’un programme,
à sa “compilation”, ou lors de son “exécution”), et à remplacer le texte de cette forme spéciale par une autre
forme, construite pour la circonstance par une fonction spécialisée.
11.2.2
Forme Syntax
La forme syntax, qui est disponible dans PCScheme (cf. [HCW88], § 7.180), offre par certains points, une
approche de plus haut niveau que les macros classiques. Sa description est la suivante :
(syntax -modèle. -expansion.)
La forme -modèle. permet de définir la syntaxe de la nouvelle forme spéciale que nous allons introduire dans
le langage. Cette forme est une liste dont le premier élément deviendra le mot-clef permettant de caractériser
notre nouvelle construction. Les autres éléments de la liste peuvent être des symboles, ou d’autres listes ; la liste
peut être une liste pointée.
Lors d’une utilisation de la nouvelle construction, les composants de celle-ci seront unifiés avec le -modèle.
formel. La construction est alors remplacée, dans le programme source, par son -expansion., dans laquelle les
symboles apparaissant dans l’expansion sont remplacés par les valeurs des symboles homonymes du modèle. La
forme syntaxe définit donc des règles de réécriture de programmes.
Voici quelques exemples illustrant la forme syntax
170
CHAPITRE 11. QUELQUES ASPECTS DES SYSTÈMES SCHEME
[1]
[2]
[3]
[4]
(syntax
(syntax
(syntax
(syntax
(freeze . exp) (lambda () . exp))
(personne nom prenom age) (list (quote nom) (quote prenom) age))
(si e1 alors e2 sinon e3) (if e1 e2 e3))
(delay . exp) (cons #f (lambda () . exp)))
La première expression est une implantation possible (et probable) de la forme freeze. La syntaxe attendue est
(freeze -liste.)
dans laquelle -liste. est une liste d’expressions. La forme générée correspondante est une lambda-expression
contenant ces diverses expressions. Dans le second exemple, la forme personne permet de définir une liste
destinée à contenir trois informations : la tâche de la saisie du programme est simplifiée en ce sens que les deux
premières sont automatiquement quotées. La troisième forme propose une variante de if, permettant d’utiliser
les mot-clefs alors et sinon. La dernière définition propose une implantation possible de la forme delay : à la
lambda-expression contenant les expressions de la forme delay est associé un indicateur booléen, initialement
faux, indiquant que l’expression n’a pas encore été évaluée.
11.3
Structures de données
Les systèmes Scheme proposent habituellement, en plus des structures de données définies dans la norme,
d’autres structures, classiques, telles les tables hashées, ou moins classiques, telles les paires faibles.
11.3.1
Paires faibles
Une paire faible est une cellule dont le car peut référencer n’importe quel type d’objet, mais ne protège pas
celui-ci du garbage collector : si l’objet n’est pas référencé par une autre structure, il sera récupéré par le
gestionnaire de mémoire. Le cdr d’une paire faible se comporte cependant de manière habituelle.
Les fonctions suivantes s’appliquent aux paires faibles :
weak-car Fournit la valeur du car d’une paire faible. Le résultat est l’objet placé dans le car de la paire faible
lors de la création de celle-ci, ou #f si l’objet a été déja récupéré par le gestionnaire de mémoire.
weak-cdr Fournit la valeur du cdr d’une paire faible. La valeur du cdr d’une paire faible n’est jamais alérée
par le gestionnaire de mémoire.
weak-cons Fonction qui alloue une nouvelle paire faible. Le premier paramètre est référencé faiblement, alors
que le second est référencé normalement.
weak-pair? Prédicat qui indique si son paramètre est une paire faible.
11.3.2
Tables hachées
Les tables hashées, ou tables hashcodées, sont des structures de données permettant un accès rapide à des
éléments désignés par des noms, dits clefs.
Les tables hashcodées sont souvent paramétrables de diverses manières. La recherche des éléments peut s’effectuer au moyen des fonctions primitives eq?, eql?, eqv? ou equal?. Les couples peuvent aussi être référencés
au moyen de paires faibles, ce qui permet aux données d’être éventuellement récupérées par le gestionnaire de
mémoire. Ces diverses options sont accessibles (au moyen de mots clés) lors de la création de la table.
11.3.3
Listes circulaires
Les listes circulaires, enfin, ne constituent pas par elles-mêmes une structure de données particulière. Le
terme de liste circulaire sera utilisé pour désigner une structure à base de cellules qui n’est pas assimilable à
un arbre binaire. Une telle structure est constitué d’un ensemble connexe de cellules, dans lequel une cellule au
moins est référencée par deux champs (car ou cdr ) de cellules de la structure. Ainsi, les instructions suivantes
définissent une certaine structure, qui s’imprime sous la forme de la liste ((b c) a b c) :
1 ]=> (define x ’(a b c))
;Value: x
1 ]=> (define y (cons (cdr x) x))
;Value: y
1 ]=> y
;Value: ((b c) a b c)
11.4. STRUCTURES DE CONTRÔLES
11.4
171
Structures de contrôles
Nous avons déja développé la notion de continuation au chapitre 9. Ce type de contrôle permet de définir
diverses abstractions, telles la notion de retour arrière (backtracking), de coroutine, etc.
11.4.1
Engins
Certains systèmes Scheme, tel PCScheme, définissent la notion d’engin (engine). Un engin est une abstraction, qui permet la mise en oeuvre de préemption liée au temps. Un engin comporte un calcul, auquel est associée
une ressource en temps de calcul, dite métaphoriquement fuel . Le calcul associé à un engin peut s’achever avant
l’épuisement du carburant : l’engin fournit alors le résultat du calcul, et la quantité de carburant restante. Dans
le cas contraire (panne d’essence), il fournit en résultat un nouvel engin, qui, démarré, réalise la poursuite du
calcul. On se reportera à la description de la mise en oeuvre qui est décrite dans le manuel (cf. [HCW88], § 4.15).
11.4.2
Erreurs
Une autre notion importante est celle d’erreur . En MIT-Scheme, les erreurs sont des objets de première
classe.
11.5
Réalisation des systèmes
Les systèmes Scheme sont fournis sous forme d’interprètes ou de compilateurs..
Un exercice classique, dans un ouvrage consacré à Lisp ou à Scheme, est l’écriture d’un interprète ou d’un
compilateur. Nous n’allons pas faillir à la tradition, et développer un petit modèle d’un interprète.
Qu’est-ce qu’un interprète ? C’est un programme informatique, qui va réaliser un ensemble d’opérations
décrites sous forme symbolique. L’interprète lit et exécute immédiatement les opérations demandées. Une calculette “ordinaire” est un exemple typique d’interprète. Le système Scheme que nous utilisons travaille en mode
interprété. Par opposition, un compilateur est un programme informatique, qui va lire un ensemble d’opérations
décrites sous forme symbolique, et qui va construire un autre ensemble d’opérations, sémantiquement équivalent,
destiné à une machine particulière, qui sera, elle, capable de réaliser les opérations demandées.1
11.5.1
Scheme en Scheme
L’algorithme d’interprétation choisi respectera la syntaxe de Scheme, ou du moins un sous-ensemble de
celle-ci : les fonctions prédéfinies sont +, - et *, les mots-clef reconnus sont if et quote.
Notons au passage la liste des différents mots-clefs, et les références de leur définition dans le texte de ce
cours, données dans la table 11.1.
Il y a certaines case vides dans ce tableau, indiquant que les formes correspondantes n’ont pas été abordées
dans ce cours. Leur importance n’est pas capitale, et le lecteur intéressé pourra se reporter à [R4R90] pour plus
d’informations.
Notons encore que la plupart des systèmes Scheme ajoutent d’autres mots clefs à cette liste. C’est le cas
de MIT-Scheme : dans le paragraphe 6.6.3, par exemple, on a vu que access, et unbound? étaient des formes
spéciales. On consultera [Han91b] pour obtenir la liste et la signification de l’ensemble de ces mots clefs.
Le squelette de notre interprète est donné à la figure 11.2. Voici un exemple de son utilisation :
1 ]=> (interprete ’(+ 2 3))
;Value: 5
1 ]=> (interprete ’(if #t 2 5))
;Value: 2
1 La distinction entre un système travaillant en mode compilé et un système travaillant en mode interprété n’est pas si simpliste.
On peut imaginer que toute implantation d’un langage de programmation se compose d’un traducteur , qui va transformer la
forme externe du programme (un flot de caractères) en une représentation interne plus appropriée. Cette transformation a lieu une
fois pour toutes, lorsque le programme est soumis par l’utilisateur. Cette représentation interne va ultérieurement être exécutée,
éventuellement de manière répétitive. Nous parlons donc de compilateur lorsque cette représentation interne est très proche de
la machine, par exemple lorsque le traducteur génère du langage d’assemblage. Nous parlerons d’interprète si la représentation
interne des programmes est très éloignée du langage de la machine. Il faut alors un programme spécialisé, l’exécutant (on dit
parfois en anglais le run time), pour réaliser, grâce à l’ordinateur, les opérations décrites dans cette représentation interne (ou
langage intermédiaire). MIT-Scheme dispose d’un compilateur qui fournit un langage intermédiaire très proche de la machine,
et relativement performant. Ce compilateur n’a pas (encore) été porté dans la version disponible sur sun Sparc, et, comme dans
d’autres systèmes Scheme ou Lisp, l’exécution est réalisée en mode purement interprétée, à partir de la représentation interne des
programmes sources, qui est une liste. C’est une telle représentation, par exemple, qui est fournie par la fonction read.
172
CHAPITRE 11. QUELQUES ASPECTS DES SYSTÈMES SCHEME
Fig. 11.1 - Les mots clefs du langage Scheme
and
case
define
do
if
let
letrec
quasiquote
set!
unquote-splicing
3.5.2
6.5.2
2.6, 4.4.2.3
2.5
4.4.1
4.4.1
7.3.1
4.5.3
7.3.1
begin
cond
delay
else
lambda
let*
or
quote
unquote
=>
4.5.3
3.5.1
8.3.1
3.5.1
4.1.2
4.4.1
3.5.2
3.1
7.3.1
1 ]=> (interprete ’(* 3 (- 2)))
;Value: -6
1 ]=> (interprete ’(quote (* 2 3)))
;Value: (* 2 3)
Il serait simple, mais fastidieux, de décrire sous cette forme l’ensemble des primitives du langage. Une solution
plus agréable serait de conserver les noms et les valeurs des opérations de base dans une structure de données
(une liste associative, par exemple, cf. section 4.2.2).
La portion :
((symbol? exp)
(cond
((eq? exp ’+) +)
((eq? exp ’-) -)
((eq? exp ’*) *)
))
peut ainsi se réécrire sous la forme :
((symbol? exp)
(let ((value (assq exp fun-list)))
(if value
(cdr value)
’())))
Notre liste de fonctions peut donc se définir par :
(define fun-list (list
(cons ’+ +) (cons ’- -) (cons ’* *) (cons ’/ /)
(cons ’abs abs) (cons ’modulo modulo) .........))
dans laquelle chaque élément est un couple nom valeur.
11.5.2
Mise en place d’environnements
Cette liste associative que nous venons de définir est en fait assimilable à un environnement .
11.6
Gestion de la mémoire
Les variables et les objets tels que les paires, les vecteurs et les chaı̂nes dénotent implicitement des emplacements de la mémoire, ou des séquences d’emplacements. Une chaı̂ne, par exemple, dénote autant d’emplacements
qu’il y a de caractères dans la chaı̂ne. (Ces emplacements ne correspondent pas nécessairement à des mots de la
machine.) Une nouvelle valeur peut être écrite dans un tel emplacement au moyen de la procédure string-set!,
mais la chaı̂ne dénote toujours les mêmes emplacements qu’auparavant.
11.6. GESTION DE LA MÉMOIRE
173
Fig. 11.2 - Un interprète Scheme - niveau zéro !
(define (interprete exp)
(cond
((or (null? exp) (boolean? exp)
(number? exp) (string? exp)) exp)
((symbol? exp)
(cond
((eq? exp ’+) +)
((eq? exp ’-) -)
((eq? exp ’*) *)
))
((list? exp)
(cond
((eq? (car exp) ’if)
(if (interprete (car (cdr exp)))
(interprete (car (cdr (cdr exp))))
(interprete (car (cdr (cdr (cdr exp)))))))
((eq? (car exp) ’quote)
(cadr exp))
(else
(apply
(interprete (car exp))
(map interprete (cdr exp))))))))
Chaque emplacement de la mémoire est marqué pour indiquer qu’il est en cours d’utilisation. Aucune variable, aucun objet ne font jamais référence à un emplacement inutilisé. Lorsque ce rapport parle d’emplacement
dans la mémoire ayant été alloué à une variable ou à un objet, celà signifie qu’un nombre approprié d’emplacements ont été choisis parmi ceux qui étaient inutilisés, et que ces emplacements ont été marqués pour indiquer
qu’ils sont dorénavant utilisés, avant que la variable ou l’objet n’y fasse effectivement référence.
Ces emplacement en mémoire sont alloués pour représenter le résultat de certaines procédures. Un exemple
typique est celui de la fonction cons : tout appel de celle-ci va créer une nouvelle paire pointée.
Tous les objets créés lors de l’exécution d’un calcul Scheme, y compris les procédures et les continuations,
ont une persistance illimitée. Aucun objet Scheme n’est jamais détruit. La raison pour laquelle les implantations
de Scheme ne tombent pas (habituellement!) à cours de mémoire est qu’elles sont autorisées à libérer un objet
si elles peuvent prouver que cet objet ne sera plus jamais utilisé par un calcul ultérieur.
En pratique, on peut imaginer qu’une partie de la mémoire centrale allouée au système Scheme est utilisée
pour constituer une liste de cellules disponibles, dite liste libre. Un appel de cons va fournir un élément de cette
liste. Il est possible de modéliser ce processus sous la forme suivante :
(define *-liste-libre-* -algorithme du système.)
(define (cons a d)
(if (pair? *-liste-libre-*)
(let ((new-cell *-liste-libre-*))
(set! *-liste-libre-* (cdr *-liste-libre-*))
(set-car! new-cell a)
(set-cdr! new-cell d)
new-cell)
<else>))
Que se passe-t-il donc lorsqu’un appel de la fonction cons (ou d’une autre fonction tout autant consommatrice de
mémoire comme list, append, map, etc.) est effectué, et se heurte à un manque de mémoire, c’est à dire lorsque
la liste libre est vide? Un processus particulier se déclenche alors, le ramasse miettes, ou garbage collector , dit
parfois glaneur de cellules, bref le GC . Son rôle est, tel le chien du berger, de rassembler toutes les cellules
inutilisées en une nouvelle liste libre.
174
CHAPITRE 11. QUELQUES ASPECTS DES SYSTÈMES SCHEME
Le problème est bien sûr de découvrir les cellules inutilisées, ce qui est assez ardu. Par contre, il est assez
simple de découvrir les cellules utilisées, et, en appliquant le tiers exclu, d’en déduire les libres.
On utilise pour cela un indicateur spécial, réservé à cette seule fin dans chaque cellule de la mémoire. Une
première passe à travers la mémoire permet de donner à tous ces indicateurs la valeur “libre”.
On examine alors la liste des objets connus du système, c’est à dire les divers environnements qui sont été
créés au cours de la session (il suffit en fait de partir de l’environnement courant). Pour toutes les liaisons, il
suffit de marquer “occupées” les cellules correspondantes. Cet algorithme est de type recherche en arbre ; il
suffit de l’interrompre chaque fois que l’on tombe sur une cellule qui est déja marquée, pour éviter de se faire
prendre au piège des listes ciculaires.
A l’issue de cette seconde passe, toutes les cellules accessibles ont été marquées. Les autres sont donc libres. Il
ne reste plus qu’à traverser à nouveau toute la mémoire, en créant une nouvelle liste libre rassemblant toutes les
cellules inutilisée. Il est alors possible de reprendre l’exécution de la fonction cons interrompue, et de poursuivre
l’exécution du programme.
Cet algorithme est très simple, et son temps d’exécution proportionnel à la taille de la mémoire allouée
au système. Il est parfaitement adapté aux petites implantations du langage, dans lesquelles le nombre de
cellules est relativement faible (quelques centaines de milliers de cellules). Sur de gros systèmes, cette phase de
recouvrement des cellules inutilisées peut être assez longue. En particulier, elle peut provoquer un arrêt notable
lorsque le programme dialogue avec un utilisateur. Le phénomène peut même être rédibitoire dans le cas d’un
système temps réel dont on attend des temps de réaction très brefs.
De très nombreux algorithmes ont été conçus pour la gestion de la mémoire dans les systèmes Lisp, visant
à améliorer l’efficacité générale du processus (par exemple, en ne balayant que certaines parties bien choisies de
la mémoire), ou à permettre un fonctionnement synchrone du GC avec le programme en cours d’exécution. On
trouvera les références de ces algorithmes dans [?].
11.7
Exercices d’application
Interprète Scheme
Compléter l’embryon d’interprète Scheme décrit à la figure 11.2.
Compilateur Scheme
Réaliser, en vous inspirant de celui qui est décrit dans [Gir85], un compilateur pour le langage Scheme.
Bibliographie
[Agh86] Gull Agha. Actors : A Model of Concurrent Computation in Distributed Systems. MIT Press,
Cambridge, Mass., 1986.
[AH87] Gull Agha and Carl Hewitt. Actors : a conceptual Foundation for Concurrent Object-Oriented
Programming. In B. Shriver and P. Wegner, editors, Research Directions in Object-Oriented Programming. MIT Press, 1987.
[ASS85] Harold Abelson, Gerald Jay Sussman, and Julie Sussman. Structure and Interpretation of Computer
Programs. MIT Press, Cambridge, Mass., 1985.
[ASS89] Harold Abelson, Gerald Jay Sussman, and Julie Sussman. Structure et Interprétation des Programmes Informatiques. InterEditions, Paris, France, 1989.
[Bac78] John Backus. Can Programming be liberated from the von Neumann style? A functional style and
its algebra of programs. . Communications of the ACM, 21(8):613–641, 1978.
[Bar84] H.P. Barendregt. The Lambda Calculus, Its Syntax and Semantics. North Holland, Amsterdam,
1984.
[BK88] Daniel G. Bobrow and Gregor Kiczales. The Common Lisp Object System Metaobject Kernel, A
Status Report. In LITP Inria, editor, International Workshop on LISP Evolution and Standardization, pages 27–32. Afcet, Afnor, February 1988.
[BWW86] J. Backus, J.H. Williams, and E.L. Wimmers. FL language manual (preliminary version). Technical
Report RJ 5339 (54809), Computer Science, IBM Almaden Research Center, Almaden, CA, 1986.
[CH90] Guy Cousineau and Gérard Huet. The CAML Primer. Technical report, INRIA, Domaine de
Voluceau, Rocquencourt, B.P. 105, 78153 Le Chesnay, France, March 1990.
[Chu41] Alonzo Church. The Calculi of Lambda Conversion. Princeton University Press, Princeton, N.J.,
1941.
[Cli91a] William Clinger. Hygienic Macros Through Explicit Renaming. Lisp Pointers, IV(4), oct-dec 1991.
[Cli91b] William Clinger. Macros in Scheme. Lisp Pointers, IV(4), oct-dec 1991.
[Coi83] Pierre Cointe. A VLISP Implementation of SMALLTALK-76. In P. Degano and E. Sandwall,
editors, Interactive Integrated Computing Systems, pages 89–102. North-Holland, New-York, 1983.
[Coi88] Pierre Cointe. Towards the design of a CLOS Metaobject Kernel : ObjVlisp as a first layer. In
LITP Inria, editor, International Workshop on LISP Evolution and Standardization, pages 33–40.
Afcet, Afnor, February 1988.
[CR91] W. Clinger and J. Rees. Macros That Work. In Conference Record of the 18th Annual ACM
SIGACT-SIGPLAN Symposium on Principles of Programming Languages. ACM, January 1991.
[DG87] L.G. DeMichiel and R.P. Gabriel. The Common Lisp Object System : An Overview. In ECCOP’87
proceedings, volume 276, pages 151–170, Paris, France, June 1987. Spinger Verlag.
[Dus88] Patrick H. Dussud. Lisp Hardware Architecture : The Explorer II and Beyond. In LITP Inria,
editor, International Workshop on LISP Evolution and Standardization, pages 67–72. Afcet, Afnor,
February 1988.
[Dyb87] R.Kent Dybvig. The Scheme Programming Language. Prentice-Hall, INC., Englewood Cliffs, New
Jersey, 1987.
175
176
BIBLIOGRAPHIE
[FM90] P. Fradet and D. Le Métayer. Compilation de Programmes Fonctionnels par Transformation de
Programmes. Bigre, 4(69):123–133, Juillet 1990.
[FWH89] Daniel P. Friedman, Mitchell Wand, and Christopher T. Haynes. Essentials of Programming Languages. MIT Press, Cambridge, MA, 1989.
[Gir85] Jean-Jacques Girardot. Les Langages et Les Systèmes Lisp. EDITest, 1985.
[Gir92] Jean-Jacques Girardot. The Ctalk Programming Language : A Stategic Evolution of APL. APL
Quote-Quad, 32(1):78–87, 1992.
[Gir93] Jean-Jacques Girardot. Introduction à la Programmation par Objets. Technical report, Ecole des
Mines, 158 Cours Fauriel 42023 Saint-Etienne, 1993.
[GM88] Jean-Jacques Girardot and François Mireaux. Manuel de Référence APL 90. Technical report,
Ecole des Mines, 158 Cours Fauriel 42023 Saint-Etienne, 1988.
[GMM+ 78] M. Gordon, R. Milner, L. Morris, M. Newey, and C. Wadsworth. A meta-language for interactive
proff in LCF. In Conference Record of the 5th Annual ACM Symposium on Principles of Functional
Programming Languages, pages 119–130. ACM, 1978.
[Gon83] M. Gondran. Introduction aux systèmes experts. Bulletin de la Direction de Etudes et Recherches
d’E.D.F., Série C, Mathématiques Informatiques, (2), 1983.
[Gre91] Gregor Kiczales and Jim des Rivières and Daniel G. Bobrow. The Art of Metaobject Protocol. MIT
Press, Cambridge, Mass., 1991.
[Han91a] Chris Hanson. A Syntactic Closures Macro Facility. Lisp Pointers, IV(4), oct-dec 1991.
[Han91b] Chris Hanson. MIT Scheme Reference Manual. Technical report, Massachusetts Institute of Technology, Boston, Mass., 1991.
[Han91c] Chris Hanson. MIT Scheme User’s Manual. Technical report, Massachusetts Institute of Technology,
Boston, Mass., 1991.
[HC58] R. Feys H.B. Curry. Combinatory Logic, volume 1. North-Holland, The Netherlands, 1958.
[HCW88] David H.Bartley, John C.Jensen, and Donald W.Oxley. PC Scheme User’s Guide and Language
Reference Manual. the MIT Press, Cambridge, Massachussets, 1988.
[HF92] Paul Hudak and Joseph H. Fasel. A Gentle Introduction to Haskell. ACM Sigplan Notices, 27(5),
May 1992.
[HFW84] Christopher T. Haynes, Daniel P. Friedman, and Mitchell Wand. Continuations and Coroutines.
In Conference Record of the 1984 ACM Symposium on LISP and Functional Programming, pages
293–298. ACM, August 1984.
[HIMW90] R.K.W. Hui, K.E. Iverson, E.E. McDonnell, and A.T. Whitney.
20(4):192–200, 1990.
APL/?
APL Quote-Quad,
[HJe92] Paul Hudak, Simon Peyton Jones, and Philip Walder (editors). Report on the Programming Language Haskell. ACM Sigplan Notices, 27(5), May 1992.
[Hud89] Paul Hudak. Functional Programming Languages. ACM Computing Surveys, 21(3):359–411, 1989.
[HV92] Thérèse Accart Hardin and Véronique Donzeau-Gouge Viguié. Concepts et Outils de Programmation
— Le style fonctionnel, le style impératif avec CAML et Ada. InterEditions, Paris, France, 1992.
[Ive62] Kenneth E. Iverson. A Programming Language. Willey and sons, New-York, 1962.
[Kee89] Sonya E. Keene. Object-Oriented Programming in Common Lisp; A programmer’s Guide to CLOS.
Addison-Wesley, 1989.
[Kri90] J-L. Krivine. Lambda calcul: types et modèles. Masson, 1990.
[Kun88] John Kunz editor. Common Lisp, The Reference. Addison Wesley, Reading, MA, 1988.
[Lie87] Henry Lieberman. Reversible Object-Oriented Interpreters. In ECCOP’87 proceedings, volume 276,
pages 13–21, Paris, France, June 1987. Spinger Verlag.
177
BIBLIOGRAPHIE
[Mau91] Michel Mauny. Functional Programming Using CAML. Technical Report 129, INRIA, Domaine de
Voluceau, Rocquencourt, B.P. 105, 78153 Le Chesnay, France, Mai 1991.
[McC60] John McCarthy. Recursive functions of symbolic expressions and their computation by machine .
Communications ACM, 3(4):184–195, 1960.
[McD88] Eugene E. McDonnell. Life : Nasty, Brutish and Short . APL Quote-Quad, 18(2):242–247, 1988.
[Mic89] Greg Michaelson. An Introduction to Functional Programming Through Lambda Calculus. AddisonWesley, 1989.
[MNC+ 89] Gérald Masini, Amedeo Napoli, Dominique Colnet, Daniel Léonard, and Karl Tombre. Les Langages
à Objets. InterEditions, Paris, France, 1989.
[pF86] Daniel p.Friedman and Matthias Felleinsen. The Little LISPer. MIT Press, Cambridge, Mass.,
1986.
[PL92a] Barak Pearlmutter and Kevin Lang. The Oaklisp Implementation Guide. Technical report, Yale
University, NEC Research Institute, 11A Yale Station, New Haven, CT 06520 and 4 Independence
Way, Princeton, NJ 08540, 1992.
[PL92b] Barak Pearlmutter and Kevin Lang. The Oaklisp Language Manual. Technical report, Yale University, NEC Research Institute, 11A Yale Station, New Haven, CT 06520 and 4 Independence Way,
Princeton, NJ 08540, 1992.
[R4R90] R4RS. Revised
December 1990.
4
Report on the Algorithmic Language Scheme. ACM Sigplan Notices, 21(12),
[Ram92] John D. Ramsdell. An Operational Semantics for Scheme. Lisp Pointers, V(2), April 1992.
[Rea89] Chris Reade. Elements of Functional Programming. Addison-Wesley, 1989.
[Roy91] Jean-Claude Royer. Common Lisp Object System. Technical report, Faculté des Sciences et des
Techniques, 2 rue de la Houssinière, 44072 Nantes, 1991.
[Sen89] Nitsan Seniak. Compilation de Scheme par Spécialisation Explicite. Bigre, 3(65), Juillet 1989.
[SF92] George Springer and Daniel P. Friedman. Scheme and the Art of Programming. MIT Press, Cambridge, MA, 1992.
[Sha92] Andrew Shalit. Dylan, an object oriented dynamic language. Technical report, Apple Computer,
Inc., 20525 Mariani Avenue, Cupertino, CA 95014-6299, 1992.
[SJ91] Emmanuel Saint-James. Introduction du Traité de Programmation Applicative (de Lisp à l’assembleur en passant par le λ-calcul). Bigre, 5(73), Juin 1991.
[Ste90] Guy L. Steele Jr. Common Lisp, The language, 2nd edition. Digital Press, Digital Equipment
Corporation, 1990.
[Tur86] David Turner. An Overview of Miranda. ACM Sigplan Notices, 21(12), December 1986.
178
BIBLIOGRAPHIE
Index
L’entrée principale pour chaque terme, concept, procédure, ou mot-clef est indiquée en premier, séparée
des autres entrées par un point-virgule.
append 41; 68, 82
append! 82
append1 67
application 6; 7
application fonctionnelle 19
apply 51; 53
arbre 83
arc 84
arc cosinus 23
arc sinus 23
arc tangeante 23
arithmétique 22
arrondi 25
ASCII 18; 37
asin 23
assoc 52
assq 52
assv 52
atan 23
atome 18
19
! 60
" 18; 19
# 18; 19, 77
’ 32; 19
( 19
() 34; 35
) 19
* 22
+ 22
, 19
,@ 19
- 23
-> 78
-ci 34
. 19
/ 23
; 19
< 26
<= 26
= 26; 34
=> 171
> 26
>= 26
? 25
@ 19
\ 18
‘ 19
back-quote 19
Backus, John 6
Bartholdi 4
begin 60; 171
beta conversion 8
beta réduction 8
blanc 19
boolean? 37; 25
booléen 25; 18
bug 148
C++ 149
caaaar 35
caaadr 35
caaar 35
caadar 35
caaddr 35
caadr 35
caar 35
cadaar 35
cadadr 35
cadar 35
caddar 35
cadddr 35
caddr 35
cadr 35
call-with-current-continuation 139
call/cc 139
Cambridge 18
CAML 14
car 35; 82
abs 23
abstraction 7
access 97; 153
accolade 19
Ackerman 38
acos 23
acteur 17; 149
addition 22
Agha, Gull 17
aka 24
Algol 60 9; 54
algorithme d’héritage 161
alpha conversion 8
ancêtre 84
and 25; 63, 171
APL 11; 80
apostrophe 19
appel par nom 9
appel par valeur 9
179
180
caractère 18
case 93; 171
catch 142
cdaaar 35
cdaadr 35
cdaar 35
cdadar 35
cdaddr 35
cdadr 35
cdar 35
cddaar 35
cddadr 35
cddar 35
cdddar 35
cddddr 35
cdddr 35
cddr 35
cdr 35; 82
ceiling 24
cellule 34
chaı̂ne 71; 77
chaı̂ne de caractères 18
char-ready? 168
char? 37
chemin 84
Church, Alonzo 7
CLASS 153
classe 151
CLOS 149
close-input-port 167
close-output-port 167
Cointe, Pierre 149
combinateur Y 9
Common Lisp 54; 149
comparaisons 26
compilateur 171
complex? 37
cond 38; 25, 171
conditionnelle 38
cons 34; 173
contexte 96; 125
continuation 125; 141
contre apostrophe 19
Conway 11
coroutine 128
cos 24
cosinus 24
CPS 127
crochet 19
CTalk 11
current-input-port 167
current-output-port 167
Currie, Doug 4
Curry, Haskell 10; 11
define 49; 58, 62, 171
delay 171
dépiler 75
descendant 84
dictionnaire 155
INDEX
dièze 19; 77
display 168
division 23
division entière 25
do 25; 171
double apostrophe 19
double quote 19
doublet 34
Dussud, Patrick 158
Dylan 149
EBCDIC 37
échappement 138
effet de bord 74; 63
Eiffel 151
élément neutre 22
else 38; 93, 171
empiler 75
emplacement 173
encapsulation 91
engin 171
environment-bindings 96; 157
environment-bound? 97
environment-lookup 152
environment-parent 96
environment? 96
environnement 54; 53, 96, 151
environnement global 53; 55, 54
environnement initial 53
environnement parent 96
eq? 32; 41
equal? 33; 41
eqv? 33; 41, 93
erreur 171
error 138
escape 138; 141
espace 19
eta conversion 8
eta réduction 8
eval 97; 152
évaluation 138
évaluation paresseuse 15
even? 26
exit 144
exp 24
exponentielle 24
expression symbolique 19
expt 24
extension à objets 90; 97
#f 25; 26
factorielle 125
false 25
FAQ 4
faux 25
Feeley, Marc 4
fermeture 60; 82, 92
feuille 84
181
INDEX
Fibonacci 38
FIFO 89
file 93
fils 84
FL 14
Flavours 149
floor 24
fluid-let 56; 143
fonction 6; 37
fonction générique 158
fonction trigonométrique 23
fonctionnelle 49
fonctions mathématique 23
for-each 63
format 168
forme conditionnelle 38
forme normale 9
forme spéciale 19
formes spéciales 101
Fortran 89
FP 12
frère 84
funcall 62
garbage collector 173; 68
GC 173
gcd 24
gensym 111; 137
Grand O 67
graphe 84
Haskell 14; 5
hauteur 84
héritage 151
héritage multiple 158
Hewitt, Carl 17
identificateur 18
if 26; 25, 171
impair 26
indice 77
input-port? 167
instance 163
integer? 26; 37
interactif 18
interprète 171
inverse 23
iota 40
Iswim 10
Iverson, Kenneth 11
J 11
Kiczales, Gregor 163
lambda 48; 171
lambda calcul 7; 17
lambda expression 53
Landin, Peter 10
langage à objet 90
langages à objets 149
lcm 24
length 82
let 56; 171
let* 56; 171
letrec 56; 171
liaison 53
liaison dynamique 57
lié 54
Lieberman, Henry 17
LIFO 75
Lisp 149
list 34; 51, 82
list->string 80
list->vector 78
list-ref 82
list? 82
liste 34; 19, 90
liste associative 51
liste circulaire 170; 174
liste libre 173
liste vide 34
log 24
logarithme 24
logique 25
MacGambit 4
macro-définition 101
macropenalty @M 169
make-escape 138
make-stack 93
make-string 82
make-vector 82
make-walker 129
map 50; 53
max 24
maximum 24
McCarthy, John 10
McDonnell, Eugene 11
member 41
memoı̈sation 65
memq 41
memv 41; 93
méta-classe 151
méthode 151
min 24
minimum 24
Miranda 5; 14
MIT 17
ML 14; 5
modification physique 74
modulo 24
mot-clef 53
multiplication 22
négatif 26
negative? 26
new 153
newclass 150
newline 168
Nial 11
niveau 84
182
noeud 84
nombre 18; 22
nombre aléatoire 97
non-lié 54
norme 17
not 25
notation préfixée 18
number? 26; 37
O(n) 67
Oaklisp 149
OBJECT 153
ObjVlisp 151
odd? 26
open-input-file 167
open-output-file 167
opposé 23
or 25; 63, 171
ordre applicatif 9
ordre normal 9
ordre terminal 85
output-port? 167
pair 26
pair? 37
paire faible 170
paire pointée 34; 83
parenthèse 19
Pascal 54
PCScheme 4
peek-char 168
père 84
persistance indéfinie 54
pgcd 24
pile 74; 92
pile d’exécution 68
plafond 24
plancher 24
point 19
point fixe (théorème) 9
point-virgule 19
polymorphisme 14
ponctuation 19
port 167
portée indéfinie 54
portée lexicale 54
positif 26
positive? 26
ppcm 24
prédicat 25
préordre 85
procedure? 37
processus 146
programmation applicative 5
programmation déclarative 5
programmation fonctionnelle 46
programmation impérative 5
Prolog 5
puissance 24
quadratique 72
INDEX
quasiquote 171
queue 89
queue de liste 71
quicksort 79
quote 31; 19, 77, 171
quotient 25
R4RS 17
racine 84
racine carrée 25
ramasse miettes 173
rational? 37
read 168
read-char 167
real? 26; 37
receveur 141
récursion 6
réel 26
région 54
remainder 25
remove 87
ressource 67
reste 24; 25
reverse 40; 75, 89
Rosser 9
round 25
Schönfinkel 11
sémantique opérationnelle 17
send 150
séquence 71; 77
set! 60; 171
set-car! 74
set-cdr! 74
Simula 67 149
sin 25
sinus 25
SmallTalk 149
snarfer 4
sommet 74
sous-liste 71
sous-séquence 71
soustraction 23
SQL 5
sqrt 25
Steele Jr., Guy L. 17
string 82
string->list 80
string-append 82
string-ci=? 34
string-fill! 82
string-length 82
string-ref 82
string-set! 82
string=? 34
string? 37; 82
structure de blocs 54
substitution 8
super-classe 158
surcharge 14
Sussman, Gerald J. 17
INDEX
symbol? 37
symbole 18
symbole défini 97
syntax 169
système expert 42
#t 25; 26
table hachée 170
table hashcodée 170
tableau 77
tan 25
tangente 25
tête de liste 71
the-environment 96; 152
throw 142
transformation CPS 131
transparence référencielle 7
tri 71
tri par fusion 72
tri par insertion 71
troncature 25
true 25
truncate 25
Turing 10
typage faible 88
typage fort 88
typage latent 88
type 77; 88
type abstrait 91
type concret 91
type natif 89
type primitif 89
unquote 171
unquote-splicing 171
valeur absolue 23
valeurs multiples 130
variable 53
variable d’abstraction 7
variable d’instance 150
variable liée 7
vecteur 77; 71
vector 82
vector->list 78
vector-fill! 82
vector-length 82
vector-ref 82
vector-set! 82
vector? 37; 82
vide? 75
von Neumann 6
vrai 25; 38
Vuilleumier 4
while 144
write 168
write-char 168
zéro 26
zero? 26
183
184
INDEX
Table des matières
Préface
0.1 Objectifs . . . . . . . . . .
0.2 Plan de l’ouvrage . . . . .
0.3 Matériaux . . . . . . . . .
0.3.1 Le langage . . . .
0.3.2 L’implantation . .
0.3.3 Le cours . . . . . .
0.3.4 Autres documents
0.3.5 Où? . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
3
3
3
3
3
4
4
4
4
1 La programmation Fonctionnelle
1.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.2 Qu’est-ce que la programmation fonctionnelle? . . . . . . . . . . .
1.2.1 Analyse des paradigmes de programmation . . . . . . . . .
1.2.1.1 Un bref rappel historique . . . . . . . . . . . . . .
1.2.1.2 Programmation impérative . . . . . . . . . . . . .
1.2.1.3 Autres paradigmes de programmation . . . . . . .
1.3 Historique de la Programmation Fonctionnelle . . . . . . . . . . . .
1.3.1 Le Lambda Calcul . . . . . . . . . . . . . . . . . . . . . . .
1.3.1.1 Syntaxe . . . . . . . . . . . . . . . . . . . . . . . .
1.3.1.2 Variable liée . . . . . . . . . . . . . . . . . . . . .
1.3.1.3 Variable libre . . . . . . . . . . . . . . . . . . . . .
1.3.1.4 L’opération de substitution . . . . . . . . . . . . .
1.3.1.5 Règles de réécriture . . . . . . . . . . . . . . . . .
1.3.1.6 Ordre de réduction . . . . . . . . . . . . . . . . . .
1.3.1.7 Forme normale . . . . . . . . . . . . . . . . . . . .
1.3.1.8 Théorèmes de Church-Rosser . . . . . . . . . . . .
1.3.1.9 Théorème du point fixe . . . . . . . . . . . . . . .
1.3.1.10 Conclusion . . . . . . . . . . . . . . . . . . . . . .
1.3.2 Lisp . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.3.3 Iswim . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.3.4 APL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.3.5 FP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.3.5.1 Exemples . . . . . . . . . . . . . . . . . . . . . . .
1.3.5.2 Impact de FP . . . . . . . . . . . . . . . . . . . .
1.3.6 ML . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.3.7 Haskell . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.3.7.1 Evaluation paresseuse . . . . . . . . . . . . . . . .
1.4 Concepts de la programmation fonctionnelle — Une rétrospective
1.5 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
5
5
5
5
5
6
6
6
7
7
7
8
8
8
9
9
9
10
10
10
11
11
12
12
14
14
15
15
16
16
2 Une Introduction à Scheme
2.1 Pourquoi Scheme . . . . . . . . . . . . . . . .
2.1.1 Scheme en quelques mots . . . . . . .
2.2 Syntaxe et Sémantique . . . . . . . . . . . . .
2.2.1 Expressions Symboliques . . . . . . .
2.2.1.1 Les atomes . . . . . . . . . .
2.2.1.2 Les expressions symboliques
2.2.1.3 Les ponctuations . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
17
17
17
18
18
18
19
19
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
185
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
186
TABLE DES MATIÈRES
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
19
20
22
22
23
25
25
26
26
26
27
27
28
29
30
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
31
31
32
32
34
37
38
38
39
39
39
40
41
42
4 Fonctions
4.1 La fonction, objet de première classe . . . . . . . .
4.1.1 Retour sur la syntaxe des expressions . . .
4.1.2 Création de fonctions . . . . . . . . . . . .
4.2 Fonctionnelles . . . . . . . . . . . . . . . . . . . . .
4.2.1 Application de fonction . . . . . . . . . . .
4.2.2 Exemples . . . . . . . . . . . . . . . . . . .
4.2.3 Zéros d’une fonction . . . . . . . . . . . . .
4.2.4 Map encore . . . . . . . . . . . . . . . . . .
4.3 Environnement . . . . . . . . . . . . . . . . . . . .
4.3.1 Définitions . . . . . . . . . . . . . . . . . .
4.3.2 Environnement dynamique . . . . . . . . .
4.3.3 Visibilité et Persistance . . . . . . . . . . .
4.3.3.1 Définitions . . . . . . . . . . . . .
4.3.4 Notions . . . . . . . . . . . . . . . . . . . .
4.3.4.1 Note . . . . . . . . . . . . . . . . .
4.4 Environnements locaux . . . . . . . . . . . . . . .
4.4.1 Création d’environnements locaux . . . . .
4.4.1.1 Exemples . . . . . . . . . . . . . .
4.4.2 Remarques . . . . . . . . . . . . . . . . . .
4.4.2.1 Equivalence entre let et lambda
4.4.2.2 La forme fluid-let . . . . . . . . .
4.4.2.3 La forme define . . . . . . . . . .
4.4.3 Le let nommé . . . . . . . . . . . . . . . . .
4.5 Utilisation dynamique des fonctions . . . . . . . .
4.5.1 Création dynamique de fonction . . . . . .
4.5.2 Quelques exemples . . . . . . . . . . . . . .
4.5.3 Quelques utilisations des fermetures . . . .
4.5.4 Define encore . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
47
47
47
48
49
51
51
52
53
53
53
54
54
54
54
55
56
56
56
57
57
57
58
59
59
59
60
60
62
2.3
2.4
2.5
2.6
2.7
2.8
2.9
2.2.2 Programmes et évaluation . . . . .
2.2.3 Une session avec MIT Scheme . . .
Type numérique . . . . . . . . . . . . . .
2.3.1 Expressions arithmétiques . . . . .
2.3.2 Autres fonctions arithmétiques . .
Le type booléen . . . . . . . . . . . . . . .
2.4.1 Les valeurs booléennes . . . . . . .
2.4.2 Prédicats . . . . . . . . . . . . . .
2.4.2.1 Fonctions de comparaison
2.4.3 Autres prédicats numériques . . .
Structures de contrôle . . . . . . . . . . .
Fonctions . . . . . . . . . . . . . . . . . .
Programmer en Scheme . . . . . . . . . .
Conclusion . . . . . . . . . . . . . . . . .
Travail personnel . . . . . . . . . . . . . .
3 Eléments de Scheme
3.1 Symboles . . . . . . . . . . . . . . . .
3.2 Opérations sur données symboliques .
3.2.1 Les mystères de la comparaison
3.3 Listes . . . . . . . . . . . . . . . . . .
3.4 Prédicats utiles . . . . . . . . . . . . .
3.5 Structures de Controles . . . . . . . .
3.5.1 La forme cond . . . . . . . . .
3.5.2 Les formes or et and . . . . .
3.6 Applications . . . . . . . . . . . . . . .
3.6.1 Parcours de listes . . . . . . . .
3.6.2 Génération de listes . . . . . .
3.7 Exercices d’application . . . . . . . . .
3.8 Un exemple complet . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
187
TABLE DES MATIÈRES
4.6
Exercices d’application . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
5 Algorithmes en Scheme
5.1 Ressources . . . . . . . . . . . . . . . . . . .
5.1.1 Remarques sur les algorithmes . . .
5.1.2 Critères . . . . . . . . . . . . . . . .
5.1.2.1 Le temps . . . . . . . . . .
5.1.2.2 L’espace . . . . . . . . . .
5.2 Algorithmes itératifs . . . . . . . . . . . . .
5.3 La liste, structure de donnée . . . . . . . . .
5.3.1 Définitions . . . . . . . . . . . . . .
5.3.1.1 Longueur d’une liste . . . .
5.3.1.2 Eléments d’une liste . . . .
5.3.2 Utilisation des listes : le tri . . . . .
5.3.2.1 Tri par insertion . . . . . .
5.3.2.2 Tri par par fusion . . . . .
5.3.3 Modification physique des listes . . .
5.3.4 Pile . . . . . . . . . . . . . . . . . .
5.3.4.1 Une implantation des piles
5.3.4.2 La fonction reverse gr^
ace
5.4 Exercices . . . . . . . . . . . . . . . . . . .
. . .
. . .
. . .
. . .
. . .
. . .
. . .
. . .
. . .
. . .
. . .
. . .
. . .
. . .
. . .
. . .
aux
. . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
piles
. . . .
6 Structures de données en Scheme
6.1 Les séquences . . . . . . . . . . . . . . . . . . . . .
6.1.1 Vecteurs . . . . . . . . . . . . . . . . . . . .
6.1.1.1 Procédures primitives sur vecteurs
6.1.1.2 Utilisation des vecteurs . . . . . .
6.1.2 Chaı̂nes . . . . . . . . . . . . . . . . . . . .
6.1.3 Opérations sur séquences . . . . . . . . . .
6.1.4 Algorithmes fonctionnels . . . . . . . . . . .
6.2 Les arbres . . . . . . . . . . . . . . . . . . . . . . .
6.2.1 Terminologie . . . . . . . . . . . . . . . . .
6.2.1.1 Définition . . . . . . . . . . . . . .
6.2.1.2 Une définition récursive . . . . . .
6.2.1.3 Ancêtres et descendants . . . . . .
6.2.1.4 Arbre binaire . . . . . . . . . . . .
6.2.2 Représentation des arbres . . . . . . . . . .
6.2.3 Exemples de parcours d’arbres . . . . . . .
6.2.3.1 Une vision fonctionnelle . . . . . .
6.3 Exercices d’application . . . . . . . . . . . . . . . .
6.4 La notion de Type . . . . . . . . . . . . . . . . . .
6.4.1 Introduction . . . . . . . . . . . . . . . . .
6.4.2 Types en Scheme . . . . . . . . . . . . . . .
6.4.2.1 Caractéristiques des types natifs .
6.4.3 Utilisation des types . . . . . . . . . . . . .
6.4.4 Types définis . . . . . . . . . . . . . . . . .
6.4.5 Protection des types . . . . . . . . . . . . .
6.4.5.1 Types concrets . . . . . . . . . . .
6.4.5.2 Types abstraits . . . . . . . . . . .
6.5 Création de types en Scheme . . . . . . . . . . . .
6.5.1 Implantation par fermetures . . . . . . . . .
6.5.2 Exemples . . . . . . . . . . . . . . . . . . .
6.6 Analyse de l’approche . . . . . . . . . . . . . . . .
6.6.1 Sécurisation . . . . . . . . . . . . . . . . . .
6.6.2 Critique de la solution . . . . . . . . . . . .
6.6.3 Environnements . . . . . . . . . . . . . . .
6.7 Exercices d’application . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
67
67
67
68
68
68
69
70
71
71
71
71
71
72
74
75
75
76
76
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
77
77
77
77
78
80
82
82
84
84
84
84
84
84
85
85
86
87
89
89
89
89
89
90
91
91
91
91
91
92
94
94
97
97
98
188
TABLE DES MATIÈRES
7 Macros
7.1 Retour sur les formes Spéciales . . . . . .
7.1.1 Puissance et Complexité . . . . . .
7.1.2 Syntaxe et Sémantique des Macros
7.1.2.1 La forme define-macro .
7.1.2.2 Sémantique des macros .
7.1.2.3 Macros et interprétation
7.1.2.4 Appel par nom . . . . . .
7.2 Utilisations des macros . . . . . . . . . . .
7.2.1 Quelques exemples classiques . . .
7.2.2 La tradition . . . . . . . . . . . . .
7.2.2.1 prog1 et prog2 . . . . . .
7.2.2.2 push et pop . . . . . . . .
7.2.3 Remarque . . . . . . . . . . . . . .
7.3 Quasi Quote et ses compères . . . . . . .
7.3.1 Présentation . . . . . . . . . . . .
7.3.2 Sémantique . . . . . . . . . . . . .
7.4 Exercices . . . . . . . . . . . . . . . . . .
7.4.1 Quelques macros . . . . . . . . . .
7.4.1.1 while . . . . . . . . . . .
7.4.2 Compréhension . . . . . . . . . . .
7.4.2.1 Entrainement . . . . . . .
7.4.2.2 Forme quasiquote . . . .
7.4.2.3 Macro-expand . . . . . .
7.5 Une question d’hygiène . . . . . . . . . . .
7.5.1 Captures . . . . . . . . . . . . . .
7.5.2 Macros hygiéniques . . . . . . . . .
7.5.3 Niveau bas . . . . . . . . . . . . .
7.5.3.1 Implantation . . . . . . .
7.5.3.2 Note . . . . . . . . . . . .
7.5.4 Exercices . . . . . . . . . . . . . .
7.5.4.1 Macros Standards . . . .
7.5.4.2 Structures . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
101
101
101
102
102
102
103
103
103
104
104
104
104
104
105
105
106
107
107
107
107
107
107
107
108
108
108
110
111
112
112
112
112
8 Flots
8.1 Opérations sur flots . . . . . . . . . . . .
8.1.1 Exemples . . . . . . . . . . . . .
8.1.2 Exercices . . . . . . . . . . . . .
8.1.2.1 Préparation . . . . . . .
8.1.2.2 Accumulate . . . . . . .
8.1.2.3 Filter . . . . . . . . . .
8.1.2.4 Transversal . . . . . . .
8.2 Flots infinis . . . . . . . . . . . . . . . .
8.3 Evaluation paresseuse en Scheme . . . .
8.3.1 Les opérations delay et force . .
8.3.1.1 Exercice. . . . . . . . .
8.3.2 Applications . . . . . . . . . . .
8.3.3 Jensen’s device . . . . . . . . . .
8.4 Modélisation des flots infinis . . . . . . .
8.4.1 Réécriture des fonctions sur flots
8.4.1.1 Remarque . . . . . . . .
8.4.2 Applications . . . . . . . . . . .
8.4.3 Exercices . . . . . . . . . . . . .
8.4.3.1 Au cas où... . . . . . . .
8.4.3.2 Alternate Streams . . .
8.4.3.3 Merge Streams . . . . .
8.4.3.4 Streams . . . . . . . . .
8.4.4 Flots infinis . . . . . . . . . . . .
8.4.5 Exercices . . . . . . . . . . . . .
8.4.5.1 nth-stream . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
115
115
115
116
116
116
116
117
117
117
117
118
118
119
121
121
121
122
122
122
122
122
123
123
123
123
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
189
TABLE DES MATIÈRES
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
123
123
123
123
123
123
124
124
124
9 Continuations
9.1 Introduction . . . . . . . . . . . . . . . . . . . . . .
9.2 Contexte . . . . . . . . . . . . . . . . . . . . . . . .
9.2.1 Un premier exemple . . . . . . . . . . . . .
9.2.2 Le style CPS . . . . . . . . . . . . . . . . .
9.2.2.1 Erreurs et échappements . . . . .
9.2.2.2 Introduction aux Coroutines . . .
9.2.2.3 Valeurs multiples . . . . . . . . . .
9.3 Transformation CPS . . . . . . . . . . . . . . . . .
9.3.1 Définitions préliminaires . . . . . . . . . . .
9.3.1.1 Formes . . . . . . . . . . . . . . .
9.3.1.2 Opérations primitives . . . . . . .
9.3.1.3 Formes spéciales . . . . . . . . . .
9.3.1.4 Autres définitions . . . . . . . . .
9.3.1.5 Exercice . . . . . . . . . . . . . . .
9.3.2 Conversion en forme CPS . . . . . . . . . .
9.3.2.1 Lambda expression . . . . . . . .
9.3.2.2 Formes simples . . . . . . . . . . .
9.3.2.3 Appel de fonction . . . . . . . . .
9.3.2.4 Formes non finales . . . . . . . . .
9.3.2.5 Formes spéciales . . . . . . . . . .
9.3.3 Exemples . . . . . . . . . . . . . . . . . . .
9.3.4 Conflits de noms . . . . . . . . . . . . . . .
9.3.5 Formes simples ou itératives . . . . . . . . .
9.3.5.1 Exemple . . . . . . . . . . . . . .
9.3.6 Evaluateurs à passage de continuation . . .
9.4 Echappement . . . . . . . . . . . . . . . . . . . . .
9.5 La forme call-with-current-continuation . . .
9.5.1 Quelques exemples . . . . . . . . . . . . . .
9.5.1.1 La forme escape . . . . . . . . . .
9.5.1.2 Les sorties exceptionnelles . . . .
9.6 Utilisation avancée des continuations . . . . . . . .
9.6.1 Exemples . . . . . . . . . . . . . . . . . . .
9.6.1.1 Catch et Throw . . . . . . . . . .
9.6.1.2 While et Exit . . . . . . . . . . . .
9.6.1.3 Exercice : boucles nommées . . . .
9.6.1.4 Exercice : make-walker revisité
9.6.1.5 Quiz . . . . . . . . . . . . . . . . .
9.6.1.6 Exercice : coroutines . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
125
125
125
125
127
127
128
130
131
131
131
131
132
132
132
133
133
133
133
133
134
135
137
137
138
138
138
139
141
141
141
142
142
142
144
145
145
145
146
10 Objets en Scheme
10.1 Introduction . . . . . . . . . . . . .
10.2 Des classes en Scheme . . . . . . .
10.2.1 Un exemple d’utilisation . .
10.2.2 L’implantation . . . . . . .
10.2.3 Critique du modèle proposé
10.2.3.1 Exercice . . . . . .
10.3 Le modèle ObjVlisp . . . . . . . .
10.3.1 Notions Initiales . . . . . .
10.3.1.1 Environnement . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
149
149
149
150
150
151
151
151
151
151
8.4.6
8.4.5.2
8.4.5.3
8.4.5.4
8.4.5.5
8.4.5.6
Flots de
8.4.6.1
8.4.6.2
8.4.6.3
Multiples . . . . .
Fibonacci . . . . .
Nombres premiers
Applicateur . . . .
Constructeur . . .
caractères . . . . .
Entrées et sorties .
wc . . . . . . . . .
zap-gremlins . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
190
TABLE DES MATIÈRES
10.3.1.2 Evaluation . . . . . . . . .
10.3.2 Présentation du modèle . . . . . . .
10.3.3 Exemples d’utilisation . . . . . . . .
10.3.4 L’implantation . . . . . . . . . . . .
10.4 Le modèle CLOS . . . . . . . . . . . . . . .
10.4.1 Classes . . . . . . . . . . . . . . . .
10.4.2 Fonction générique . . . . . . . . . .
10.4.3 Objets de Common Lisp . . . . . . .
10.4.3.1 CLOS et les types primitifs
10.4.3.2 Hiérarchie des classes . . .
10.4.4 Le protocole de méta-objets . . . . .
10.4.5 Niveau de base . . . . . . . . . . . .
10.4.5.1 Classes et Slots . . . . . . .
10.4.5.2 Algorithme d’héritage . . .
10.4.5.3 Fonctions génériques . . . .
10.4.5.4 Instances . . . . . . . . . .
10.5 CLOS en Scheme . . . . . . . . . . . . . . .
10.6 Autres extensions à Objets . . . . . . . . .
10.6.1 Oaklisp . . . . . . . . . . . . . . . .
10.6.2 Dylan . . . . . . . . . . . . . . . . .
11 Quelques aspects des systèmes Scheme
11.1 Opérations d’entrées-sorties . . . . . . .
11.1.1 Ports d’entrée-sortie . . . . . . .
11.1.2 Lecture et Ecriture . . . . . . . .
11.1.3 Formatage . . . . . . . . . . . . .
11.2 Aspects syntaxiques . . . . . . . . . . .
11.2.1 Macros . . . . . . . . . . . . . .
11.2.2 Forme Syntax . . . . . . . . . .
11.3 Structures de données . . . . . . . . . .
11.3.1 Paires faibles . . . . . . . . . . .
11.3.2 Tables hachées . . . . . . . . . .
11.3.3 Listes circulaires . . . . . . . . .
11.4 Structures de contrôles . . . . . . . . . .
11.4.1 Engins . . . . . . . . . . . . . . .
11.4.2 Erreurs . . . . . . . . . . . . . .
11.5 Réalisation des systèmes . . . . . . . . .
11.5.1 Scheme en Scheme . . . . . . . .
11.5.2 Mise en place d’environnements .
11.6 Gestion de la mémoire . . . . . . . . . .
11.7 Exercices d’application . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
152
152
153
155
158
159
159
159
159
159
159
161
161
161
163
164
164
165
166
166
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
167
167
167
167
168
169
169
169
170
170
170
170
171
171
171
171
171
172
172
174
Bibliographie
175
Index
179
TABLE DES MATIÈRES
191
Téléchargement