Eric Brasseur Accueil    |    Liens    |    Contact    |   
   



ABC des ordinateurs pour informaticiens en herbe



Les octets
Langage de description
Langage de programmation







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 :


Vous auriez du voir un rond rouge sur fond noir


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 :
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 :


www.mac-net.com/host/pages/hp35/calc.html
icrank.com/data/calculator/calc_app.htm


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 :
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 :
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 :


TI 200 Voyage


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 :
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 :




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...


www.solve-et-coagula.com/As3Lisp.html


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 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 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 :
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.41421351.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   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" :
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 :
Voici quelques nouveaux opérateurs :   and   or   xor   not

not est la négation :


(not t)       donne    NIL

(not nil)     donne    


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 :
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).



Eric Brasseur  -  2 mai 2007  au  11 avril 2009
www.000webhost.com