ABC des ordinateurs pour informaticiens en herbe
Les octets
Vous avez souvent entendu que les ordinateurs utilisent des "octets"
(en anglais on dit "bytes") pour mémoriser les informations.
Qu'est-ce que cela veut dire au juste ? Un octet est une petite mémoire
qui peut retenir un nombre
entre 0 et 255. En d'autres termes : si vous mettez le nombre 47 dans
un octet d'ordinateur, deux heures après il contient toujours 47.
Vous auriez aussi pu mettre 0 dedans, ou 255, ou 79, ou 139... tout ce
que vous voulez tant que c'est un entier dans l'interval [0, 255].
Un PC moderne dispose d'un nombre astronomique d'octets. La
mémoire RAM des modèles actuellement en vente est souvent
faite de 1 milliard d'octets. Les disques durs contiennent entre 100
milliards et 500 milliards d'octets.
A quoi utilise-t-on ces octets ? Comment les utilise-t-on ? Un aspect
très important de la réponse est qu'à la base les
octets se suivent dans un ordre précis. Ils sont tous soigneusement
numérotés. Par exemple le premier octet de la mémoire RAM porte le
numéro 21.783, le deuxième octet porte le numéro 21.784, le troisième
porte le numéro 21.785 et ainsi de suite. Donc, si on prend quatre
octets
consécutifs de mémoire et que l'on met 78, 6, 200 et 32
dedans, deux ans après on y trouve toujours 78, 6, 200 et 32. Et
certainement pas 200, 78, 32 et 6. Ni d'avantage 32, 78, 6 et 200.
Mais comment s'en sert-on ? Et bien en fait, vous vous en servez comme
vous voulez. Imaginez qu'il existe une race d'oiseaux capables de
retenir deux nombres entre 0 et 255. Ces oiseaux sont dressés
pour faire la navette entre deux personnes. Quand une personne veut
transmettre un message à l'autre, elle dit deux nombres à
l'oiseau, qui s'envole aussitôt pour les répéter
à l'autre personne. Libres à ces deux personnes de
convenir d'un code. Voici par exemple un table pour le premier nombre :
1
|
apporter |
2
|
détruire |
3
|
préparer |
4
|
manger |
5
|
découper |
6
|
décorer |
7
|
planter |
8
|
dresser |
9
|
lancer |
10
|
chanter |
11
|
louer |
12
|
boire |
13
|
lire |
Voici une table pour le deuxième nombre :
1
|
pomme |
2
|
cerise
|
3
|
fleur |
4
|
table |
5
|
rue
|
6
|
repas |
7
|
livre |
Si l'oiseau arrive à vous et dit "4, 6", vous comprenez que
votre correspondant vous dit "manger repas" donc de passer à
table. Si l'oiseau vous dit "12, 7", donc de boire un livre, vous vous
demanderez si votre correspondant vous fait une blague ou si l'oiseau a
fait une erreur. L'oiseau ne comprend rien aux deux nombres qu'il
transporte. Il ne fait que les répéter. A vous d'en faire
bon usage.
De même, si deux amis utilisent une paire de tables et deux
autres amis utilisent une paire de table complètement
différente, il peut y avoir des confusions. Supposons que vous
êtes chez un ami pendant son absence et qu'un oiseau arrive et
dit deux nombres... Si vous interprétez les deux nombres
d'après votre propre table vous risquez de faire n'importe quoi,
en croyant lui rendre service. Dans un pays bien organisé, on
essaye de faire en sorte que tout le monde utilise les mêmes
tables.
Une des tables les plus simples et les plus importantes
utilisées par les ordinateurs est la table ASCII. Elle sert
à encoder des textes :
| |
|
|
32 |
espace
|
|
64 |
@ |
|
96 |
` |
| |
|
|
33 |
! |
|
65 |
A |
|
97 |
a |
| |
|
|
34 |
" |
|
66 |
B |
|
98 |
b |
| |
|
|
35 |
# |
|
67 |
C |
|
99 |
c |
| |
|
|
36 |
$ |
|
68 |
D |
|
100 |
d |
| |
|
|
37 |
% |
|
69 |
E |
|
101 |
e |
| |
|
|
38 |
& |
|
70 |
F |
|
102 |
f |
| |
|
|
39 |
' |
|
71 |
G |
|
103 |
g |
| |
|
|
40 |
( |
|
72 |
H |
|
104 |
h |
| 9 |
tabulation |
|
41 |
) |
|
73 |
I |
|
105 |
i |
| 10 |
descendre d'une ligne |
|
42 |
* |
|
74 |
J |
|
106 |
j |
| |
|
|
43 |
+ |
|
75 |
K |
|
107 |
k |
| |
|
|
44 |
, |
|
76 |
L |
|
108 |
l |
| 13 |
retour début de ligne |
|
45 |
- |
|
77 |
M |
|
109 |
m |
| |
|
|
46 |
. |
|
78 |
N |
|
110 |
n |
| |
|
|
47 |
/ |
|
79 |
O |
|
111 |
o |
| |
|
|
48 |
0 |
|
80 |
P |
|
112 |
p |
| |
|
|
49 |
1 |
|
81 |
Q |
|
113 |
q |
| |
|
|
50 |
2 |
|
82 |
R |
|
114 |
r |
| |
|
|
51 |
3 |
|
83 |
S |
|
115 |
s |
| |
|
|
52 |
4 |
|
84 |
T |
|
116 |
t |
| |
|
|
53 |
5 |
|
85 |
U |
|
117 |
u |
| |
|
|
54 |
6 |
|
86 |
V |
|
118 |
v |
| |
|
|
55 |
7 |
|
87 |
W |
|
119 |
w |
| |
|
|
56 |
8 |
|
88 |
X |
|
120 |
x |
| |
|
|
57 |
9 |
|
89 |
Y |
|
121 |
y |
| |
|
|
58 |
: |
|
90 |
Z |
|
122 |
z |
| |
|
|
59 |
; |
|
91 |
[ |
|
123 |
{ |
| |
|
|
60 |
< |
|
92 |
\ |
|
124 |
| |
| |
|
|
61 |
= |
|
93 |
] |
|
125 |
} |
| |
|
|
62 |
> |
|
94 |
^ |
|
126 |
~ |
| |
|
|
63 |
? |
|
95 |
_ |
|
127 |
|
Donc si 7 octets consécutifs d'une mémoire
d'ordinateur contiennent 66,
111, 110, 106, 111, 117, 114, et bien ils
contiennent le texte "Bonjour". Enfin, à condition qu'il
avait été décidé que c'était du texte... Sinon, ces nombres sont
peut-être le code de lancement des missiles, allez savoir... Dans un
ordinateur bien tenu on garde la trace d'à quoi sert chaque zone de
mémoire. Sinon... les conséquences peuvent être totalement
psychédéliques.
Les octets des ordinateurs peuvent aussi contenir des images. Prenons
par exemple la minuscule image suivante :
Je vous la montre agrandie :
Voici comment elle est mémorisée :
71
73 70 56 55 97 8 0 8 0 128 2 0 0 0 0 255 0 0 44 0 0
0 0 8 0 8 0 0 2 12 132 143 137 17 217 240 204 139 146 170 107 10 0 59
59
Il est inutile que vous compreniez cette suite de 45 nombres. Remarquez
toutefois deux choses :
- L'image est constituée de 8 x 8 points. Constatez que
le septième et le neuvième nombre sont 8. Il n'y a pas de hasard, ils
veulent
bien dire que l'image a une taille de 8 x 8.
- Cette image est encodée suivant le format "GIF". Constatez
que les trois premiers nombres sont 71, 73 et 70, ce qui donne bien G, I et F dans
la table ASCII.
Certains nombres de l'image utilisent la table ASCII (comme "GIF"), d'autres
doivent êtres pris littéralement (8
et 8)... C'est un peu
le souk. Peu importe, tant que les informaticiens s'y retrouvent.
Faisons-leur confiance même s'ils ne le méritent pas
toujours.
Ne me faites pas aveuglément confiance. Mettez mes dires
à l'épreuve, au moins un minimum. Par exemple enregistrez
la petite image ci-dessus dans votre ordinateur (cliquez dessus avec le
bouton de droite de votre souris et faites pour un mieux). Cela stocke
dans votre ordinateur un petit fichier nommé "programmation_01.gif".
Demandez à votre ordinateur la taille de ce fichier (cliquez
dessus avec le bouton de gauche ou cliquez dessus avec le bouton de
droite et demandez les propriétés du fichier). Il vous
répondra que la taille est de 45 octets. Cela ne vous
prouve pas que l'image est bien constituée d'exactement les
45 nombres mentionnés ci-dessus. Mais au moins vous vérifiez
que le nombre d'octets utilisés est bien celui que je
prétends. C'est toujours ça.
Vous avez sans doute déjà utilisé un "traitement
de texte" pour écrire une lettre ou un document quelconque (par
exemple les traitements de texte Word, Wordpad, Works, Abiword,
OpenOffice, Star Office...). Démarrez un tel traitement de texte
et faites l'expérience d'écrire un minuscule petit texte
et par exemple d'en agrandir une lettre, comme ceci :
Bonjour ordinateur
!
Ensuite enregistrez ce texte (Menu Fichier -> Enregister) sous un
nom de fichier quelconque et demandez à votre ordinateur de vous
donnez la taille du fichier. Il vous répondra un taille
incroyable. Peut-être 16.000 octets, voire beaucoup plus (s'il
vous répond 16k, le "k" veut dire 1.000) (s'il répond 16
ko, le "o" veut dire "octets"). Pour bêtement mémoriser ces
quelques mots et le fait que la onzième lettre est agrandie,
votre traitement de texte utilise 16.000 octets ! C'est grotesque. Il
n'y
a pas d'excuse à cela. N'y voyez que la
décrépitude de l'informatique actuelle. Versons une larme
sur notre dignité perdue et continuons.
Nous ne sommes que de modestes informaticiens en herbe, nous allons
nous contenter de choses simples. Démarrez un "éditeur de
texte". J'ai bien dit éditeur
et pas "traitement". Sous
Windows, vous trouverez aisément l'éditeur "Bloc-notes"
(Notepad en anglais). Sous Linux, vous trouverez GEdit, KWrite,
Leafpad, Mousepad, KEdit... Une fois l'éditeur ouvert, tapez
à nouveau un petit texte court. Par exemple ceci :
Ceci
est un texte.
Enregistrez-le en lui donnant
un nom de fichier de votre choix. Un conseil :
si votre éditeur ne le fait pas automatiquement, veillez
à ce que nom du fichier se termine par les quatre caractères ".txt".
Demandez la taille du fichier. Vous constaterez qu'elle est de 18
octets. Ou un peu plus si vous avez tapé des blancs
supplémentaires ou des passages à la ligne.
Un éditeur de texte
lit et enregistre des textes de façon
très sobre, en se contenant d'utiliser la table ASCII.
Langage de description
Le travail des informaticiens est de faire obéir les
ordinateurs. Mais comment leur donne-t-on des ordres ? Pour cela on
utilise des langages. Un langage très simple et fort utile est
le HTML. (Quand je dis simple, je veux dire avant que certains
informaticiens ne compliquent tout par incompétence et par
malhonnêteté. Mais bon, il est toujours possible d'utiliser les
bases du HTML de façon simple.) A quoi sert le HTML ? Il sert
à décrire les pages sur Internet. Regardez l'adresse du
présent document ;
http://www.4p8.com/eric.brasseur/programmation.html, le fait que
l'adresse se termine par html ne veut rien dire d'autre : la page est
programmée en langage HTML. Alors, démarrez un
éditeur de texte et
tapez le texte suivant :
<html>
<body>
Bonjour, vous allez <font
color="red">bien</font> ?
</body>
</html>
Enregistrez ce texte dans un fichier. Cette-fois-ci veillez à ce
que le nom du fichier se termine par .html
et non par .txt. Sur Windows
il faudra sans doute vous battre pour obtenir que le nom du fichier se
termine réellement par .html...
Double-cliquez sur le fichier. Il doit s'ouvrir dans votre navigateur
et vous afficher ceci :
Bonjour, vous allez bien ?
L'ordinateur vous a obéi ! Le code HTML stipule que le mot
"bien" doit être écrit en rouge ("red" en anglais). Le
navigateur l'a effectivement écrit en rouge ! Ou alors vous avez
fait une erreur...
Libre à vous maintenant d'apprendre les autres "balises" HTML.
Celle pour passer à la ligne (<br>), celles pour mettre un
titre (mettez le titre entre <h1> et </h1>), celles pour
mettre en gras (entre <b> et </b>), celle pour souligner
(entre <u> et </u>), etc...
Le code HTML a de nombreux avantages. Par exemple voici comment encoder
l'exemple donné ci-dessus (j'ai
mis moi-même les balises HTML en vert pour que vous distinguiez
mieux la structure) :
<html>
<body>
Bonjour or<font
size=+4>d</font>inateur !
</body>
</html>
Si vous tapez cet exemple avec un éditeur de texte et vous
l'enregistrez dans un fichier .html, vous constaterez
qu'il ne prend que quelques dizaines d'octets. C'est tout de même
plus rationnel que 16.000 octets ! Cela dit, on ne peut pas faire en
simple HTML tout ce qu'on peut faire avec un traitement de texte. Rien
n'est parfait...
Notez que quand votre navigateur affiche une page HTML, vous pouvez lui
demander
de vous montrer le "code source" HTML de la page. Vous pouvez le faire
pour cette page-ci, par exemple. Cherchez dans les menus du navigateur.
Vous pouvez faire l'essai suivant : tapez un court texte dans un
traitement de texte, éventuellement mettez un mot en gras ou en
couleur, ensuite enregistrez ce texte en format HTML. Pour qu'il soit
enregistré en HTML, vous devrez le préciser dans la boîte de dialogue
lors de l'enregistrement (celle qui vous permet de taper le nom du
fichier). Ensuite, ouvrez ce fichier avec un éditeur de texte. Ce que
vous verrez sera probablement assez long et tortueux mais vous pourrez
retrouver le texte au milieu. Vous devriez également pouvoir
reconnaître certaines balises HTML.
Langage de programmation
Un ami m'a plusieurs fois dit qu'il regrettait que je ne continue pas
ce texte, que je n'ajoute pas une chapitre sur la programmation. Il
m'aura fallu près de deux ans pour prendre une décision : quel langage
de programmation utiliser pour illustrer les explications. Voyez-vous,
je connais très peu de personnes qui soient réellement capables
d'écrire des programmes informatiques. Je connais pas mal de personnes
intelligentes qui arrivent à se débrouiller... Mais vraiment
programmer... il n'y en a pas beaucoup. C'est une des raisons de la
mauvaise qualité des systèmes informatiques actuels. Je crois que le
problème est en partie dû à une légère arnaque dans les langages de
programmation : ces langages ressemblent à une sorte d'anglais
rudimentaire. Cela donne l'impression que l'ordinateur sait parler
anglais et... qu'avec un peu de bon sens on arrivera à lui faire faire
ce qu'on veut. C'est une illusion !
J'ai fini par choisir le langage LISP. Je vais approximativement vous
expliquer pourquoi.
N'essayez pas de comprende l'exemple suivant. Il n'y a rien à
comprendre... et ce texte n'en parlera plus par la suite :
10011000
10011001
11010001
10011001
11110001
01001100
C'est ce qu'on appelle "du langage binaire". Quand le microprocesseur
de l'ordinateur reçoit cela, il effectue des actions et des calculs
bien précis. Pour le dire autrement : c'est un genre de code Morse
qu'on envoie dans le microprocesseur pour lui dire ce qu'il doit faire.
J'ai parfois programmé des ordinateurs de cette façon. C'est laborieux
mais ça marche. Répétons-le : le but de ce texte n'est pas de vous
apprendre ce "code Morse".
Il n'y a rien à comprendre non plus dans ce seconde exemple :
POP
A
POP
B
MUL
A B
POP
B
ADD
A B
MOV
(DE) A
C'est ce qu'on appelle "du langage Assembleur". C'est la même chose que
le langage binaire présenté ci-dessus mais écrit d'une façon plus
lisible pour les humains. J'ai écrit des kilomètres de code Assembleur
pendant des années... Pour vous montrer que le langage binaire se
traduit directement en Assembleur et réciproquement, voici les deux
exemples placés côté à côté et coloriés. Il n'y a toujours rien à
compendre mais remarquez simplement qu'il y a des correspondances
directes entre
l'assembleur et le code binaire :
| POP A |
|
10011000
|
| POP B |
|
10011001 |
| MUL
A B |
|
11010001
|
| POP B |
|
10011001 |
| ADD
A B |
|
11110001
|
MOV
(DE) A
|
|
01001100
|
Ne vous attardez pas à essayez de comprendre ces exemples. Ils sont
juste là pour vous montrer que la façon dont on donne des ordres à un
microprocesseur est brutale et illisible.
Des programmeurs en ont eu marre décrire des programmes en Assembleur
et ils ont décidé d'exprimer ce que l'ordinateur doit faire de façon un
peu plus claire pour un humain. Cela a donné des langages comme le
LISP, le Forth ou la "notation polonaise inverse" (RPN en anglais).
Voici un exemple, un calcul exprimé en LISP :
(*
2 (+ 5 8))
N'essayez pas de le comprendre... mais le but de ce texte est de vous
le faire comprendre. Et bien d'avantage.
Hewlett Packard (HP) a
fabriqué les premières calculatrices de poche pour ingénieurs. Elles
fonctionnaient en "notation polonaise inverse". Pour calculer 2 x (5 + 8) il fallait
taper ceci sur le clavier de la calculatrice :
8
ENTER 5 + 2 x
Dans le cadre de ce texte, il est inutile que vous compreniez comment
fonctionne la notation polonaise inverse. Constatez juste que le LISP
et la polonaise inverse se ressemblent : les deux exemples ci-dessus
sont presque identiques si on lit un des deux à l'envers.
Si vous voulez essayer, voici deux calculatrices en ligne qui
fonctionnent en notation polonaise inverse :
Un peu plus tard, Texas Instruments
(TI) a
fabriqué des calculatrices pour ingénieurs qui permettaient de taper
les calculs "presque en langage humain". Pour faire le calcul avec une
TI, on tapait simplement ceci :
2 x
( 5 + 8 ) =
Votre calculatrice actuelle fonctionne probablement de cette façon...
Pour que cela soit possible, on avait ajouté aux calculatrices TI un
dispositif qui
traduisait le "langage humain" en polonaise inverse. Dans ses circuits,
la TI faisait les calculs en polonaise inverse... mais on tapait les
calculs en langage humain et ils étaient traduits en polonaise inverse
avant d'être effectués.
Ainsi, tout est devenu une question de chaine de traduction. De nos
jours, les programmeurs écrivent les programmes informatiques "dans une
sorte de langage humain". L'ordinateur traduira ce pseudo-langage
humain en une sorte de LISP. Ensuite ce LISP sera traduit en
Assembleur. Et puis enfin cet Assembleur est traduit en code machine...
La question est : s'il est possible de programmer les ordinateurs "dans
une sorte de langage humain", c'est bien pratique... Pourquoi est-ce
que je vais plutôt vous expliquer la programmation en LISP ? Un début
de réponse :
- Il faut enfoncer 8 touches du clavier pour faire le calcul sur
une TI et seulement 6 touches sur une HP.
- Pour des calculs très courts, je dois admettre que la TI était
plus pratique. Par contre quand je faisais de calculs longs et
complexes, j'avais beaucoup moins de problèmes avec une HP...
- En particulier, parfois je ne savais simplement pas comment je
devais me servir de la TI. Il fallait relire le mode d'emploi... Je me
suis rendu compte que ceux qui savaient comment la TI traduisait les
calculs en polonaise inverse, avaient beaucoup moins de problèmes. Ils
savaient comment mener la TI à faire les traductions adéquates. Ils
savaient comment "manipuler" la TI, qui en réalité était très bête et
faisait juste semblant de comprendre les calculs en langage humain.
Si vous êtes capables de bien programmer en LISP (ou en Forth, ou en
Assembleur...) vous pourrez vous débrouiller dans un peu tous les
langages de programmation "pseudo-humains". Parce que vous comprennez
ce que vous faites...
Il existe d'excellents informaticiens, qui n'ont jamais appris le LISP
ni le Forth. Mais ils ne sont devenus bons que quand il ont perçu la
structure "LISP" au travers des programmes...
Les langages "presque humains" sont une excellente chose. Ils
permettent à des informaticiens d'écrire des programmes que des
non-informaticiens pourront relire et un peu comprendre. Mais, il faut
que vous compreniez deux choses :
- Si un programme vous semble lisible ; vous comprenez ce qu'il
fait faire à l'ordinateur rien qu'en le regardant distraitement, c'est
parce que l'informaticien qui l'a écrit l'a bien voulu. Cela lui a
demandé du travail supplémentaire. Et ce n'est pas toujours possible...
- Vous comprenez un programme que vous lisez... Il vous semble même
parfaitement clair dans chacune de ses lignes... Un informaticien vous
confirme que vous ne vous trompez pas... Ensuite, derrière le dos de
l'informaticien, vous faites des modifications au programme... Vous
faites cela de façon rigoureuse et en veillant à ce que le programme
reste toujours aussi clair et parfaitement rationnel. Hé bien, le
programme ne fonctionnera probablement plus !
Je ne prétends pas qu'il est mauvais de programmer dans les langages
qui ont l'air lisibles par les humains. Je le fais chaque fois que
c'est nécessaire. Je n'utilise plus de calculatrice HP depuis
longtemps... À présent je fais mes calculs scientifiques avec une
ravissante TI 200, très douée en langage humain. Regardez la belle
racine carrée :
Pour faire des calculs avec mon ordinateur, j'utilise un interpréteur
Haskell, qui lui aussi me permet d'écrire les calculs de façon presque
humaine :
GHCi, version 6.8.2:
http://www.haskell.org/ghc/ :? for
help
Loading package base ...
linking ... done.
Prelude> 2 + 2
4
Prelude> 45 + 32 + 78 + 6.8 + 9
170.8
Prelude> sin (2.36)
0.7044107657701763
Prelude> 2 + 3 * 5
17
Prelude> sqrt( 34.2^2 + 2.89^2 )
34.3218895167501
Prelude> 2 * pi * 10.1^2
640.9477331853896
Prelude>
|
Il y a des tas de bonnes raisons d'utiliser autre chose que le LISP :
- Si vous programmez dans une entreprise... il vaut mieux utiliser
un langage que les non-informaticiens peuvent relire.
- Certains langages sont fournis avec des possibilités techniques
que le LISP n'a pas en standard.
- Certains langages permettent d'écrire des programmes beaucoup
plus rapides qu'en LISP.
Je vais utiliser le LISP, parce qu'il est proche de la façon dont la
machine travaille et cela va donc vous permettre de comprendre les
véritables rouages des choses. Il y a d'autres raisons :
- On peut faire faire à un microprocesseur des choses absolument
hallucinantes. Ils acceptent n'importe quoi... Cela permet parfois de
faire des prodiges. Mais... la plupart des langages de programmation
"pseudo-humains" limitent très fort ce qu'on peut faire faire à la
machine. Ce n'est pas très créatif... Le LISP, au contraire, vous
permet de faire n'importe quoi (en bien comme en mal).
- Le LISP est un langage "à faible empoisonnement". Voyez-vous, un
logiciel a souvent une période de croissance, une période de stabilité
et puis une période de déliquescence. Pendant la période de croissance,
on élimine les problèmes du logiciel. Il devient stable... Et puis...
commencent à apparaître des problèmes que l'on ne comprend pas bien.
Ils deviennent de plus en plus nombreux... cela coûte de plus en plus
cher pour garder le logiciel à flots... Les clients sont de plus en
plus mécontents. Finalement, presque plus rien ne fonctionne... On se
sent désarmé et à bout de nerfs. On doit jeter le logiciel et en
recommencer un autre à partir de zéro ! On a par exemple reproché au
Forth d'être "un langage à déliquescence quasi-immédiate". Le Forth
vous permet d'écrire des petits logiciels fabuleux en quelques minutes.
Mais... si vous essayez de les améliorer cela tourne très rapidement à
la catastrophe. Cela peut se passer sur quelques heures de temps ! Le
langage Ada, par contre, est très robuste contre la déliquescence des
logiciels. On l'utilise pour programmer les centrales nucléaires, les
avions et les fusées spatiales... On peut même dire qu'un logiciel en
Ada fait dans les règles, n'a pas de période de croissance. Par
exemple, quand je programme en langage C ou C++, je fais tout le temps
des sauvegardes de mes programmes. Il arrive régulièrement qu'un
programme cesse de fonctionner correctement et que je me sente désarmé
pour en rechercher la cause. Je préfère alors repartir de la sauvegarde
la plus proche... Par contre quand je programme en Ada, je fais des
modifications au programme en toute confiance. Je n'ai jamais eu de
problème d'empoisonnement... Le LISP, quant à lui, est relativement
sain de ce point de vue. Mais, surtout : il fait prendre de bonnes
habitudes. Parce que, le problème est aussi une question de discipline.
Certaines entreprises ont écrit de grands logiciels en Forth sans
rencontrer d'empoisonnement. Pour obtenir cela, leurs informaticiens
s'étaient astreints à des règles draconiennes. Comme à l'armée... Un
exemple typique est la programmation des dernières sondes américaines
lancées vers la planète Mars. Les ingénieurs qui les ont mises au
point,
ont obtenu de ne plus être obligés de programmer en Ada. Ils ont
utilisé le langage C. En principe, le langage C est juste bon pour
écrire des jeux vidéo et des logiciels grand public... Pourtant, cela a
fonctionné. Les sondes ont eu de bogues mais rien de grave. Les
ingénieurs s'étaient astreints à ne jamais utiliser les ressources du C
qui sont typiquement les causes des problèmes. Cela a rendu les
logiciels des sondes quasi préhistoriques. Mais ça fonctionne...
Vous trouverez des moteurs LISP gratuits à télécharger un peu partout.
Ce n'est pas plus compliqué à installer qu'un jeu vidéo. Enfin parfois
si. Faites-vous aider... Sinon, voici une page sur Internet qui propose
un interpréteur LISP en ligne (rudimentaire). Il existe quelques autres
pages du
même type mais
leurs interpréteurs LISP sont vraiment trop limités...
Attention : j'utilise le "Common LISP". C'est le LISP "standard",
globalement le plus utilisé. Si vous utilisez une LISP différent, cela
devrait fonctionner pour les exemples simples de ce texte. Mais...
Plus précisément, j'utilise le système Common LISP "GNU" : le CLISP.
Voici ce qu'il affiche au lancement :
i i i i i i
i ooooo
o ooooooo
ooooo ooooo
I I I I I I
I 8 8
8
8 8 o
8 8
I \ `+'
/ I
8
8
8 8
8 8
\
`-+-' /
8
8
8 ooooo 8oooo
`-__|__-'
8
8
8 8 8
|
8 o
8
8 o 8 8
------+------
ooooo 8oooooo ooo8ooo
ooooo 8
Welcome to GNU CLISP 2.44.1
(2008-02-23) <http://clisp.cons.org/>
Copyright (c) Bruno Haible,
Michael Stoll 1992, 1993
Copyright (c) Bruno Haible,
Marcus Daniels 1994-1997
Copyright (c) Bruno Haible,
Pierpaolo Bernardi, Sam Steingold
1998
Copyright (c) Bruno Haible,
Sam Steingold 1999-2000
Copyright (c) Sam
Steingold, Bruno Haible 2001-2008
Type :h and hit Enter for
context help.
[1]>
|
À l'endroit du [1]>, peut être tapée une commande LISP.
Commençons par taper des commandes très simples. N'oubliez pas de taper
la touche Enter de votre clavier après chacune :
7
5
4.532
Cela donne ceci :
i i i i i i
i ooooo
o ooooooo
ooooo ooooo
I I I I I I
I 8 8
8
8 8 o
8 8
I \ `+'
/ I
8
8
8 8
8 8
\
`-+-' /
8
8
8 ooooo 8oooo
`-__|__-'
8
8
8 8 8
|
8 o
8
8 o 8 8
------+------
ooooo 8oooooo ooo8ooo
ooooo 8
Welcome to GNU CLISP 2.44.1
(2008-02-23) <http://clisp.cons.org/>
Copyright (c) Bruno Haible,
Michael Stoll 1992, 1993
Copyright (c) Bruno Haible,
Marcus Daniels 1994-1997
Copyright (c) Bruno Haible,
Pierpaolo Bernardi, Sam Steingold
1998
Copyright (c) Bruno Haible,
Sam Steingold 1999-2000
Copyright (c) Sam
Steingold, Bruno Haible 2001-2008
Type :h and hit Enter for
context help.
[1]> 7
7
[2]> 5
5
[3]> 4.532
4.532
[4]>
|
Chaque fois que j'ai tapé un nombre, le LISP a donné une réponse, qui
est ce nombre. Cela doit vous sembler extravagamment utile mais... ne
pensez pas pour autant que le LISP ne fait rien. Essayez par exemple de
taper ceci :
007
Le LISP répond par simplement 7 :
I \ `+' /
I
8
8
8 8
8 8
\
`-+-' /
8
8
8 ooooo 8oooo
`-__|__-'
8
8
8 8 8
|
8 o
8
8 o 8 8
------+------
ooooo 8oooooo ooo8ooo
ooooo 8
Welcome to GNU CLISP 2.44.1
(2008-02-23) <http://clisp.cons.org/>
Copyright (c) Bruno Haible,
Michael Stoll 1992, 1993
Copyright (c) Bruno Haible,
Marcus Daniels 1994-1997
Copyright (c) Bruno Haible,
Pierpaolo Bernardi, Sam Steingold
1998
Copyright (c) Bruno Haible,
Sam Steingold 1999-2000
Copyright (c) Sam
Steingold, Bruno Haible 2001-2008
Type :h and hit Enter for
context help.
[1]> 7
7
[2]> 5
5
[3]> 4.532
4.532
[4]> 007
7
[5]> |
Cela montre que le LISP "sait" que vous n'avez pas juste tapé 0 0 et 7.
Il a "réalisé" que vous avez tapé le nombre 7.
Une deuxième preuve que le LISP "travaille" : si vous tapez une
fraction comme 3/12 le LISP va
automatiquement la simplifier :
`-__|__-'
8
8
8 8 8
|
8 o
8
8 o 8 8
------+------
ooooo 8oooooo ooo8ooo
ooooo 8
Welcome to GNU CLISP 2.44.1
(2008-02-23) <http://clisp.cons.org/>
Copyright (c) Bruno Haible,
Michael Stoll 1992, 1993
Copyright (c) Bruno Haible,
Marcus Daniels 1994-1997
Copyright (c) Bruno Haible,
Pierpaolo Bernardi, Sam Steingold
1998
Copyright (c) Bruno Haible,
Sam Steingold 1999-2000
Copyright (c) Sam
Steingold, Bruno Haible 2001-2008
Type :h and hit Enter for
context help.
[1]> 7
7
[2]> 5
5
[3]> 4.532
4.532
[4]> 007
7
[5]> 3/12
1/4
[6]>
|
Comment faire calculer 2 + 2 à l'ordinateur,
en langage LISP ?
Tapez ceci, et puis la touche "Enter" de votre clavier :
(+
2 2)
Cela donne 4, le résultat attendu :
------+------
ooooo 8oooooo ooo8ooo
ooooo 8
Welcome to GNU CLISP 2.44.1
(2008-02-23) <http://clisp.cons.org/>
Copyright (c) Bruno Haible,
Michael Stoll 1992, 1993
Copyright (c) Bruno Haible,
Marcus Daniels 1994-1997
Copyright (c) Bruno Haible,
Pierpaolo Bernardi, Sam Steingold
1998
Copyright (c) Bruno Haible,
Sam Steingold 1999-2000
Copyright (c) Sam
Steingold, Bruno Haible 2001-2008
Type :h and hit Enter for
context help.
[1]> 7
7
[2]> 5
5
[3]> 4.532
4.532
[4]> 007
7
[5]> 3/12
1/4
[6]> (+
2 2)
4
[7]>
|
Quelques remarques :
- Vous pouvez exprimer ce (+ 2 2)
ainsi : "dans la machine d'addition, on met 2 et
puis on met encore 2."
- Il est bien sûr naturel que le LISP ait répondu 4 à cette
commande... mais sachez que toutes
les commandes en LISP donnent une réponse. Si une commande ne
donne pas un retour, c'est que le LISP est mort... Programmer en LISP,
c'est
presque l'art de gérer les réponses des commandes...
Vous me direz que les langages informatiques qui permettent d'écrire
simplement 2 + 2 en langage humain comme à
l'école, c'est tout de même plus simple et plus pratique que
de devoir écrire (+ 2 2)
Peut-être... Mais dites-vous
bien que cela aurait pu être pire :
- Vous n'avez pas eu à vous soucier de l'endroit dans l'ordinateur
où ont été stockés le 2, le 2 et la réponse 4. En langage "Assembleur",
par contre,
non seulement vous auriez dû prendre le temps de décider où ils
seraient stockés mais vous auriez aussi du décider si vous les stockiez
dans la "mémoire RAM" de l'ordinateur ou dans les "registres" du
microprocesseur. Tout un
programme. Dites merci au LISP de s'être chargé de ces questions
élémentaires à votre place !
- Le LISP s'est également chargé de vider la machine de sommation
avant de commencer les opérations. Ben oui, parce que imaginez que la
machine de sommation contenait 83 avant de commencer. Vous ajoutez 2 et
puis vous ajoutez encore 2, hé bien le résultat c'est 87...
- Le 2 et le 2, ce sont des nombres entiers. Imaginons maintenant
que vous demandez (+ 2.4
3.1416). Houlalaaaa... mais
faire la somme de nombres décimaux, c'est complètement différent pour
l'ordinateur ! Et beaucoup plus compliqué. Le LISP fait le
nécessaire et vous n'avez pas à vous préoccuper de ces nuances
extrêmes. Alors plus fort encore : vous demandez (+ 2
3.1416) soit la somme d'un nombre entier et d'un nombre décimal.
Vous
n'imaginez pas les abîmes de tracas que cela engendre. Certains
langages de programmation, comme l'Ada, refusent carrément de vous
suivre sur ce terrain instable. Une fois de plus, le LISP tout mignon
règle les problèmes pour vous : il prend la décision de transformer le
2 en 2,0 et tout se passe bien.
Vous pouvez même lui demander ceci :
(
+ 45 32 78 6.8 9 )
Il calculera la somme 45 + 32 + 78 + 6,8 + 9 et
affichera comme résultat 170,8
Le fait que vous pouvez lui donner une liste de nombres à sommer n'est
pas étranger au fait que "LISP" vient de "LISt Processor", ce qui veut
dire "Processeur de LIStes" en français. D'une façon générale, le
travail de l'ordinateur consiste à gérer des listes de
données. Le LISP est donc, comme promis, très proche de la façon dont
l'ordinateur travaille.
Les trois autres opérations de base sont bien entendu disponibles :
(-
3 9)
(* 4 5)
(/ 2 4)
Cela donnera comme résultats -6, 20 et 1/2. Notez juste ceci :
- La multiplication est notée par *
- Si la division ne donne pas un résultat entier, il est gardé sous
forme de fraction. Donc, le résultat de l'opération (/ 2 4) ci-dessus donne comme
résultat 1/2 et non pas 0 ou 0,5. C'est très pratique à l'usage mais il
faut s'habituer...
L'informatique est anglo-saxonne. Une première raison à cela est
qu'elle a véritablement démarré aux USA, pendant la Seconde Guerre
Mondiale. Une deuxième raison est que les gouvernements européens, en
particulier français, n'ont jamais beaucoup soutenu la Recherche. (Si
Clément Ader avait été encouragé, le premier avion aurait été
français...) En conséquence, en informatique les nombres décimaux
s'écrivent toujours comme en anglais : avec un "." faisant fonction de
virgule.
Le nombre π s'écrit donc
3.1415927 et non 3,1415927
Si vous voulez qu'un résultat de division donne un nombre décimal,
faites en sorte qu'au moins un de ses arguments soit écrit de façon
décimale. Par exemple, pour que la division de 2 par 4 donne 0,5 et non
1/2, dites au LISP (/ 2.0 4)
ou (/ 2 4.0) ou encore (/ 2.0 4.0)
Si vous n'entendez rien aux calculs scientifiques, n'essayez pas de
comprendre en détail ce qui suit. Contentez-vous de vous en imprégner.
Vous disposez des fonctions mathématiques scientifiques de base. Par
exemple ceci vous donne la racine carrée de 49 :
(sqrt
49)
Le résultat sera 7.
Et ceci donne 2 3 :
(expt
2 3)
Le résultat est 8.
Dans les exemples ci-dessus, je n'ai utilisé que des nombres entiers.
Mais le Common LISP est très doué avec les nombres décimaux. Dans
certains langages informatiques, il est par exemple impossible de
demander 50,3. Ils n'acceptent que des exposants entiers...
Vous n'aurez pas ce type de soucis avec le Common LISP. Il mange tout...
Voici une liste de fonctions disponibles :
racine carrée
|
(sqrt
9)
|
exponentiation
|
(expt
2
3)
|
logarithme népérien
|
(log
2.35453)
|
exponentielle
|
(exp
1.0)
|
sinus
|
(sin
-3.1416)
|
cosinus
|
(cos
2.45)
|
tangente
|
(tan
0.163)
|
arcsinus
|
(asin
0.33)
|
arccosinus
|
(acos
-0.0023)
|
arctangente
|
(atan
0.3)
|
sinus hyperbolique
|
(sinh
45.56)
|
cosinus hyperbolique
|
(cosh
3)
|
tangente hyperbolique
|
(tanh
22.1)
|
| arcsinus hyperbolique |
(asinh
0.1)
|
| arccosinus hyperbolique |
(acosh
5.235)
|
| arctangente hyperbolique |
(atanh
0.5)
|
Un mot à propos de la "notation scientifique" des nombres décimaux.
Donnez par exemple ce calcul à faire au LISP, soit 320 :
(expt
3.0 20)
Le réponse sera 3.4867843E9
Qu'est ce que c'est que ce charabia ? Vous saviez déjà que le
"." est en réalité la virgule mais qu'est-ce que le "E" vient faire
dans l'histoire ? Et puis de toute façon, le résultat de 3 20
est bien plus grande que 3... Réponse : le "E9" veut en réalité dire x109 ou en d'autres
termes que le nombre 3,4867843 doit être multiplié par 1.000.000.000.
La réponse est donc 3.486.784.300 soit trois
milliards et quelques.
Un détail : le LISP vous a répondu 3,4867843x109 et j'ai
affirmé que c'était la même chose que 3.486.784.300 mais attention :
les deux zéros à la fin... je les ai inventés ! Le LISP n'a jamais
prétendu qu'il y a des zéros là. Le résultat exact est 3.486.784.401.
Par contrat, le LISP ne vous donne que les quelques premiers chiffres
des nombres décimaux...
Répétons :
À l'école, 700.000 peut
être écrit comme ceci : 7x105.
Le x105
voulant dire que "le 7 est déplacé
de 5 positions vers la gauche". De même, 8x10-2 est la même
chose que
0,08 tandis que 5,54356735x104
est la même chose que 55.435,6735.
En informatique,
on remplace le "x10..."
par la lettre "E"
(en majuscule ou en minuscule, peut
importe). Comme ceci : 7e5
8E-2 5.54356735e4.
| 1e1 |
veut dire
|
10 |
| 1e2 |
veut dire
|
100 |
| 1e0 |
veut dire
|
1 |
| 1e-1 |
veut dire
|
0,1 |
| 1e-2 |
veut dire
|
0,01 |
| 1e-10 |
veut dire
|
0,0000000001 |
En Lisp, à la place du
"E" on peut aussi utiliser "D" ou "L". La différence est que les
nombres avec "D" sont plus précis que ceux avec "E". Ceux avec "L" sont
encore plus précis. Mais... cela prend plus de place en mémoire et
demande plus de temps de calcul... Par exemple, si vous voulez la
racine carrée de 2, selon que vous écrirez (sqrt 2), (sqrt 2d0) ou (sqrt 2L0) vous
obtiendrez un résultat dont la précision est différente : 1.4142135, 1.4142135623730951d0 ou 1.4142135623730950488L0. Il y a
également une différence pour l'exposant maximal possible. Les maximums
sont de l'ordre de e38, d308 et L631285. Le
LISP n'acceptera pas 1e100 mais il acceptera très bien 1d100. Il
n'acceptera pas 1d-978 mais il acceptera sans sourciller 1L-978.
Il existe également un encodage encore moins précis que "E" : "S". (sqrt 2s0) donne comme résultat 1.41422s0. Par contre pour la partie
exponentielle, la limite est la même : e38.
Un détail cosmétique : si vous écrivez simplement 2, le LISP le prend
pour un nombre entier. Si vous voulez signifier expressément que le
nombre est décimal alors écrivez 2.0
ou 2e0. Vous pouvez aussi
écrire
2.0e0, 2.e0, 2d0, 2L0, 2.0L0... le LISP a été conçu pour
accepter un peu n'importe quoi, tant que cela reste conforme à une
certaine logique.
Une astuce : dans les réponses qu'il vous donne, le LISP distingue
soigneusement les nombres entiers et les nombres décimaux. Si vous
attendiez un entier comme réponse et vous voyez
apparaître quelque chose comme 294.0
alors il y a un problème. 294
est
un entier, par contre 294.0
n'est certainement pas un entier. D'un point de vue mathématique la
valeur numérique est
exactement la même mais pour le LISP ce sont deux choses très
différentes.
Un petit plus pour les pros en Maths : le Common LISP manie
parfaitement les nombres complexes. Ils s'écrivent comme ceci : #c(réel
imaginaire). Si vous
demandez la racine carrée de 4+7i en tapant (sqrt #c(4 7)) vous obtiendrez comme
résultat #C(2.4558356 1.4251769).
Bien entendu, (sqrt -1) donne
comme résultat #C(0 1).
Pour les moins pros en Maths : si le LISP vous donne comme résultat
d'un calcul un "machin" dans le genre #C(...
...),
cela veut dire que le résultat de ce calcul n'est pas exprimable par un
nombre "normal". Une calculatrice scientifique banale n'aurait
simplement pas accepté de faire ce calcul. Par exemple, on vous a
appris à l'école qu'on ne peut pas extraire la racine carrée d'un
nombre négatif. Mais, dans l'univers des nombres complexes cela a
parfaitement un sens. Si vous demandez (sqrt
-9), vous obtenez #C(0 3),
un nombre complexe... parce que le LISP essaye à tout prix de vous
faire plaisir. (Cela peut parfois entrainer des choses inattendues.
Supposons par exemple qu'un programme exécute le calcul (* (sqrt a) (sqrt b)). Vous vous
êtes dit que si "a" ou "b" contenaient un nombre négatif, le programme
planterait. Non seulement il ne plantera jamais mais de surcroit il
produit un résultat "normal" si "a" et "b" contiennent des nombres
négatifs. Par exemple, (* (sqrt -4)
(sqrt -9)) donne comme résultat -6.)
Vous pouvez ainsi calculer la majorité des expressions mathématiques
utilisées à l'école ou en entreprise. Vous avez une petite calculatrice
scientifique entre les mains...
Cette calculatrice... demande un certain apprentissage. Sur votre
calculatrice habituelle, pour calculer l'expression suivante, hé bien
vous tapez l'expression telle qu'elle :
2 + 3 x 5
Comme ceci : 2 +
3 x 5 =
Votre calculatrice est conçue pour d'abord calculer le 3 x 5 et ensuite ajouter
2, ce qui donne 17. Ses concepteurs ont fait le nécessaire pour qu'elle
effectue les
multiplications avant les additions, conformément aux usages.
En LISP,
par contre, vous devez détailler les opérations :
(+
2 (* 3 5))
Si vous voulez distinguer les parties avec des couleurs :
(+
2 (* 3
5))
Le (* 3 5)
est calculé en premier, à cause des parenthèses. Cela donne
15. L'expression devient donc (+ 2 15)
ce qui donne 17.
Ceci donnera exactement le même résultat :
(+
(* 3 5) 2)
Par contre ceci :
(*
2 (+ 3 5))
Le résultat sera 16, parce que le (+ 3 5) est calculé en premier.
Le LISP n'est pas conçu pour effectuer la multiplication avant
l'addition. Il se contente de regarder où vous avez placé les
parenthèses et d'effectuer en premier les opérations qui se trouvent
dans les parenthèses les plus profondes.
Une fonctionnalité souvent appréciée sur les calculatrices consiste à
pouvoir utiliser le résultat précédent dans un nouveau calcul. En LISP,
vous faites allusion au résultat précédent en utilisant l'étoile. Le résultat final de ces deux
calculs sera 67 :
(+
56 4)
(+ * 7)
Bien entendu, (+ 7 *)
aurait fonctionné tout aussi bien.
Le résultat final de ces deux-ci sera 120 :
(+
56 4)
(* * 2)
Vous trouvez peut-être qu'il est un peu malheureux d'utiliser l'étoile
à la fois pour la multiplication et pour rappeler le résultat
précédent. Heureusement, il n'y a pas de confusion possible. Attention
:
n'utilisez pas l'étoile pour rappeler le résultat précédent quand vous
écrivez des programmes.
Le calcul suivant :
S'écrira comme ceci en LISP :
(sqrt
(+ (expt 34.2 2) (expt 2.89 2)))
En couleurs :
(sqrt
(+ (expt 34.2 2)
(expt 2.89 2)))
Un détail important : imaginons que vous voulez faire le même calcul
mais avec 2,90 à la place de 2,89. Êtes-vous obligé de retaper la
formule entière ? Non : tapez quelques fois sur la touche curseur vers
le haut de votre clavier. Cela vous permet de retourner sur la formule,
la modifier et à nouveau la faire calculer en tapant Enter. (Sur
certains système LISP, vous devez taper Enter une première fois, quand
vous avez amené le curseur sur la formule, pour pouvoir commencer à la
modifier...)
Vous êtes à présent capables de vous servir du LISP comme d'une machine
à calculer scientifique. Traduire un calcul en LISP, c'est déjà faire
de la
programmation...
Vous pouvez simplement copier-coller les exemples de ce texte vers un
interpréteur LISP. Cela ne fonctionne pas avec tous les systèmes
LISP... Cela dit, quand il s'agit d'un exemple que vous ne comprenez
pas, le mieux à faire est de prendre le temps de le taper vous-mêmes.
L'idéal est de la taper de mémoire. Sinon, tapez-le en recopiant
visuellement. Vous constaterez que vos doigts sont parfois plus
intelligents que vous...
Si votre calculatrice scientifique est un peu performante, elle a des
mémoires dans lesquelles vous pouvez stocker des nombres. Comment
fait-on cela en LISP ? Commencez par décider de donner un nom aux
mémoires que vous voulez utiliser. Par exemple "rayon" et "hauteur"
pour les dimensions d'un cylindre :
(setf
rayon 10.1)
(setf hauteur 32.7)
Vérifiez que tout a bien fonctionné, en tapant les noms de ces mémoires
que vous venez de
demander à LISP de créer :
rayon
hauteur
S'il s'affiche 10,1 et 32,7, vous avez la preuve que les mémoires
existent bien et qu'elles contiennent les valeurs souhaitées. (Les
informaticiens ne disent pas "mémoires" mais "variables". Peu
importe...)
Par contre si vous demandez le contenu d'une mémoire qui n'existe pas
(pas encore...) :
inclinaison
Le LISP vous répondra par un message d'erreur. La mémoire "inclinaison"
n'a pas été définie, n'a pas de contenu... n'existe pas... Libre à vous
bien sûr de décider de la faire exister :
(setf
inclinaison 10)
Détail : si vous voulez assigner un contenu à plusieurs mémoires, vous
pouvez le faire par un seul setf
:
(setf
rayon 46 hauteur 8.5 inclinaison 9.5)
Voici ce que cela donne sur l'interpréteur CLISP :
Copyright (c) Sam Steingold, Bruno
Haible
2001-2008
Type :h and hit Enter for context help.
[1]> (setf rayon 10.1)
10.1
[2]> (setf hauteur 32.7)
32.7
[3]> rayon
10.1
[4]> hauteur
32.7
[5]> inclinaison
*** - EVAL: La variable
INCLINAISON n'a pas de valeur.
Rentrées possibles:
USE-VALUE
:R1 You may input a value to be used
instead of INCLINAISON.
STORE-VALUE
:R2 You may input a new value for
INCLINAISON.
ABORT
:R3 Abort main loop
Break 1 [6]> (setf inclinaison 10)
10
Break 1 [6]> (setf rayon 46
hauteur 8.5 inclinaison 9.5)
9.5
Break 1 [6]>
|
Nous en avons déjà parlé mais il faut insister : constatez que les
commandes setf
entrainent l'affichage d'un résultat. Comprenez bien : le travail de setf consiste à stocker par exemple
le nombre 10,1 dans la mémoire "rayon". OK... mais pourquoi ensuite
afficher ce nombre 10,1 ? À priori, c'est complètement inutile... Quand
vous tapez le calcul (+ 2 2)
et qu'il s'affiche 4, là oui c'est utile et nécessaire. Mais dans le
cas d'un setf, pourquoi diable donner un résultat... comme si on avait
donné un calcul à faire... Hé bien, c'est un fondement du LISP ! Toutes les commandes donnent un
résultat ! Parfois ce résultat est complètement inutile... c'est
un peu le cas avec ces setf...
Parfois même, ce résultat est absurde et inutilisable...
Pour bien vous montrer que le résultat rendu par (setf rayon 10.1) est rigoureusement
équivalent au résultat que rendrait par exemple (+ 4 6.1), vous pouvez écrire la
commande suivante :
(+ 4.1 (setf rayon 10.1))
Cela donne comme résultat 14,2, parce que (setf rayon 10.1)
donne comme résultat 10,1, ensuite de quoi (+ 4.1 10.1)
donne comme résultat 14,2. Et, en supplément, la mémoire "rayon"
contient à
présent le nombre 10,1...
Comment utiliser ces mémoires dans des calculs ? Tout bêtement. Voici
comment
calculer la surface du fond du cylindre :
(*
2 pi rayon rayon)
Cela veut dire 2 x
π x rayon x rayon ce
qui est une façon de calculer la formule de la surface d'un disque :
2 π r2
Vous auriez bien entendu pu écrire ceci à la place :
(*
2 pi (expt rayon 2))
Ce qui se traduirait littéralement par 2 x π x rayon 2
Vous pouvez imposer une autre valeur à la mémoire "rayon" :
(setf
rayon 10.8)
Vous pouvez également décider d'ajouter disons 0,7 au rayon, quelle que
soit sa valeur :
(setf rayon (+ rayon 0.7)
)
(+ rayon 0.7) donne
ici comme résultat 11,5 et cette valeur
est stockée dans la mémoire "rayon" par la grâce de setf, comme si vous
aviez donné la commande (setf
rayon 11.5)
Notez que quand vous voulez simplement ajouter un nombre à une mémoire,
vous pouvez juste taper ceci :
(incf
rayon 0.7)
C'est plus court...
Pour connaître la surface de ce nouveau disque, vous n'êtes pas obligé
de retaper en entier la formule (* 2
pi rayon rayon). Tapez quelques
fois sur la touche curseur vers le haut de votre clavier. Cela vous
permet de revenir sur la formule telle que vous l'aviez tapée. Tapez
Enter et
le tour est joué. (Cela ne fonctionne pas avec tous les systèmes. Sur
certains systèmes il faut taper Enter deux fois...)
C'est pratique... Mais calculons à présent le volume du cylindre :
(*
2 pi rayon rayon hauteur)
Si cela vous semble plus lisible, n'hésitez pas à écrire ceci :
(*
(* 2 pi rayon rayon) hauteur)
En couleurs :
(* (* 2 pi rayon rayon)
hauteur)
Si vous tenez à ce qu'il soit explicite que le rayon est mis au carré :
(*
(* 2 pi (expt rayon 2)) hauteur)
Le LISP s'en fiche... ces différences sont là pour vous
faire plaisir. Et n'hésitez pas à faire des permutations puisque la
multiplication est commutative :
(* hauteur
(* (expt rayon 2) 2 pi))
Tous ces calculs sont corrects... Mais ils manquent un peu de panache.
Il est parfois plus élégant de procéder ainsi :
(setf
surface (* 2 pi rayon rayon))
(* surface hauteur)
On calcule sagement la surface. Ensuite on calcule sagement le volume...
Attention ! Supposons que vous changez à présent à nouveau la valeur du
rayon :
(setf
rayon 11.4)
Ensuite, vous vous contentez de faire ce calcul :
(*
surface hauteur)
Vous obtiendrez la mauvaise réponse, parce que la mémoire "surface"
contient toujours le résultat précédent, pour un rayon de 10,8. Si vous
voulez obtenir la bonne réponse, vous devez faire refaire au LISP tous
les calculs qui y mènent (utilisez la touche curseur vers le haut) :
(setf
surface (* 2 pi rayon rayon))
(* surface hauteur)
Si vous tenez absolument à avoir un système qui aurait recalculé la
surface automatiquement, utilisez le Haskell... ou un tableur.
Vous tapez une commande à la fois... Si elle lui convient le LISP
l'exécute... Fort bien mais un "programme" informatique est en principe
une suite d'instructions. Comment faire exécuter au LISP une suite de
quelques instructions, en bloc ? Réponse : grâce à progn, comme le montre l'exemple
suivant :
(progn
(setf
rayon 3.8 hauteur 9.1)
(setf
surface (* 2 pi rayon rayon))
(* surface hauteur) )
En couleurs, c'est plus explicite :
(progn
(setf
rayon 3.8 hauteur 9.1)
(setf
surface (* 2 pi rayon rayon))
(*
surface hauteur) )
Les trois commandes seront exécutées l'une après l'autre. Le résultat
du progn sera le résultat de
la dernière commande. En d'autres termes :
il sera affiché le volume du cylindre. Les résultats des deux commandes
précédentes ne sont pas affichés ; "ils sont mis à la poubelle".
Ne soyez pas interpellés par le fait que le progn est scindé sur plusieurs
lignes. Quand vous taperez Enter après chaque ligne, vous verrez que le
LISP attend sagement que vous tapiez la ligne suivant. Il déduit par
lui-même ce qu'il doit faire, en comptabilisant les parenthèses :
i i i i i i
i ooooo
o ooooooo
ooooo ooooo
I I I I I I
I 8 8
8
8 8 o
8 8
I \ `+'
/ I
8
8
8 8
8 8
\
`-+-' /
8
8
8 ooooo 8oooo
`-__|__-'
8
8
8 8 8
|
8 o
8
8 o 8 8
------+------
ooooo 8oooooo ooo8ooo
ooooo 8
Welcome to GNU CLISP 2.44.1
(2008-02-23) <http://clisp.cons.org/>
Copyright (c) Bruno Haible,
Michael Stoll 1992, 1993
Copyright (c) Bruno Haible,
Marcus Daniels 1994-1997
Copyright (c) Bruno Haible,
Pierpaolo Bernardi, Sam Steingold 1998
Copyright (c) Bruno Haible,
Sam Steingold 1999-2000
Copyright (c) Sam
Steingold, Bruno Haible 2001-2008
Type :h and hit Enter for
context help.
[1]> (progn
(setf rayon 3.8 hauteur 9.1)
(setf surface (* 2 pi rayon rayon))
(* surface hauteur) )
825.6357
[2]>
|
Vous pouvez faire un reproche à ce programme : il place dans la mémoire
"surface" la surface de la base du cylindre. C'est pratique, parce que
vous pouvez alors utiliser "surface" pour un autre calcul... ou
simplement pour afficher ce que contient "surface"... Il n'en va
pas de même pour le volume. Le volume est affiché à la fin de
l'évaluation de ce programme mais... il ne se trouve pas dans une
mémoire. Il n'est pas "sous la main"... Le remède est simple :
(progn
(setf
rayon 3.8 hauteur 9.1)
(setf
surface (* 2 pi rayon rayon))
(setf volume (* surface hauteur))
volume )
Dans le programme ci-dessus, la dernière ligne est inutile. La
précédente donne elle aussi comme résultat le contenu de "volume"...
donc si on
efface la dernière, le résultat est exactement le même : le volume est
affiché :
(progn
(setf
rayon 3.8 hauteur 9.1)
(setf
surface (* 2 pi rayon rayon))
(setf
volume (* surface hauteur)) )
Vous pourriez me faire le reproche suivant : pourquoi privilégier le
volume par rapport à la surface ? Ne serait-il pas plus utile
d'afficher comme résultat la surface et le volume ? La façon la
plus
simple pour satisfaire cela consiste à grouper les deux dans une liste :
(progn
(setf
rayon 3.8 hauteur 9.1)
(setf
surface (* 2 pi rayon rayon))
(setf volume (* surface hauteur))
(list
surface volume) )
Cela donne comme résultat (90.729195
825.6357) ce qui veut
donc dire que vous avez une surface de 90,7 et un volume de 826.
Le programme suivant fait exactement la même chose. Ne perdez pas trop
de temps à essayer de comprendre pourquoi :
(progn
(setf
rayon 3.8 hauteur 9.1)
(list
(setf
surface (* 2 pi rayon rayon))
(setf volume (* surface
hauteur))
) )
Vous pouvez scinder ce genre de ligne un
peu trop longue :
(progn
(setf
rayon 3.8 hauteur 9.1)
(list
(setf
surface (* 2 pi rayon rayon))
(setf volume (*
surface hauteur))
) )
Vous êtes très content d'avoir ce petit programme sous la main. Il
suffit de tapoter la touche curseur vers le haut pour le rappeler et le
modifier ; lui faire calculer des surfaces et des volumes pour d'autres
valeurs... Vous pouvez lui ajouter autant d'autres calculs/commandes
que vous le souhaitez... Mais... il y a tout de même un petit problème
: vous êtes enthousiasmé qu'il vous affiche la surface et le volume et
de surcroit qu'il les mémorise dans "surface" et dans "volume"... mais
il ne vous convient pas que le contenu de la mémoire "rayon" soit
modifié. Vous utilisez "rayon" pour autre chose. Cela vous dérange que
son contenu soit modifié chaque fois que vous faites fonctionner ce
programme. Par exemple, dans cette suite de calculs, vous auriez voulu
que le dernier calcul se fasse pour un rayon de 12 :
(*
pi 42)
(setf rayon 12)
(+ 45.4 123 44.1 rayon)
(progn
(setf
rayon 44.1 hauteur 123)
(setf
surface (* 2 pi rayon rayon))
(setf
volume (* surface hauteur)) )
(* pi rayon)
Évidemment, dans le programme vous pourriez utiliser à la place de
"rayon" un
nom de mémoire au hasard :
(progn
(setf
rayontralalayoupie 3.8 hauteur 9.1)
(setf
surface (* 2 pi rayontralalayoupie
rayontralalayoupie))
(setf volume (* surface hauteur))
(list
surface volume) )
Ce n'est pas prudent... et cela rend votre programme moins lisible,
plus
difficile à retoucher... La bonne solution, ce serait de pouvoir
continuer à utiliser "rayon" mais de pouvoir spécifier au LISP que
c'est juste une mémoire utilisée localement dans le petit programme. On
l'oublie dès que l'exécution du programme est terminée... Et surtout :
s'il existe une autre mémoire "rayon" globale, il ne faut pas modifier
son contenu. La solution est let
:
(let
(rayon)
(setf
rayon 3.8 hauteur 9.1)
(setf
surface (* 2 pi rayon rayon))
(setf volume (* surface hauteur))
(list
surface volume) )
Ainsi, si vous faites exécuter ces trois choses :
(setf rayon 64)
(let (rayon)
(setf
rayon 3.8 hauteur 9.1)
(setf
surface (* 2 pi rayon rayon))
(setf volume (* surface
hauteur))
(list surface volume) )
rayon
La dernière affichera le nombre 64, parce que "rayon" contient toujours
le nombre 64. L'usage local
d'une mémoire "rayon" dans le
let, n'a pas eu d'effet "sur
le monde global".
Vous pouvez bien entendu protéger "hauteur" de la même façon :
(let
(rayon hauteur)
(setf
rayon 3.8 hauteur 9.1)
(setf
surface (* 2 pi rayon rayon))
(setf volume (* surface
hauteur))
(list
surface volume) )
Ce n'est pas essentiel, mais let
permet d'assigner une valeur aux
mémoires
locales. Cela permet d'économiser une ligne... :
(let
( (rayon 3.8)
(hauteur 9.1) )
(setf
surface (* 2 pi rayon rayon))
(setf volume (* surface
hauteur))
(list
surface volume) )
Cela permet d'écrire le programme en deux lignes, comme ci-dessous. Ce
programme fait tout ce qui est demandé : il stocke la surface et le
volume dans deux mémoires "surface" et "volume" et il les affiche sous
forme d'une petite liste. Si des mémoires "rayon" ou "hauteur"
existaient avant l'exécution de ce petit programme, leurs contenus ne
sont pas modifiés.
(let
(
(rayon 3.8) (hauteur 9.1) )
(list (setf
surface (* 2 pi rayon rayon))
(setf volume (* surface
hauteur))
) )
Notez une chose de la plus grande importance : dans l'exemple
ci-dessus, le calcul de la surface
est effectué avant
celui du volume... Il en va toujours
ainsi. Les éléments de la liste sont évalués dans l'ordre où ils
apparaissent. LISP ne "réfléchit" pas pour se dire "hmmm... si je veux
calculer le volume il faut manifestement que je calcule d'abord la
surface..." Le LISP effectue les opérations dans l'ordre où
elles apparaissent. Si vous aviez placé le calcul du volume
avant celui
de la surface, hé bien simplement le programme n'aurait pas fonctionné
correctement.
À vous de décider ce qui est le plus pratique pour l'usage que vous en
faites. Mais ne pensez pas que le dernier exemple est le plus "pro". Un
vrai pro écrit avant tout des choses claires et lisibles. Il n'écrit
des choses hirsutes que quand cela est inévitable.
Répétons encore une fois ce fondement du LISP : tout ce qui
donne un nombre comme résultat, peut être utilisé à la place d'un
nombre.
Si la mémoire "k"
contient un nombre, la taper sous forme de commande donne son contenu :
k
Donc les calculs
suivants sont
corrects :
(+
2 k)
(+ k 2)
(+ k k)
Si vous vous servez de
setf
pour stocker un nombre dans une mémoire, le résultat du setf est le nombre que vous avez
stocké. Donc la
commande suivante va faire deux choses : stocker 5
dans la mémoire "kilimandjaro" et afficher comme résultat le nombre 7 :
(+ 2 (setf kilimandjaro 5))
Si vous voulez stocker ce nombre 7 dans une mémoire "ghkjy", ne vous
gênez pas :
(setf ghkjy (+ 2 (setf kilimandjaro 5)))
Le tout pourrait à son tour servir dans un autre calcul ou dans un
autre setf... La commande
suivante affiche
10 comme résultat et place 5 dans "kilimandjaro", 7 dans
"ghkjy" et 3 dans "bonjour" :
(+
(setf bonjour 3) (setf ghkjy (+ 2 (setf kilimandjaro 5))))
Mais... et si nous faisons ceci ? :
(+
(setf kilimandjaro 3) (setf kilimandjaro (+ 2 (setf kilimandjaro 5))))
Les opérations seront effectuées de gauche à droite et dans l'ordre
imposé par les
parenthèses. "kilimandjaro" prendre successivement les valeurs 3, 5 et
puis 7. Le résultat affiché sera 10.
Il est supposé être plus élégant d'écrire cela comme ceci :
(+
(setf kilimandjaro 3)
(setf kilimandjaro
(+ 2 (setf kilimandjaro 5))
) )
Un nom de mémoire, un calcul ou un setf
ne sont pas les seules
choses capables de produire un nombre. Un progn ou un let peuvent également produire un
nombre (si leur dernière commande produit un nombre). Par exemple le progn suivant produit comme résultat
23 :
(progn
(setf a 45)
(setf b (+a 1))
(setf c (/ b 2))
c )
(La dernière ligne est inutile. Peu importe...) (Le progn a également comme effet de
stocker des valeurs dans les mémoires "a", "b" et "c". Peu importe...)
Vous pouvez donc écrire ceci :
(+ 2 (progn
(setf
a 45)
(setf
b (+a 1))
(setf
c (/ b 2))
c ) )
Cela affichera comme résultat 25. Et le progn a toujours également pour
effet de
stocker des valeurs dans les mémoires "a", "b" et "c".
Mais réécrivons cela sans la dernière ligne du progn, afin de ne pas
nous faire passer pour des incompétents ou pour des programmeurs qui
essayent d'être lisibles à tout prix... :
(+
2 (progn
(setf
a 45)
(setf
b (+a 1))
(setf
c (/ b 2)) ) )
Vous pouvez imbriquer absolument n'importe quoi. Tant que le LISP
obtient un nombre quand il attend un nombre, "il est content". Dans
l'exemple suivant, il fait la somme du résultat d'un setf et du résultat d'un progn. Le résultat du setf est lui-même le résultat d'un let . Essayez de trouver par
vous-mêmes quelles mémoires se voient attribué quel contenu et quel est
le résultat final de la somme :
(+ (setf a (let (a b)
(setf a 45)
(setf b 363)
(+ a b) ) )
(progn
(setf alpha 48)
(setf alpha (/ alpha 2)) ) )
Vous avez une raison d'être jaloux. Le LISP propose des fonctions
mathématiques. Par exemple, pour connaître la racine carrée de 99 vous
utilisez la fonction sqrt,
comme ceci :
(sqrt
99)
Mais... pouvez-vous vous-mêmes ajouter de nouvelles fonctions
mathématiques ?! Par exemple une fonction qui donnerait la surface d'un
disque si on lui en donne le rayon ? Mais bien sûr. Vous "expliquez" la
fonction au LISP grâce à defun,
comme ceci :
(defun
surface-du-disque-de-rayon (schmilblick) (* 2 pi schmilblick
schmilblick))
En couleurs :
(defun
surface-du-disque-de-rayon (schmilblick)
(* 2 pi schmilblick
schmilblick))
Vous l'utilisez comme ceci :
(surface-du-disque-de-rayon
9.7)
Pouf, cela vous donne la surface d'un cercle de rayon 9,7. Vous pouvez
l'utiliser autant de fois que vous le désirez :
(surface-du-disque-de-rayon
145)
(surface-du-disque-de-rayon
0.35)
(surface-du-disque-de-rayon
7.73e9)
(surface-du-disque-de-rayon
-3.533)
Vous pouvez bien entendu l'appliquer au contenu de la mémoire "rayon" :
(surface-du-disque-de-rayon
rayon)
Où à n'importe quoi qui produit un nombre, bien entendu :
(surface-du-disque-de-rayon
(+ 7 pi (setf
a (sqrt 64))))
Pourquoi ai-je choisi un nom de mémoire comme "schmilblick" pour
contenir le rayon ? Parce que je voulais impliciter le fait que le
choix du nom de cette mémoire temporaire n'a vraiment pas beaucoup
d'importance. On pourrait dire que ce schmilblick est "quelque chose"
et que la fonction doit calculer 2 x
π x "ce
quelque chose" x "ce
quelque chose ".
Ces quatre autres définitions de la fonction, sont rigoureusement aussi
correctes :
(defun
surface-du-disque-de-rayon ( hululement ) (* 2 pi hululement hululement
))
(defun
surface-du-disque-de-rayon ( Fyu78J ) (* 2 pi Fyu78J Fyu78J ))
(defun
surface-du-disque-de-rayon ( Fyu78J ) (* Fyu78J pi Fyu78J 2 ))
(defun surface-du-disque-de-rayon (
masse ) (* 2 pi masse masse ))
La quatrième définition ferait grincer des dents n'importe quel
instituteur bien né. Confondre masse et longueur... Mais le LISP n'est
pas conçu pour se préoccuper de cela.
Tout ce qui compte pour lui est de pouvoir déduire ce qu'il doit faire
avec quoi. Il n'a pas la moindre notion de ce que sont des masses et
des longueurs. (Par contre en Ada, vous êtes prié de définir ces
choses, pour que le système puisse vous contraindre à ne jamais les
confondre.)
Le nom de la fonction, lui aussi, pourrait être n'importe
quoi :
(defun
hhh87oyoyo ( Fyu78J ) (* 2 pi Fyu78J Fyu78J ))
(hhh87oyoyo 43.9)
Bien entendu, comme nous voulons écrire des programmes lisibles, nous
préférons utiliser des noms explicites :
(defun
surface-du-disque-de-rayon ( rayon ) ( * 2 pi rayon rayon
) )
Nous pouvons à présent calculer le volume du cylindre de la façon
suivante
:
(*
(surface-du-disque-de-rayon rayon) hauteur)
Soyons fous, définissons une formule pour calculer le volume :
(defun
volume-du-cylindre-de-rayon-et-hauteur (r h)
(*
(surface-du-disque-de-rayon r) h) )
Vous l'utilisez comme ceci :
(volume-du-cylindre-de-rayon-et-hauteur
24.4 100.5)
(volume-du-cylindre-de-rayon-et-hauteur
rayon hauteur)
(volume-du-cylindre-de-rayon-et-hauteur
24.7 (* hauteur 2) )
Faites attention à quelque chose : dans la définition de
volume-du-cylindre-de-rayon-et-hauteur, j'utilise la définition de
surface-du-disque-de-rayon. C'est rationnel... Mais c'est aussi une
décision à prendre. Si je modifie la façon dont
surface-du-disque-de-rayon fait son calcul, cela impactera
automatiquement les résultats produits par
volume-du-cylindre-de-rayon-et-hauteur.
En principe c'est une bonne chose. Mais, si j'avais voulu éviter cela,
alors je pouvais définir la fonction
volume-du-cylindre-de-rayon-et-hauteur sans utiliser
surface-du-disque-de-rayon :
(
defun volume-du-cylindre-de-rayon-et-hauteur (r h) (* 2 pi
r r h) )
Vous apprendrez, par l'expérience et les bons conseils de vos ainés, à
faire les bons choix quand ce type de question se pose...
Revenons un instant à la définition de surface-du-disque-de-rayon :
(defun
surface-du-disque-de-rayon (rayon) (* 2 pi rayon rayon))
Une question très importante... considérons cette suite de trois
commandes :
(setf
rayon 453)
(surface-du-disque-de-rayon 12)
rayon
Qu'est-ce que le LISP va répondre quand il recevra la troisième
commande ? Que contient la mémoire "rayon" ? La question est capitale
parce que la fonction surface-du-disque-de-rayon a été utilisée. Pour
faire son travail, elle a placé le nombre 12 dans une mémoire qui
s'appelle "rayon". Alors... qu'est ce que notre mémoire "rayon"
contient maintenant ? 453 ou 12 ?
Elle contient toujours 453 ! La mémoire "rayon" de la fonction, a
automatiquement été considérée comme une mémoire locale (exactement
comme une mémoire déclarée en en-tête d'un let).
Ce que la fonction surface-du-disque-de-rayon a tricoté avec "sa"
mémoire "rayon", n'a eu aucun impact sur le reste de
l'environnement. Dame, pensez un peu à l'horreur que ce serait. Vous
seriez obligés de
connaître par coeur les noms de toutes les mémoires qu'utilisent toutes
les fonctions. Genre : "ha je ne peux pas utiliser une mémoire nommée
"rayon" parce qu'il y a une des fonctions qui l'utilise déjà." Ce ne
serait pas vivable.
Pour voir, essayez ceci :
(
surface-du-disque-de-rayon )
Vous invoquez la fonction mais sans lui donner un paramètre... Cela ne
fonctionne pas. Le LISP va vous glapir son indignation. Vous lui avez
défini une fonction surface-du-disque-de-rayon qui prend un paramètre.
Ce paramètre, il le veut ! Sinon l'invocation de la fonction n'a pas de
sens pour lui.
Mais... libre à vous de définir une fonction qui ne prend pas de
paramètre :
(defun
surface-du-disque-de-rayon () (*
2 pi rayon rayon) )
(setf rayon 453)
(surface-du-disque-de-rayon)
Est-ce que le LISP va se plaindre du fait qu'il ne voit pas de quel
"rayon" il s'agit dans la formule (*
2 pi rayon rayon) ? Hé
bien non, pour une fois ils se comporte comme vous l'auriez souhaité :
il regarde dans la globalité autour de lui et il utilise sagement la
mémoire "rayon" que
vous avez définie ; il
calcule la surface pour un rayon de 453.
Détail vital : tout comme un progn
ou un let, une fonction peut
contenir une suite de commandes. Le résultat de la fonction est le
résultat de la dernière commande :
(
defun volume-du-cylindre-de-rayon-et-hauteur (r h)
(setf
surface (* 2 pi
r r)
(setf
volume h)
volume
)
Autre détail vital : le jeu des parenthèses ! En langage humain, ces
expressions mathématiques reviennent au même :
2+2
(2+2) (2)+2
(2)+(2) (((2)+2))
((((2))))+(2)
Ces parenthèses sont inutiles mais pas illégales. Parfois, ajouter des
parenthèses de la sorte est utile, pour la lisibilité... En LISP, par
contre, chaque parenthèse ajoutée ou enlevée change complètement le
contexte. Si par exemple vous tapez ceci :
a
Vous aurez comme retour le contenu de la mémoire "a". Si par contre
vous tapez cela entre parenthèses :
(a)
Vous invoquez une fonction dont le nom est "a" ! Ce n'est pas du tout
la même chose... Et si vous tapez ceci :
((a))
Vous créez une liste dont le contenu sera le résultat des calculs de la
fonction "a"... Beaucoup de problèmes peuvent provenir de confusions
dans les parenthèses.
Il vous manque encore deux choses pour pouvoir écrire de vrais petits
programmes : les boucles et les conditions.
Commencez par réinstaurer la bonne version de
surface-du-disque-de-rayon :
(defun
surface-du-disque-de-rayon (rayon) (* 2 pi rayon rayon) )
(Et n'oubliez pas que dans les exemples qui suivent, cette fonction est
utilisée. Vous devrez la réapprendre au LISP chaque fois que vous le
redémarrez et que vous essayez un de ces exemples.)
Quelle est la surface d'un ensemble de 5 disques ayant des rayons de 1,
2, 3, 4 et 5 mètres ?
Une première réponse :
(+
(surface-du-disque-de-rayon
1)
(surface-du-disque-de-rayon 2)
(surface-du-disque-de-rayon 3)
(surface-du-disque-de-rayon 4)
(surface-du-disque-de-rayon 5) )
Rappelons que vous n'êtes pas tenus de placer tous les éléments
d'une commande sur une même ligne. La commande ci-dessus est décomposée
sur 5 lignes mais vous auriez pu écrire ceci, c'est pareil, juste moins
lisible :
(+
(surface-du-disque-de-rayon
1)
(surface-du-disque-de-rayon 2)
(surface-du-disque-de-rayon 3) (surface-du-disque-de-rayon 4) (surface-du-disque-de-rayon 5) )
Dans ce calcul de la surface totale des 5 disques, chacune des lignes
est différente ; 1, 2, 3, 4, 5... Où est le problème ? Le problème est
que ces lignes ne peuvent donc pas être répétées. Il faudrait
trouver le
moyen de faire le calcul mais en répétant cinq fois exactement la même chose.
Voici une proposition :
(setf
surface 0)
(setf r 1)
(setf surface (+ surface
(surface-du-disque-de-rayon r)))
(setf r (+ r 1))
(setf surface (+ surface
(surface-du-disque-de-rayon r)))
(setf r (+ r 1))
(setf surface (+ surface
(surface-du-disque-de-rayon r)))
(setf r (+ r 1))
(setf surface (+ surface
(surface-du-disque-de-rayon r)))
(setf r (+ r 1))
(setf surface (+ surface
(surface-du-disque-de-rayon r)))
(setf r (+ r 1))
surface
Vous pouvez bien sûr grouper tout cela dans un progn :
(progn
(setf
surface 0)
(setf r 1)
(setf surface (+ surface
(surface-du-disque-de-rayon r)))
(setf r (+ r 1))
(setf surface (+ surface
(surface-du-disque-de-rayon r)))
(setf r (+ r 1))
(setf surface (+ surface
(surface-du-disque-de-rayon r)))
(setf r (+ r 1))
(setf surface (+ surface
(surface-du-disque-de-rayon r)))
(setf r (+ r 1))
(setf surface (+ surface
(surface-du-disque-de-rayon r)))
(setf r (+ r 1))
surface )
Vous pouvez tout aussi bien le grouper dans une déclaration de
fonction, ensuite de quoi vous lancez la fonction. C'est un peu lourd
mais... cela fonctionne :
(defun
fonction-de-test ()
(setf
surface 0)
(setf r 1)
(setf surface (+ surface
(surface-du-disque-de-rayon r)))
(setf r (+ r 1))
(setf surface (+ surface
(surface-du-disque-de-rayon r)))
(setf r (+ r 1))
(setf surface (+ surface
(surface-du-disque-de-rayon r)))
(setf r (+ r 1))
(setf surface (+ surface
(surface-du-disque-de-rayon r)))
(setf r (+ r 1))
(setf surface (+ surface
(surface-du-disque-de-rayon r)))
(setf r (+ r 1))
surface )
(fonction-de-test)
Peu importent ces progn et defun... Revenons à nos moutons et
examinons de plus près ces lignes répétées. Bon d'accord,
l'avant-dernière ligne est inutile. Peu importe. La
troisième ligne pouvait être raccourcie. Peu importe. Répéter autant de
fois des lignes identiques est un peu grotesque... Peu importe.
Ce qui importe, c'est qu'il est possible de demander au LISP qu'il se
charge lui-même de répéter 5 fois les deux lignes :
(setf
surface 0)
(setf r 1)
(dotimes (n 5)
(setf surface (+ surface (surface-du-disque-de-rayon r)))
(setf r (+ r 1)) )
surface
Le (dotimes (n 5) ... ) peut
se lire : "faire 5 fois ... "
Vous me direz que vous comprenez bien le rôle du chiffre 5 mais
vous ne voyez pas l'utilité du "n"... Ce "n" est une mémoire locale,
qui
prendra successivement les valeurs 0, 1, 2, 3, 4. Cela permet par
exemple de réécrire le programme comme ceci :
(setf surface 0)
(dotimes (n 6)
(setf surface (+ surface (surface-du-disque-de-rayon n))) )
surface
Il y a une petite perte de temps, puisqu'il sera calculé la surface
d'un disque de rayon 0... Ce qui compte est que seront bien calculées
ensuite les surfaces de disques de rayon 1, 2, 3, 4 et 5. Le vrai
problème, est que cette version du programme manque un peu de clarté.
On doit calculer 5 disques et on en calcule 6... Ce n'est pas grave
parce que le premier passe au bleu, vu qu'il a une surface de zéro...
c'est correct mais pas très sérieux ni parfaitement prudent. Il faut
mieux écrire ceci :
(setf
surface 0)
(dotimes
(n 5)
(setf r (+ n 1))
(setf surface (+
surface (surface-du-disque-de-rayon r))) )
surface
Ou ceci, c'est pareil :
(setf
surface 0)
(dotimes (n 5)
(setf surface (+ surface (surface-du-disque-de-rayon (+ n 1) ))) )
surface
Le monstre de tout à l'heure s'est considérablement dégonflé... Malgré
tout, la simple somme des cinq lignes... c'était plus simple, plus
explicite... Je suis de votre avis. Mais demandez-vous ce que vous
feriez s'il fallait faire la somme des surfaces de 10.000 disques...
Tout comme progn, let et defun, un dotimes vous permet de faire
effectuer plusieurs fois une suite
arbitraire de commandes. Si cela vous chante vous pouvez faire calculer
ceci :
(setf
surface 0)
(dotimes
(n 10000)
(setf r (* 2 (+ n
1)))
(setf s (surface-du-disque-de-rayon r ))
(setf a (* s s))
(setf b (/ a 7))
(setf c (+ a b))
(setf surface
(+ surface c)) )
surface
N'oubliez pas ce détail important : la mémoire "n" est locale au dotimes.
N'hésitez pas à grouper tout cela dans un progn, c'est plus pratique :
(progn
(setf
surface 0)
(dotimes
(n 5)
(setf r (+
n 1))
(setf surface
(+
surface (surface-du-disque-de-rayon r))) )
surface )
Attention... comme j'utilise une mémoire "r" dans les calculs... mieux
vaut la déclarer comme étant locale, grâce à un let. Ainsi, une éventuelle mémoire
"r" globale ne sera pas détruite quand le programme est lancé :
(let
(r)
(setf
surface 0)
(dotimes
(n 5)
(setf r (+
n 1))
(setf surface
(+
surface (surface-du-disque-de-rayon r))) )
surface )
J'ai décidé que "surface" serait une mémoire globale, je ne
l'ai donc pas déclarée dans l'en-tête du let. Ainsi, après l'exécution du
programme, la mémoire "surface" contient le résultat du calcul
effectué.
Si vous voulez insister sur le fait que "r" n'est utilisé que dans le dotimes :
(progn
(setf
surface 0)
(dotimes
(n 5)
(let (r)
(setf
r (+
n 1))
(setf
surface
(+
surface (surface-du-disque-de-rayon r))) ) )
surface )
Est-ce que... on pourrait enlever la dernière ligne ? Après tout, la
commande juste au dessus de l'appel de la mémoire "surface"... est un (setf surface... Donc on pourrait
enlever la dernière ligne ?... Hé bien non. Parce que le dotimes ne donne pas comme résultat
le résultat du let. Il donne
un résultat qui est propre à lui-même et que vous ne pouvez pas encore
comprendre.
Juste pour bien mettre les points sur les i... regardez le programme
suivant. J'ai inséré une ligne totalement inutile, qui donne une valeur
s+1 à
la mémoire "a". Le résultat de cette opération est aussitôt effacé par
la ligne suivante, qui place la valeur de sxs dans la mémoire "a"...
(let (a b c s r)
(setf
surface 0)
(dotimes
(n 10000)
(setf r (* 2 (+ n
1)))
(setf s (surface-du-disque-de-rayon r ))
(setf a
(+ s 1))
(setf a (* s
s))
(setf b (/ a
7))
(setf c (+ a b))
(setf surface
(+ surface c)) )
surface )
Cela ne dérange pas du tout le LISP ! Dans le meilleur des cas, le
système LISP éliminera automatiquement les calculs engendrés par cette
ligne inutile. D'autres systèmes de programmation, comme l'Ada, sont
beaucoup plus tatillons et vous signaleront automatiquement ce genre de
chose, parce qu'elles pourraient être la conséquence d'une erreur de
votre part.
Vous voilà capables, en principe, de faire abattre du travail à
l'ordinateur. Tout au moins de lui faire répéter un grand nombre de
fois les mêmes calculs. Ils vous faut à présent apprendre à utiliser
les "conditions".
Vous vous souvenez de (+ 2 2), qui donne comme résultat 4 ?
L'opérateur "+" prend deux nombres et donne comme résultat un nombre...
Hé bien il existe d'autres sortes d'opérateurs. Par exemple
>
< >= <= = /=
L'opérateur ">", ainsi que ses amis, peuvent prendre
deux nombres mais ils donnent comme résultat tout à fait autre chose.
Ce résultat est "T" ou "NIL" :
- T vient de l'anglais
"true" et veut dire "vrai".
- NIL vient du latin
"nihil" et veut dire en quelque sorte "rien du
tout". Ici, cela veut dire "faux".
Comme ceci :
(
< 2 67 )
Donne comme résultat T, parce
qu'il est vrai que 2 < 67.
Tandis que :
(
< 4 4 )
Donne comme résultat NIL parce
qu'il est faux que 4 < 4.
Inutile de vous expliquer plus avant. Voici une table qui résume la
fonction de ces opérateurs :
>
|
est plus grand que ?
|
<
|
est plus petit que ?
|
>=
|
est plus grand que ou égal à ?
|
<=
|
est plus petit que ou égal à ?
|
=
|
est égal à ?
|
/=
|
n'est pas égal à ?
|
Vous me direz qu'il aurait été plus simple, pour dire "vrai" et "faux",
de par exemple utiliser "True" et "False" comme en Ada... Je suis
absolument de votre avis, d'autant plus que le choix de NIL pour
signifier le faux pose parfois des problèmes techniques. Le choix de T
pour signifier le vrai, vous empêche d'utiliser T comme nom de mémoire.
Ce n'est pas très malin... mais bon... bienvenue en Common LISP.
Vous voila capables de faire comparer au LISP deux nombres. Vous pouvez
par exemple taper ceci :
(
<= rayon 5 )
Cela produira T si le contenu
de la mémoire "rayon" est plus petit ou
égal à 5, sinon cela produira NIL.
Ceci donne exactement le même résultat, bien entendu :
(
>= 5
rayon )
Ne vous gênez pas :
(
= a b )
Produira NIL si le contenu des
deux mémoires a et b est différent. Et T
si les contenus sont identiques.
Vous pouvez comparer plus de deux nombres à la fois :
(=
5 5 5 5 5)
(< 3 6 9 13 45)
Un détail important : vous pouvez stocker T et NIL dans des mémoires.
Exemples :
(setf
ok nil)
(setf reussi ok)
(setf arret t)
(setf a 107)
(setf depassement (> a 100))
ok
reussi
arret
depassement
"ok" contiendra NIL, "reussi"
contiendra NIL, "arret"
contiendra T et "depassement"
contiendra T puisque a>100.
Le progn suivant produit comme résultat NIL, parce que sa dernière ligne
produit NIL :
(progn
(setf a 25)
(setf b (/ a 0.99))
(> a b) )
Le problème, à présent, est de faire en sorte que cela entraîne des
conséquences. Tenez, imaginons par exemple que nous calculons la somme
des surfaces de 22 disques mais... la surface du disque de rayon 17
doit être comptée deux fois.
Pour cela, utilisons if. Comme
ceci :
(setf
surface 0)
(dotimes
(n 22)
(setf r (+ n 1))
(setf s
(surface-du-disque-de-rayon r))
(if ( = r
17 )
(setf
surface (+ surface (* 2 s)))
(setf surface
(+ surface s
)) )
)
surface
Si r=17 alors la valeur de 2xs est
ajoutée à la mémoire "surface". Sinon,
la simple valeur de s est
ajoutée à la mémoire "surface".
Vous allez me faire deux remarques :
- Pour comprendre ce programme, il était tout à fait inutile de
savoir que "vrai" se dit T en
Common Lisp et que "faux" se dit NIL.
Exact. Mais vous êtes ici pour apprendre comment fonctionnent les
entrailles des ordinateurs. Et croyez-moi, avec NIL vous allez voir du
pays. Tout informaticien lisant ceci et se disant que vous le lisez
vous aussi, éclate d'un hurlement de rire qui vous glacerait le sang si
vous pouviez l'entendre.
- C'est bien joli de pouvoir traiter r=17 comme un cas particulier
mais que faisons-nous s'il y a deux cas particuliers, par exemple 17 et
19 ? Réponse ci-dessous.
Voici quelques nouveaux opérateurs : and or
xor not
not est la négation :
(not
t) donne NIL
(not nil)
donne T
Exemple :
(not
(< 2 5) )
Donne NIL, parce que (< 2
5) donne T.
(not
(> 2 5) )
Donne T, parce qu'il est faux
que 2 > 5.
(not
reussi )
Donne T si "reussi" contient NIL et NIL si "reussi" contient T.
and veut dire "et". Par exemple
:
(and
(< 2 5) (< 2 7) (= 9 9) (/= 2 3) (>= 4
4) )
Donne T parce que les cinq
conditions donnent T. Si une
seule ou
plusieurs des cinq conditions avait donné NIL, le résultat du and
aurait été NIL.
Par contre pour or, qui veut
dire "ou", il suffit qu'une seule des
conditions soit T pour
que le résultat soit T :
(or
(< 2 0) (> 4 1) (< 7 2) (< 6 3) (=
56 3) )
Le (> 4 1) donne T, donc le
résultat de l'ensemble est T.
Si toutes
les comparaisons avaient donné NIL,
alors le résultat aurait
été NIL.
Notez qu'en lieu et place des comparaisons, vous pouvez directement
mettre des T et des NIL, si cela vous chante :
(and
(< 2 5) (< 2 7) (= 9 9) T
(>= 4
4) )
(or NIL T
(< 2 7) (< 6 3) NIL )
Ou des mémoires qui contiennent T
ou NIL :
(setf a (/= 4
5))
(setf b nil)
(setf c t)
(and
(< 2 5) (< 2 7) (= 9 9) a
(>= 4
4) )
(or b c
(< 2 7) (< 6 3) nil )
Pour l'instant cela vous semble inutile mais je vous ferais remarquer
que placer des (< 2 5) et des (>= 4 4) est tout aussi inutile.
Attendez de devoir gérer des programmes, vous serez bien contents de
pouvoir placer des T et des NIL quand c'est nécessaire.
Voici comment effectuer le traitement particulier si le rayon vaut 17 ou 19 :
(setf
surface 0)
(dotimes
(n 22)
(setf r (+ n 1))
(setf s
(surface-du-disque-de-rayon r))
(if (or (= r
17) (= r 19) )
(setf surface (+ surface (*
2 s) ))
(setf surface (+ surface s
)) ) )
surface
Voici un exemple pour illustrer pourquoi il est parfois "inutile en
théorie" mais "utile en pratique" de mettre des T et des NIL dans un
programme. Supposons que tout d'un coup, par simple curiosité, vous
vous demandez quel serait le résultat de la somme des surfaces si tous les disques avaient
leur
surface comptée double. Il est très facile de réécrire le programme
pour qu'il fasse cela :
(setf
surface 0)
(dotimes
(n 22)
(setf r (+ n 1))
(setf s
(surface-du-disque-de-rayon r))
(setf surface (+ surface (*
2 s) )) )
surface
Oui mais, cette version-ci a demandé beaucoup moins de travail de
transformation :
(setf
surface 0)
(dotimes
(n 22)
(setf r (+ n 1))
(setf s
(surface-du-disque-de-rayon r))
(if (or
(= r
17) (= r 19) t )
(setf surface (+ surface (*
2 s) ))
(setf surface (+ surface s
)) ) )
surface
J'ai juste ajouté un T au bon
endroit... et zou les surfaces sont
toujours comptées double. Il me suffit d'enlever ce t et tout
redevient comme avant. C'est parfois un jeu dangereux... mais c'est
bien pratique.
Supposons que quand "r" vaut 17 ou 19, il ne faut simplement pas
comptabiliser les surfaces. La solution la plus immédiate serait de
multiplier la surface par 0 au lieu de par 2... Mais vous pouvez aussi
simplement mettre une commande vide. Comme ceci :
(setf
surface 0)
(dotimes
(n 22)
(setf r (+ n 1))
(setf s
(surface-du-disque-de-rayon r))
(if (or
(= r
17) (= r 19) )
()
(setf surface (+ surface s
)) ) )
surface
Une liste vide "()" ou NIL,
c'est la même chose :
(setf
surface 0)
(dotimes
(n 22)
(setf r (+ n 1))
(setf s
(surface-du-disque-de-rayon r))
(if (or
(= r
17) (= r 19) )
nil
(setf surface (+ surface s
)) ) )
surface
Vous pensez que je suis sympa de vous signaler que () et NIL sont la
même chose mais que cela ne vous sert à rien... Attendez plus tard...
Bien entendu, la deuxième commande, elle aussi, peut être remplacée par
() si cela vous est utile. Dans ce cas on ne comptabilise que les
surfaces de rayons 17 et 19 et en les comptant double :
(setf
surface 0)
(dotimes
(n 22)
(setf r (+ n 1))
(setf s
(surface-du-disque-de-rayon r))
(if (or
(= r
17) (= r 19) )
(setf surface (+ surface (*
2 s) ))
()
) )
surface
Plus simplement, vous pouvez ne pas mettre de deuxième commande du tout
:
(setf
surface 0)
(dotimes
(n 22)
(setf r (+ n 1))
(setf s
(surface-du-disque-de-rayon r))
(if (or
(= r
17) (= r 19) )
(setf surface (+ surface (*
2 s) )) ) )
surface
Supposons que vous vous rendez soudain compte du fait qu'il fallait
faire le contraire : il faut comptabiliser toutes les surfaces en
double sauf pour des rayons
de 17 et 19... Bien entendu, inverser les deux lignes est très facile.
Mais voici une autre possibilité :
(setf
surface 0)
(dotimes
(n 22)
(setf r (+ n 1))
(setf s
(surface-du-disque-de-rayon r))
(if (not (or
(= r
17) (= r 19)) )
(setf surface (+ surface (*
2 s) ))
(setf surface (+ surface s
)) ) )
surface
Ayez le réflexe LISP ; il y a deux fois (setf surface (+ surface... et c'est
déjà une fois de trop. Voici une solution :
(setf
surface 0)
(dotimes
(n 22)
(setf r (+ n 1))
(setf s
(surface-du-disque-de-rayon r))
(setf
surface (+ surface
(if (or
(= r
17) (= r 19))
(*
2 s)
s ) ) )
surface
Et puis de toute facon, (setf surface
(+ surface... peut être remplacé par (incf surface... :
(setf
surface 0)
(dotimes
(n 22)
(setf r (+ n 1))
(setf s
(surface-du-disque-de-rayon r))
(incf
surface
(if (or
(= r
17) (= r 19))
(*
2 s)
s ) ) )
surface
C'est assez court pour être placé sur une seule ligne :
(setf
surface 0)
(dotimes
(n 22)
(setf r (+ n 1))
(setf s
(surface-du-disque-de-rayon r))
(incf
surface (if (or
(= r
17) (= r 19)) (*
2 s) s ) ) )
surface
Un problème technique : let, progn, dotimes... permettent de faire
répéter un certain nombre de fois une commande ou une suite de
commandes... Dans le cas d'un if,
vous pouvez faire exécuter une
commande ou non... mais comment faire exécuter une suite de commandes ? Cela
semble
impossible à priori. Voici la solution :
(setf
surface 0)
(dotimes
(n 22)
(setf r (+ n 1))
(setf s
(surface-du-disque-de-rayon r))
(if (or
(= r
17) (= r 19))
(progn (setf x (*
2 s))
(setf x (- x 1))
(setf x (* x 2))
(setf surface (+ surface x)) )
(progn (setf x (* 3 s))
(setf x (- x 10))
(setf x (/ x 2))
(setf surface (- surface x)) ) ) )
surface
Vous ne donnez toujours que deux choses à manger au if mais ce sont
chacune une liste de commandes... Bien entendu vous pouvez utiliser une
liste de commande pour l'un et juste une simple commande pour l'autre...
Tout d'un coup vous devenez tout anxieux : si progn a été utilisé à l'intérieur du
if, alors il n'est plus
disponible pour encapsuler tout le programme ?! Mais si :
(progn
(setf
surface 0)
(dotimes
(n 22)
(setf r (+ n
1))
(setf s
(surface-du-disque-de-rayon r))
(if (or
(= r
17) (= r 19))
(progn (setf x (*
2 s))
(setf x (- x 1))
(setf x (* x 2))
(setf surface (+ surface x)) )
(progn (setf x (*
3 s))
(setf x (- x 10))
(setf x (/ x 2))
(setf surface (- surface x)) ) ) )
surface )
Quoiqu'un let serait plus sûr :
(let
(r s x)
(setf
surface 0)
(dotimes
(n 22)
(setf r (+ n
1))
(setf s
(surface-du-disque-de-rayon r))
(if (or
(= r
17) (= r 19))
(progn (setf x (*
2 s))
(setf x (- x 1))
(setf x (* x 2))
(setf surface (+ surface x)) )
(progn (setf x (*
3 s))
(setf x (- x 10))
(setf x (/ x 2))
(setf surface (- surface x)) ) ) )
surface )
Et si vous voulez, vous pouvez remplacer les deux progn par des let. Et même en profiter
sournoisement pour bien spécifier que l'usage de la mémoire "x" est
local aux if. C'est
relativement inutile mais pourquoi pas :
(let
(r s)
(setf
surface 0)
(dotimes
(n 22)
(setf r (+ n
1))
(setf s
(surface-du-disque-de-rayon r))
(if (or
(= r
17) (= r 19))
(let (x) (setf x (*
2 s))
(setf x (- x 1))
(setf x (* x 2))
(setf surface (+ surface x)) )
(let (x) (setf
x (*
3 s))
(setf x (- x 10))
(setf x (/ x 2))
(setf surface (- surface x)) ) ) )
surface )
Dans l'exemple ci-dessus, un if
est imbriqué dans un dotimes.
Vous
pouvez bien entendu faire le contraire : imbriquer un dotimes dans un if. Vous pouvez mettre plusieurs if à la suite l'un de l'autre...
Vous pouvez imbriquer des if
dans des if... Un if peut contenir
un dotimes qui contient trois if dont l'un contient un dotimes
qui contient deux if... Vous
faites ce que vous voulez. Ce ne sont
que des leviers et des engrenages, que vous êtes libres d'assembler,
côte à côte ou comme des poupées russes, pour former un mécanisme
d'horlogerie qui remplit un travail utile
quelconque. C'est un métier qui s'apprend. Sachez juste qu'il est
absolument normal qu'au début vous passiez des heures pour assembler
des programmes pourtant très simples, avec plein de problèmes qui vous
semblent abscons et insupportables. Si vous arrivez à la certitude que
tout cela n'est qu'une vaste escroquerie et que vous feriez mieux de
laisser tomber mais que quelque jours plus tard vous vous dites soudain
"j'aurais peut-être dû placer la parenthèse plus haut, essayons pour
voir..." alors vous êtes un informaticien. N'hésitez pas non plus à
consulter ceux qui savent. Mais prenez garde : les escrocs abondent.
Les pires sont les escrocs qui s'ignorent. Certaines personnes sont
capables de programmer très correctement mais ne trouvent pas comment
transmettre leurs bons réflexes. Trouvez quelqu'un qui comprend la
situation et qui démystifie calmement les choses.
Vous pouvez définir une fonction à l'intérieur d'une fonction. Dans ce
court exemple, la fonction Johan crée la fonction Pirlouit. Il faut
donc invoquer Johan pour que Pirlouit soit créé et puisse être invoquée
:
(defun
johan ()
(defun
pirlouit (p) (+ p 1) ) )
(pirlouit
2)
; ne fonctionne pas
(johan)
; crée Pirlouit
(pirlouit
2)
; maintenant cela fonctionne
Par contre si vous désirez que Pirlouit soit invisible au monde global
; que seul Johan puisse l'utiliser :
(defun
johan ()
(let
(pirlouit)
(defun
pirlouit (p) (+ p 1) )
(pirlouit 2) ) )
(pirlouit
2)
; ne fonctionne pas
(johan)
; affiche 3
(pirlouit
2)
; ne fonctionne toujours pas
Vous pouvez vraiment imbriquer n'importe quoi n'importe comment :
(let
(a b c)
(setf a 45)
let (a b)
(setf a 643)
(setf b (+ a
545))
(setf c (/ b
2)) )
(setf b (+ a c))
b )
En général, un let encapsule
l'entièreté d'un defun. Ce
n'est en aucune façon obligatoire. Dans cet exemple, la fonction
utilise une mémoire "resultat" de façon locale mais à la fin elle
modifie la mémoire "resultat" globale :
(defun
test (n)
(let (b)
(let (resultat a)
(setf a (* n 2))
(setf
resultat (+ a n))
(setf b (* n resultat a)) )
(setf resultat
b) ) )
L'exemple suivant est plus redoutable. Le résultat du let est sa dernière commande, soit
le contenu de "b". C'est cela qui sera stocké dans la mémoire
"resultat" globale :
(defun
test (n)
(setf resultat
(let (resultat a
b)
(setf a (* n
2))
(setf resultat (+ a n))
(setf b
(* n resultat a))
b ) ) )
Autre exemple : un progn
pouvant répondre T ou NIL, il peut donc être la condition
d'un if :
(setf
a 535)
(if (progn
(setf b 463)
(a < b) )
(setf resultat 4)
(setf resultat -4) )
Répétons certaines choses :
Quand vous donnez quelque chose à manger au
LISP, il répond toujours
quelque chose. Parfois, ce qu'il a répondu vous a semblé aberrant ou
inutile. Quand vous lui donnez à manger le nom d'une mémoire, il vous
répond par le contenu de cette mémoire. Quand vous lui donnez à manger
un calcul, il vous donne le résultat du calcul. Ça, c'est pratique...
Avec le
temps, vous apprendrez à comprendre chaque type de réponse...
Développons un peu comment LISP procède pour vous répondre
quelque chose. Si vous lui donnez simplement un calcul à effectuer ou
si vous tapez le nom d'une mémoire, le type de réponse du LISP est
évident : c'est le résultat du calcul ou le contenu de la mémoire...
Mais qu'en est-il si on lui donne une liste de commandes ? Commençons
par quelques commandes séparées, qui donnent chacune un résultat :
(setf
a 47)
(setf b (+ a 9))
(setf c (+ a b))
b
c
Elles répondent l'une après l'autre 47,
56, 103, 56 et 103. Les setf donnent comme
réponse ce qu'ils ont stocké dans la mémoire... Pourquoi pas, c'est un
comportement rationnel comme un autre.
À présent groupons ces commandes grâce à la commande list, afin qu'elles soient
évaluées l'une après l'autre et que leurs résultats soient alignés dans
une liste :
(list (setf a
47)
(setf b (+ a 9))
(setf c (+ a b))
b
c )
Cela fournit une seule réponse : la liste (47 56 103 56 103)
Les réponses des commandes individuelles ont donc été groupées dans une
liste.
Dans le cas d'une fonction, le comportement est différent : tout comme progn, la fonction
répond par la réponse de la dernière commande qu'elle contient. Essayez
par exemple cette fonction-ci :
(defun
test () (setf a 47)
(setf b (+ a 9))
(setf c (+ a b))
b
c )
(test)
La réponse à l'exécution de la fonction par la commande (test) est
simplement le nombre 103. Parce que 103 est le résultat de la dernière
commande, à savoir juste "c". La mémoire "c" contient 103...
Si vous voulez que la fonction fournisse le même résultat que plus
haut, à savoir la liste (47 56 103 56 103) alors faites par exemple en
sorte que
la fonction ne contienne qu'une seule commande, comme ceci :
(defun
test () (list (setf
a 47)
(setf b (+ a 9))
(setf c (+ a b))
b
c ) )
(test)
L'idéal, est bien entendu de placer une dernière commande bien ciblée
au bas de la fonction, qui fait ce qu'il est le plus utile de faire :
(defun
test () (setf a 47)
(setf b (+ a 9))
(setf c (+ a b))
(list b
c ) )
(test)
L'évaluation de (test) répond (56 103),
soit le contenu des mémoires "b" et "c"...
Voici une fonction "caracteristiques-du-cylindre". Elle prend comme
paramètres le diametre et la hauteur d'un cylindre, elle donne comme
résultat une liste contenant la surface de la base, la surface du tube
et le volume du
cylindre :
(defun
caracteristiques-du-cylindre (diametre hauteur)
(let (rayon
surface-disque surface-tube volume)
(setf rayon (/
diametre 2))
(setf
surface-disque (* 2 pi rayon rayon))
(setf
surface-tube (* diametre pi))
(setf volume
(* surface-disque hauteur))
(list
surface-disque surface-tube volume) ) )
(caracteristiques-du-cylindre 2.5
10)
Vous auriez parfaitement pu la définir ainsi :
(defun
caracteristiques-du-cylindre (diametre hauteur)
(let (
(rayon
(/ diametre 2))
(surface-disque (* 2 pi rayon rayon))
(surface-tube (* diametre pi))
(volume (* surface-disque hauteur)) )
(list
surface-disque surface-tube volume) ) )
(caracteristiques-du-cylindre 2.5
10)
Ou ainsi :
(defun
caracteristiques-du-cylindre (diametre hauteur)
(list (/
(* pi diametre diametre) 2) (* diametre pi)
(/
(* pi diametre diametre hauteur) 2) ) )
Voici un inquiétant hybride :
(defun
caracteristiques-du-cylindre (diametre hauteur)
(let ( (rayon
(/ diametre 2)) surface-disque surface-tube )
(setf
surface-disque (* 2 pi rayon rayon))
(setf
surface-tube (* diametre pi))
(list
surface-disque surface-tube (*
surface-disque hauteur) ) ) )
Toutes ces définitions de la fonction caracteristiques-du-cylindre sont
correctes. La plus lisible est la première. Mais quelle est la plus
performante ; la plus rapide lors des calculs ? Est-ce que c'est la
première, qui fait le minimum de calculs ? Est-ce que c'est la
deuxième, qui ne nécessite pas l'usage de mémoires ? La réponse dépend
des circonstances... en principe, avec les systèmes modernes, il ne
doit y avoir que peu ou pas du tout de différences entre ces versions,
parce que les calculs sont optimisés. Quelle que soit la façon dont
vous les présentez au LISP, il est sensé les ramener à un minimum
d'opérations. Donc... privilégiez la lisibilité...
Dans tous les exemples ci-dessus, la réponse de la fonction est le
résultat de sa dernière commande. Cela n'est pas obligatoire. return-from vous permet
d'interrompre le cours d'une fonction à tout
moment et de donner un résultat. La fonction suivante donne des valeurs
nulles (0 0 0) si un des paramètres du cylindre est négatif :
(defun
caracteristiques-du-cylindre (diametre hauteur)
(let (rayon surface-disque
surface-tube volume)
(if (or (< diametre 0) (<
hauteur
0))
(return-from
caracteristiques-du-cylindre (list 0 0 0)
) )
(setf rayon (/ diametre 2))
(setf surface-disque (* 2 pi rayon rayon))
(setf surface-tube (* diametre pi))
(setf volume (* surface-disque hauteur))
(list surface-disque surface-tube volume) ) )
(caracteristiques-du-cylindre 2.5
10)
(caracteristiques-du-cylindre
2.5
-32)
À l'usage, vous pourriez constater un désagrément : vous ne vous
souvenez pas toujours s'il faut donner le diamètre et puis la hauteur
ou la hauteur et puis le diamètre. Les auteurs du LISP proposent une
solution à ce problème :
(defun
caracteristiques-du-cylindre ( &key
diametre hauteur)
(let (rayon surface-disque surface-tube volume)
(setf rayon (/ diametre 2))
(setf surface-disque (* 2 pi rayon rayon))
(setf surface-tube (* diametre pi))
(setf volume (* surface-disque hauteur))
(list surface-disque surface-tube volume) ) )
(caracteristiques-du-cylindre :diametre 2.5
:hauteur 10)
(caracteristiques-du-cylindre :hauteur 10 :diametre 2.5)
Par la grâce du &key, vous
pouvez à présent donner les paramètres dans n'importe quel ordre. Mais
vous devez spécifier le nom de chaque paramètre...
Vous pouvez définir un paramètre comme facultatif. Supposons que dans
l'usine où vous travaillez, presque tous les
cylindres ont une longueur de 3 mètres. Vous avez programmé cette
fonction qui calcule les surfaces et volume d'un cylindre... le
magasinier et les techniciens vous en sont reconnaissant... mais ils en
ont un peu marre de devoir chaque fois spécifier que le tube a une
longueur de 3 mètres. Il a presque toujours une longueur de 3 mètres...
Une solution serait de définir une fonction qui calcule les
caractéristiques d'un tube de 3 mètres. On ne lui donne que le
diamètre. Comme ceci :
(defun
caracteristiques-du-cylindre-de-3-metres-de-long
(diametre)
(caracteristiques-du-cylindre diametre 3) )
(caracteristiques-du-cylindre-de-3-metres-de-long 2.5)
Une autre solution consiste à définir le paramètre de longueur comme
étant optionnel et lui donner une valeur par défaut. S'il n'est pas
donné lors d'un usage de la fonction, la valeur par défaut est prise :
(defun
caracteristiques-du-cylindre (diametre &optional
(hauteur 3))
(let (rayon surface-disque
surface-tube volume)
(setf rayon (/ diametre 2))
(setf surface-disque (* 2 pi rayon rayon))
(setf surface-tube (* diametre pi))
(setf volume (* surface-disque hauteur))
(list surface-disque surface-tube volume) ) )
(caracteristiques-du-cylindre 2.5)
(caracteristiques-du-cylindre
4)
(caracteristiques-du-cylindre 1.5
2)
(caracteristiques-du-cylindre 2.5
2)
Il y a encore d'autres telles possibilités pour la définition des
paramètres de fonctions. Vous pouvez ne pas définir la valeur par
défaut d'un paramètre optionnel... vous pouvez mélanger ces
possibilités dans la définition d'une même fonction... Rien de bien
compliqué mais cela n'est pas nécessaire pour l'instant.
Voici six grands amis des informaticiens : floor
ceiling
truncate round abs mod rem
random
truncate donne la partie
entière
d'un nombre. Par exemple (truncate
1.3)
donne 1. (truncate 1.999)
donne
toujours 1. Et (truncate 1)
donne
1. Il y a un piège, avec les nombres
négatifs : (truncate -3.6)
donne
-3. Cela peut vous sembler naturel mais
lorsque vous écrirez des programmes, vous vous attendrez parfois, sans
vous en rendre compte, à ce que le résultat soit plutôt -4.
floor fait ce que l'on voudrait
parfois que truncate fasse : (truncate -3.6) donne -4. Pour les
nombres positifs, le résultat est le même que truncate.
ceiling est le symétrique de floor. (ceiling 5) donne 5
mais (ceiling 5.0001) donne 6
et (ceiling
5.9999) donne bien sûr 6 aussi. Et... (ceiling -7.4) donne -7.
round est la valeur arrondie ;
l'entier le plus proche. (round
5.4999) donne 5 et (round 5.5)
donne 6.
Bien entendu, (round -9.49)
donne -9 et (round -9.5) donne
-10.
abs donne la valeur absolue.
(abs -4.234) donne 4,234 et (abs 634.54) donne 634,54.
mod donne le reste de la
division entière. Si vous divisez 20 par 3, le résultat entier est 6 et
le reste est 2, parce que 3 x
6 + 2 = 20. Donc, (mod 20 3)
donne 2. Vous verrez à quel point c'est
pratique, dans toutes sortes de situations.
rem fonctionne de la même façon
que mod avec les nombres
positifs mais pas pour les nombres négatifs. Comme je n'ai jamais eu
l'utilité de cela je n'ai pas réussi à m'intéresser à la question. Je
ne peux donc pas vous expliquer la différence entre mod et rem.
random produit un nombre au
hasard. Vous avez le choix : vous pouvez demander un nombre entier ou
un nombre décimal. (random 6)
vous donnera un entier au hasard
parmi 0, 1, 2, 3, 4
et 5 tandis que (random 2.0)
vous donnera un nombre décimal au hasard
entre 0,00000...
et 1,99999... Par exemple 0,4533553 ou 1,8443564
Certaines fonctions modifient la valeur de la mémoire qu'on leur
confie. Un excellent moyen pour comprendre ce distingo sont les
fonctions incf
decf 1+ 1-
1+ produit comme résultat le
nombre qu'on lui
confie + 1. Par exemple (1+ 6)
donne comme résultat 7. Si la mémoire "a" contient 23, alors (1+ a) donne comme résultat 24.
Mais... le contenu de la
mémoire "a" n'est pas modifié. Je suppose qu'il est inutile
d'expliquer ce que fait 1-
incf, par contre, modifie le contenu de la mémoire
qu'on lui confie. Si la mémoire "a" contient 23, alors après (incf a) la mémoire "a" contient le
nombre 24. "a" a été modifiée.
Le résultat rendu par ce (incf a)
est bien sûr 24.
Cela implique qu'il n'a pas de sens d'écrire (incf 3) puisque 3 n'est pas le nom
d'une mémoire qui pourrait être modifiée. Le LISP calera si vous tentez
de faire une telle chose.
decf fait bien sûr le contraire
: soustraire 1 au contenu de la mémoire.
Notez que incf et setf peuvent prendre un deuxième
paramètre, si vous voulez ajouter ou soustraire autre chose que 1 à une
mémoire. Par exemple pour ajouter 822 à la mémoire "a" : (incf a 822)
Pour la suite de l'exposé, il nous faut une fonction qui répond si un
nombre est divisible par une autre. Par exemple 6 est divisible par 3
et par 2 mais 9 n'est pas divisible par 5. Voici comment beaucoup
d'informaticiens ont écrit une telle fonction. Elle répond T si "n" est
divisible par "d" et sinon elle répond NIL
:
(defun
divisible (n d)
(let (a b c
resultat)
(setf
a (/ n d))
(setf b (floor a))
(setf c (* a d))
(if (= n c)
(setf resultat t)
(setf resultat nil) )
resultat ) )
En voici une version plus courte, le if
était vraiment lourd et
inutile
:
(defun
divisible (n d)
(let (a b c)
(setf a (/ n d))
(setf b (floor a))
(setf c (* a d))
(=
n c) ) )
Il n'était pas nécessaire d'utiliser trois mémoires différentes a, b et
c. La mémoire "a" suffit largement pour contenir les étapes
intermédiaires du calcul :
(defun
divisible (n d)
(let (a)
(setf
a (/ n d))
(setf a (floor a))
(setf a (* a
d))
(=
n a) ) )
Et puis de toute façon, ventiler tout cela sur quatre lignes n'est pas
nécessaire, cette déclaration-ci de la fonction est bien plus courte...
mais peut-être moins lisible :
(defun
divisible (n d)
(= n (* (floor (/ n d)) d)) )
Tout ce qui précède est bon pour le panier. Un véritable informaticien
utilise mod. Si "x" est
divisible par "y", alors (mod x y)
donne 0 :
(defun
divisible (n d)
(= 0 (mod n d)) )
Pourquoi avons-nous besoin de cela ? Pardi, pour écrire une fonction
qui répond si un nombre entier est premier (divisible uniquement par 1
et par lui-même). Voici un exemple, pas du tout optimisé :
(defun
premier (x)
(let (il-est-premier)
(setf
il-est-premier
t)
(dotimes (i (- x 3))
(if (divisible x (+ i 2))
(setf il-est-premier nil)))
il-est-premier ) )
(premier 4)
(premier 7)
Voici un exemple optimisé. Cette fonction est considérablement plus
rapide à l'usage :
(defun
premier (x)
(dotimes (i
(- (floor (sqrt x)) 1))
(if (divisible x (+ i 2))
(return-from premier nil) ) )
t )
Voici la même fonction mais qui ne fait pas appel à la fonction
"divisible" (aucun intérêt mais c'est pour vous montrer) :
(defun
premier (x)
(dotimes (i
(- (floor (sqrt x)) 1))
(if (= 0 (mod x (+ i 2)))
(return-from premier nil) ) )
t )
Toujours juste pour vous montrer, ci-dessous se trouve comment j'avais
commencé par
écrire cette fonction optimisée. J'avais placé en tête un if pour
traiter les cas particuliers où x vaut 1, 2 ou 3. Pourquoi ? Parce que
je n'avais pas confiance dans la suite de la fonction pour traiter ces
cas particuliers, trop petits pour avoir une racine carrée
significative. Je n'ai même pas essayé de réfléchir pour savoir si cela
était pertinent ou non. Ensuite j'ai fait un essai en enlevant le if... cela a donné le bon résultat
pour (premier 1), (premier 2) et
(premier 3)... alors j'ai
laissé comme ça. Ce n'est pas tout à fait
sérieux comme façon de travailler mais soyez sûr, hélas, que la
majorité des systèmes et des logiciels actuels sont conçus de façons
bien pires. Notez également que le fait d'avoir enlevé le if a rendu
la fonction plus "intelligente". En effet, avec le if, la fonction
répondait que tout nombre négatif est premier. C'est ridicule. Sans le if, la fonction plante. Elle donne
un message d'erreur si on lui
demande si un nombre négatif est premier. C'est mieux. Peut-être
trouvez-vous que l'idéal serait que la fonction accepte les nombres
négatifs et donne une réponse sensée. Après tout, -8 n'est pas premier
et -7 est premier... N'hésitez pas à faire vous-mêmes cette
amélioration ! Mais il y a ici tout un débat à faire. Certains diront
que si les nombres négatifs et le zéro sont prévus, alors la fonction
ne plante jamais, donc elle n'est jamais la cause d'un arrêt du
programme et c'est tant mieux. D'autres diront que, au contraire,
il est utile que la fonction plante et bloque l'exécution du programme
si elle reçoit un nombre négatif, parce que si elle reçoit un nombre
négatif, cela est anormal et il faut interrompre le programme et le
vérifier. Ils vous feront d'ailleurs remarquer ceci : si la fonction
accepte n'importe quoi sans broncher, elle accepte donc aussi des
nombres très grands, du genre qui requièrent un an de calcul. La
fonction peut donc bloquer le cours du programme si elle reçoit par
erreur un nombre astronomiquement grand à évaluer.. En réalité, tout
dépend de l'usage final du programme. Si par exemple c'est le programme
du microcontrôleur de bord de votre avion téléguidé, il vaut mieux
que la fonction ne plante jamais. Supposons qu'elle reçoit en entrée la
vitesse de l'avion. Toutes les quelques minutes, le capteur de vitesse
a un bogue et il donne une vitesse négative. Si la fonction plante
là-dessus et plante tout le microcontrôleur, votre avion va se crasher.
Par contre si la fonction essaye de faire son travail et donne un
résultat même aberrant, l'avion va peut-être faire n'importe quoi
pendant un dixième de seconde, on aura l'impression qu'il a un hoquet,
mais il continuera à voler normalement le reste du temps. C'est
préférable. Situation complètement différente : la
fonction fait partie d'un programme d'expérimentations mathématiques.
Il vaut alors mieux que tout s'arrête en cas d'anomalie et que vous
soyez contraints de relire le programme ou de faire des tests pour
comprendre le problème. Il ne faut pas laisser passer quelque chose qui
pourrait être le signe d'une erreur qui met en doute la fiabilité de
tout votre travail. Dans la réalité, lorsqu'on programme par exemple
les ordinateurs d'un avion, on essaye de ne rien laisser au hasard.
Toutes les fonctions de tous les programmes seront sensées être prévues
pour fonctionner de la meilleure façon possible dans tous les cas de
figure. On utilise des outils comme le "traitement d'exceptions" et on
prouve de façon mathématique que les fonctions ne peuvent pas planter.
Malgré cela, il y a des accidents. Un Boeing 757 a un jour eu le hoquet
en plein vol, à haute altitude. Il s'est mis à monter en flèche et à
redescendre, comme un wagonnet de montagnes russes. Les passagers ont
eu la peur de leur vie. Les pilotes ont immédiatement réagi. Ils ont
débranché le pilote automatique et sont passés en commande manuelle. Le
problème venait d'un capteur de vitesse tombé en panne... Le logiciel
n'avait pas été assez blindé contre cette possibilité. Cela a bien
entendu été aussitôt revu et corrigé sur tous les 757 en circulation.
Un autre 757, a soudain eu les deux moteurs qui s'éteignent, en phase
d'atterrissage. Les pilotes n'ont pas pu les relancer. Ils ont dû faire
un atterrissage en catastrophe sur la pelouse qui précède la piste. Le
train d'atterrissage de l'avion a monstrueusement charrué la terre...
heureusement pas de blessés graves. Les ordinateurs s'étaient emmêlés
les pinceaux... Les sondes spatiales, quant à elles, recontrent un
problème tout à fait flippant : les rayons cosmiques traversent la
sonde de part en part et changent des bits ou hasard dans les
mémoires... permuttent l'état des transistors ou les bloquent en
position... Les concepteurs de la sonde Pioneer 11, la première à
passer près de la planète Jupiter, n'ont simplement pas osé la doter
d'un ordinateur de bord. Ils ont préféré la piloter entièrement depuis
la Terre ! Quand les sondes spatiales Voyager sont passées près de
Jupiter, leurs ordinateurs de bord se sont ainsi plantés plusieurs
fois. C'était prévu par les concepteurs... un système rudimentaire et
très sûr veillait à redémarrer les ordinateurs dès qu'ils
cessaient de se comporter normalement. La sonde Cassini, par contre,
n'a virtuellement pas eu de problème en passant près de Jupiter, parce
qu'elle dispose d'une électronique extrêmement résistante aux
radiations.
(defun
premier (x)
(if (<= x 3) (return-from premier t))
(dotimes (i
(- (floor (sqrt x)) 1))
(if (divisible x (+ i 2))
(return-from premier nil) ) )
t )
Pourquoi avons-nous besoin de cette fonction qui détermine si un nombre
est premier ou non ? Principalement parce que sinon il manquerait
quelque chose d'essentiel à ce traité d'informatique. Les nombres
premiers, c'est
une tradition. C'est important, la tradition. Ne manquez pas,
à ce titre, d'acheter un exemplaire du "Jargon File". La raison annexe
est qu'on se sert de cette fonction pour produire des listes de nombres
premiers. Cela amène à un problème crucial. Considérons la fonction
suivante, qui teste tous les nombres de 1 à n pour voir s'ils sont
premiers. Comment peut-elle rendre le résultat de son travail ?
(defun
liste-de-nombres-premiers (n)
(dotimes (i n)
(setf nombre-a-tester (+ i 1))
(if (premier nombre-a-tester)
( ...
nombre-a-tester est premier mais que dois-je faire avec ? ... )
) ) )
Il faut... constituer une liste avec les nombres premiers trouvés !
Stocker une liste dans une mémoire est très simple. Voici comment
stocker la liste vide dans une mémoire "joachim", de deux façon
différentes mais qui donnent exactement le même résultat :
(
setf joachim ()
)
( setf joachim nil )
Voici comment stocker une petite liste de quatre nombres dans Joachim.
Ici aussi, deux méthodes différentes, qui aboutissent au même résultat
(mais pas toujours) :
(setf
joachim (list
45 632 48 97) )
(setf joachim '(45 632 48 97)
)
Mais comment ajouter
un nombre
à la liste "joachim" ? Réponse
: en utilisant push, comme
ceci :
(push
777 joachim)
"joachim" est à présent la liste (777
45 632 48 97). Notez, c'est
très important, que le 777 a été ajouté au début de la liste.
La façon d'utiliser push dans
le programme coule de source. La
mémoire "resultat" sera au départ une liste vide. Chaque nombre premier
trouvé sera pushé dans cette
liste "résultat" :
(defun
liste-de-nombres-premiers (n)
(let (resultat)
(setf resultat () )
(dotimes (i n)
(setf nombre-a-tester (+ i 1))
(if (premier nombre-a-tester)
(push nombre-a-tester
resultat) ) )
resultat
) )
(liste-de-nombres-premiers 100)
donne comme résultat (97 89 83 79 73
71
67 61 59 53 47 43 41 37 31 29 23 19 17 13 11 7 5 3 2 1)
Vous souhaiteriez que la liste contienne les nombres premiers en ordre
croissant ? reverse répond à
votre souhait :
(defun liste-de-nombres-premiers (n)
(let (resultat)
(setf resultat () )
(dotimes (i n)
(setf
nombre-a-tester (+ i 1))
(if
(premier nombre-a-tester)
(push
nombre-a-tester resultat) ) )
(reverse
resultat)
) )
Bien sûr, on pouvait aussi balayer les nombres en ordre inverse, de "n"
à
1 :
(defun liste-de-nombres-premiers (n)
(let (resultat)
(setf resultat () )
(dotimes (i n)
(setf nombre-a-tester (- n i) )
(if (premier nombre-a-tester)
(push
nombre-a-tester resultat) ) )
resultat ) )
Et, au fait, pourquoi utiliser un setf
pour donner un contenu initial
à la mémoire "resultat" puisque le let
peut se charger de cette
besogne :
(defun liste-de-nombres-premiers (n)
(let ( (
resultat
() ) )
(dotimes (i n)
(setf nombre-a-tester (- n i)
)
(if (premier nombre-a-tester)
(push
nombre-a-tester resultat) ) )
resultat ) )
Si nous ne voulons pas simplement afficher la liste des nombres
premiers mais la stocker dans une mémoire "nombres-premiers", c'est
sans problème :
(setf
nombres-premiers (liste-de-nombres-premiers 100000))
Nous pouvons bien entendu faire afficher cette liste à tout moment :
nombres-premiers
Et nous pouvons demander sa longueur grâce à length :
(length
nombres-premiers)
Soudainement, nous nous posons la question suivante : Combien de ces
nombres premiers sont divisibles par 7 quand on leur ajoute 3 ? Il va
falloir passer tous les nombres de la liste en revue. Donc, question :
comment fait-on pour prélever un élément dans une liste ? La réponse
est pop. Commençons par créer
une mémoire "h" qui contient une liste de quelques nombres :
(setf
h (list 45 28 53 9))
Après l'exécution de ceci :
(pop
h)
La liste "h" ne contiendra plus que (28
53 9) et le nombre 45 a été affiché. Nous aurions bien sûr pu
écrire (setf a (pop h)) pour
mémoriser ce nombre 45 et faire en sorte qu'il ne soit pas "perdu"...
Ou nous aurions pu accepter de le perdre mais tout de même faire
quelque chose avec, avec une commande comme celle ci :
(setf
somme-des-nombres (+ somme-des-nombres (pop h)))
Nous pouvons donc écrire ce petit progn,
qui va "manger" la liste
"nombres-premiers" et nous donner comme résultat le nombre des éléments
qui obéissent à la condition :
(progn
(setf compte 0)
(dotimes (n (length
nombres-premiers))
(if (= 0 (mod
(+ 3 (pop nombres-premiers)) 7))
(setf compte (+ compte 1)) ) )
compte)
Cela donnera comme réponse que 1601 des nombres de la liste obéissent à
la condition.
Mais... la liste "nombres-premiers" est à présent vide. Elle a
été "mangée" par les pop
successifs. Si vous vouliez garder la liste, il fallait d'abord en
faire une copie. Ou alors il fallait déclarer une fonction, qui aurait
automatiquement fait une copie (locale) de la liste :
(defun
examen-special (les-nombres)
(let ((compte 0))
(dotimes (n (length nombres-premiers))
(if (= 0 (mod (+ 3 (pop
nombres-premiers)) 7))
(setf compte (+ compte 1)) )
)
compte ) )
(setf nombres-premiers
(liste-de-nombres-premiers 100000))
(examen-special nombres-premiers)
Mais... Pourquoi diable est-ce que j'écris un programme qui détruit la liste de
nombres, en la mangeant par des pop
?! N'est-il pas possible de laisser la liste intacte et de simplement
demander "je voudrais le n-ième nombre dans la liste". Cela est
certainement possible mais... ce serait une grosse bêtise. Tout au
moins, cela rendrait le programme beaucoup plus lent. Pourquoi ? Pour
vous le faire comprendre, il faut à présent donner une mot
d'explication sur la façon dont le LISP mémorise les listes.
Regardez ces deux commandes, qui stockent une liste de six nombres dans
une mémoire "z" et puis demandent l'affichage du contenu de "z" pour
vérification :
(setf
z '(74 25 43 51 94 28))
z
Vous avez l'impression que le
LISP "voit" la liste de 6 nombres comme un ensemble ; qu'il "voit" tous
les six nombres en même temps, comme vous-mêmes les voyez. Il n'en
rien. En réalité, le LISP ne "voit" que le premier élément de la liste,
soit le nombre 74. Il n'a pas la moindre idée des éléments suivants ni
même de combien il y en a. Il ne sait pas où ils se trouvent. Il ne
sait rien.
Vous vous demandez, dès lors, comment diable il peut malgré tout les
mémoriser et les afficher sur demande...
La réponse est relativement simple : à l'endroit où le 74 est mémorisé,
se trouve également noté l'endroit où se trouve l'élément suivant de la
liste, donc le 25. Et à cet endroit où est mémorisé le 25, se trouve
également noté l'endroit où se trouve le 43... ainsi de suite jusqu'au
dernier endroit, où est mémorisé l'élément 28, qui se caractérise par
le fait qu'il porte une marque explicite pour signifier qu'il n'a pas
de suivant.
C'est ce qu'on appelle "une liste chaînée", chaque nombre étant un
maillon de la chaîne. On tient la liste/chaîne par un maillon à une
extrémité. Si on veut attraper un des maillons suivants, il faut
remonter la chaîne maillon après maillon.
Cela a plusieurs conséquences :
- Si vous demandez la longueur d'une chaîne très longue, sera
demandera beaucoup plus de temps que pour demander la longueur d'une
chaîne courte. Parce que le LISP doit suivre tous les maillons de la
chaîne et compter combien il y en a. Plus la liste/chaîne est longue,
plus cela prend de temps...
- Si vous demandez quel est le 3ème élément d'une liste, cela prend
moins de temps que si vous demandez quel est le 4ème élément. Ne
parlons pas si vous demandez quel est le 100.000ème...
- Ajouter un élément à la fin d'un longue liste, prend
considérablement plus de temps qu'ajouter un élément au début de la
liste.
Donc, il était de loin préférable de remonter la liste/chaîne pop après pop, en jettant les maillons au fur
et à mesure. Ainsi, le maillon suivant était toujours immédiatement
sous la main.
Un philosophe vous expliquera volontiers que j'exagère un peu. Sommes
toutes, la liste chaînée est "la façon dont le LISP voit une liste dans
son ensemble". J'ai peut-être raison de dire qu'à un certain niveau le
LISP ne voit qu'un seul élément de la liste à la fois. Mais, à un
niveau plus élevé, on a le droit de dire qu'il voit la liste dans son
ensemble. Cela est parfaitement correct mais... si vous ne comprenez
pas le fonctionnement du LISP au niveau que j'ai traité ici... vous
connaissez la chanson.
Vous me direz que vous avez bien compris mais que malgré tout vous
voudriez savoir comment accéder à un élément précis d'un liste, sans la
détruire... La
réponse est nth. Voici comment
obtenir le premier et le dernier élément
de la liste "z" (qui sont les nombres 74 et 28) :
(nth
0 z)
(nth 5 z)
Réécrivons la fonction pour qu'elle utilise nth. Cela permet de vérifier
qu'elle devient ainsi considérablement plus lente :
(defun
examen-special (les-nombres)
(let ((compte 0))
(dotimes (n (length nombres-premiers))
(if (= 0 (mod (+ 3 (nth n
nombres-premiers)) 7))
(setf compte (+ compte 1)) )
)
compte ) )
Ma préférée est cette fonction, qui mange proprement la liste de
nombres
et "pond" une liste avec les nombres qui obéissent à la condition :
(defun
examen-special (les-nombres)
(let ((ont-reussi ()) un-nombre)
(dotimes (n (length nombres-premiers))
(setf un-nombre (pop
nombres-premiers))
(if (= 0 (mod (+ 3 un-nombre) 7))
(push un-nombre ont-reussi)
)
)
ont-reussi ) )
(setf nombres-premiers
(liste-de-nombres-premiers 100000))
(setf resultat (examen-special nombres-premiers))
(length resultat)
Elle peut être racourcie en utilisant dolist,
qui, au lieu de balayer de 0 à x, balaye les éléments d'une liste :
(defun
examen-special (les-nombres)
(let ((ont-reussi () ))
(dolist
(un-nombre nombres-premiers)
(if (=
0 (mod (+ 3 un-nombre) 7))
(push un-nombre ont-reussi)
)
)
ont-reussi ) )
Voici deux fonctions
célébrissimes permettant
de travailler sur des listes :
car cdr
car donne le premier élément
d'une liste. Si par exemple une mémoire "a" donne accès à la liste (3 5 6 7 6 9) alors
(car
a)
Donnera comme résultat 3. Mais attention : ceci pourrait vous donner
l'impression que car a juste
jeté un coup d'oeil dans la liste et vous répond qu'il a vu que le
premier élément de la liste est le nombre 3. C'est complètement faux.
En réalité, (car a) est le premier élément de
la liste. Donc, vous pouvez taper ceci :
(setf
(car a) 8)
Cela modifie la liste "a". Elle est maintenant (8
5 6 7 6 9). Parce que (car a)
est le premier élement
de la liste "a".
Dans le même ordre d'idées, souvenez-vous que nth donne accès à un élément
quelconque d'une liste. Par exemple (nth
3 a) donne comme résultat 7. En réalité, (nth 3 a) est le quatrième élément de
la liste et donc si vous donnez la commande suivante :
(setf
(nth 3 a) 11)
La liste "a" est à présent (8 5 6 11 6 9)
cdr donne la suite d'une liste
; sans son premier élément. Si par exemple une mémoire "a" donne accès
à la liste (8 5 6 11 6 9) alors
(cdr
a)
Donnera comme résultat (5 6 11 6 9).
Mais attention : (cdr a) n'a pas fait une copie de la
liste "a". En réalité, (cdr a)
vous donne la main sur le deuxième élément de la liste "a".
Souvenez-vous : les listes sont des chaînes de maillons. La mémoire "a"
tient en main, façon de parler, le premier élément de la liste, qui est
8. Les maillons suivants sont 5, 6, 11, 6 et 9. On peut dire que (cdr a) "vous met en main" l'élément
suivant de cette liste, donc le 5. Donc, si vous donnez la commande
suivante :
(setf
(cdr a) '(-1 -4 -7 -9 -11 99 -61))
La liste "a" devient (8 -1 -4 -7 -9 -11 99
-61)
Si vous tapez ceci :
(setf
b (cdr a))
"b" ne deviendra pas une copie de "a" sans son premier élément. Au
contraire, "b" est
"a". Les deux donnent accès à la même liste. La différence est que "a"
tient en main l'élément 8 tandis que "b" tient en main l'élément -1.
Donc, si vous modifiez le premier élément de la liste "b" :
(setf (car b) 22)
La liste "a" devient (8 22 -4 -7 -9 -11 99 -61)
Il n'y a qu'une seule liste, dont "a" et "b" tiennent des maillons
différents.
Si vous avez déjà fait un peu d'informatique, vous vous indignez
peut-être en vous disant : "mais enfin, même des langages rudimentaires
permettent de définir de grandes tables de nombres et d'accéder
instantanément à tous les éléments de la table." Rassurez-vous, le
Common Lisp est capable de faire cela aussi.
Les tables à accès direct sont une grande chose. Servez-vous en chaque
fois que nécessaire. Mais... ce n'est pas ça l'informatique. Et l'usage
maladroit de tables est une des principales sources de problèmes dans
les logiciels. Au premier abord, utiliser une table semble plus simple
et plus rationnel qu'utiliser une liste. Plus rapide aussi... Et puis
rapidement c'est l'enfer... Prenez le temps de rester du côté des
listes. Cela semble demander plus de travail, il faut d'avantage
réfléchir à ce qu'on fait... mais on va beaucoup plus loin sans heurts.
"La qualité, ça coute moins cher." Un détail : en général, les
fonctions permettant de travailler sur les listes, ne fonctionnent pas
sur les tables. Tout au moins, elles donneront des résultats qui
semblent aberrants si on ne comprend pas bien la logique interne du
LISP.
Pour définir un tableau
"y" de 6 éléments, utilisez la commande make-array. Comme ceci :
(setf
y (make-array 6))
Pour imposer un contenu initial à ce tableau, procédez comme ceci :
(setf
y (make-array 6 :initial-contents '(74 25 43 51 94 28)))
Ou, plus sobrement :
(setf
y '#(74 25 43 51 94 28))
Le # devant la liste signifie qu'elle est un tableau à accès direct...
Vous pouviez même taper ceci, qui aurait signifié explicitement, par le
A1, que le tableau est à une
dimension (en anglais : Array of 1 dimension) :
(setf
y '#A1(74 25 43 51 94 28))
Pour accéder à un élément du tableau, utilisez aref. Voici comment demander le
troisième élément du tableau "y" :
(aref
y 2)
Notez que (aref y 2) est le troisième élément du
tableau. Donc vous pouvez écrire ceci :
(setf
(aref y 2) 100)
Cela aura pour effet que "y" deviendra #(74
25 100 51 94 28)
Comment définir un tableau à deux dimensions ? Facile :
(setf
q (make-array '(2 2))
(setf q (make-array '(2 2)
:initial-contents '((953 355) (682 72))))
(setf q '#A2((953 355) (682 72)) )
Pour accéder à un élément de ce tableau à 2 dimensions :
(aref
q 0 1)
Un grand merci à Frédéric Cloth, propriétaire de 4p8
et herbergeur de ce texte (le LISP est un de ses outils professionnels).