[PDF] Apprendre à programmer avec le langage Lisp | Formation informatique

Texte intégral

(1)

Chapitre 3. Exemple 1 - Fonction générique,

abstraction et réutilisation

Cet exemple montre l'économie d'écriture de code que représente l'utilisation de fonctions génériques. Il montre aussi que

l'abstraction est une technique "naturelle" en Lisp (*). La fonction SORT-ELEMENTS-BY-INDEX trie une séquence d'éléments optiques selon leur indice de réfraction, pour une longueur d'onde particulière.

(DEFUN SORT-ELEMENTS-BY-INDEX (ELEMENTS WAVELENGTH) (SORT

ELEMENTS

#'(LAMBDA (ELEMENT-1 ELEMENT-2)

(< (ELEMENT-REFRACTIVE-INDEX ELEMENT-1 WAVELENGTH)

(ELEMENT-REFRACTIVE-INDEX ELEMENT-2 WAVELENGTH)))))

Rappel : tous les mots appartenant à la définition du langage sont en italique.

Examinons la syntaxe du Lisp; elle est simple et uniforme : toute expression correcte du langage s'écrit sous la forme d'une

parenthèse gauche, d'un opérateur (ou fonction), de 0 ou plusieurs arguments, et d'une parenthèse droite (un argument pouvant être lui-même une expression). Toute expression du langage (ou

programme) est ainsi représentée sous la forme d'une liste, la liste étant une structure de données du Lisp (il y a uniformité de la représentation des programmes et des données).

DEFUN est l'opérateur de définition d'une fonction; la fonction SORT-ELEMENTS-BY-INDEX prend 2 arguments comme l'indique la liste d'arguments formels (ELEMENTS WAVELENGTH). Après la liste d'arguments de la fonction définie par DEFUN apparaît le corps de la fonction, soit ici, la liste commençant par SORT : (SORT ... ).

SORT est une fonction de tri générique qui fait partie

du Common Lisp; elle a deux arguments, le premier étant une séquence d'objets à trier, le deuxième une fonction (prédicat) de comparaison de deux objets selon laquelle le tri devra être fait. Cet argument fonctionnel sera soit un nom de fonction, soit directement une définition de fonction, comme dans cet example.

SORT est dite fonction générique dans la mesure où elle accepte, comme argument, n'importe quel type de séquence (liste, vecteur ou chaîne de caractères). De plus, la taille de l'argument séquence

(2)

est quelconque et la séquence peut contenir des objets de n'importe quel type. Enfin, il n'y a pas de contrainte sur la fonction de

comparaison (elle doit simplement accepter deux arguments et renvoyer "vrai" ou "faux").

L'algorithme de tri utilisé par SORT dépend de l'implantation du Lisp utilisé : en général, cet algorithme est sophistiqué; il tient compte du type de séquence, de la longueur de la séquence et de la complexité de la fonction de comparaison.

Il est important de remarquer que la fonction utilisateur SORT-ELEMENTS-BY-INDEX est elle-même automatiquement générique. Le premier argument de la fonction SORT est donc la séquence d'éléments optiques, ELEMENTS.

Le deuxième argument la fonction SORT est un argument

fonctionnel: c'est une fonction de comparaison de deux éléments optiques, soit (LAMBDA ...); pour la signification de #', voir l'annexe 1. L'opérateur LAMBDA permet de définir des fonctions anonymes et peut être utilisé en place d'un argument fonctionnel, au lieu d'un nom de fonction. Contrairement à DEFUN, LAMBDA n'est donc pas suivi d'un nom de fonction : LAMBDA est directement suivi de la liste d'arguments formels, (ELEMENT-1 ELEMENT-2), et ensuite du corps de la fonction anonyme, (<

(ELEMENT-REFRACTIVE-INDEX...)...).

La fonction de comparaison appelle la fonction

ELEMENT-REFRACTIVE-INDEX qui calcule l'indice de réfraction d'un élément optique en fonction de la longueur d'onde (cette dernière fonction est définie par ailleurs). Cette dernière peut aussi être une fonction générique, valable pour différents types d'éléments optiques; elle doit renvoyer une valeur numérique acceptable pour le prédicat "plus petit", soit < (**)

Cet exemple démontre le haut pouvoir d'expression du Lisp;

l'abstraction est maximum, l'essence conceptuelle de ce programme apparaissant clairement à tout lecteur attentif.

——————————

(*) Le lecteur pourra préférer lire l'annexe 1 avant de lire cet exemple

(**) A noter que le corps de l'argument fonctionnel (LAMBDA...) peut faire référence à la variable WAVELENGTH sans déclaration particulière, parceque cette référence est dans le champ textuel de

(3)

cette variable (lexical scoping) ; c'est un des intérêts de la notion de fonction anonyme.

Chapitre 4. Exemple 2 - Mesure informelle du

pouvoir d'expression d'un langage

Du point de vue de la théorie du calcul, tous les langages de programmation usuels sont équivalents (plus

précisément Turing équivalent, du mathématicien anglais Alan Turing). Ceci concerne le pouvoir d'expression théorique d'un langage de programmation mais n'exprime en rien le pouvoir d'expression effectif d'un langage, c'est-à-dire la facilité avec laquelle un langage permet de programmer un problème donné. Il n'y a pas de test formel de mesure du pouvoir d'expression effectif d'un langage. On a montré dans l'exemple précédent,

comment les moyens d'abstraction du Lispcontribuaient au pouvoir d'expression du Lisp. D'autre part, on peut mesurer le pouvoir d'expression effectif d'un langage de façon informelle, par la facilité relative avec laquelle il permet la résolution de certains problèmes fondamentaux réputés difficiles.

En 1983, un problème de ce type fût proposé par Ken Thomson lors d'un exposé dans le cadre de la chaire Turing; ce "défi", lancé à la communauté informatique, était d'écrire un programme n'ayant d'autre fonction que de s'auto-reproduire et qui soit le plus court possible (l'exécution de ce programme doit donc reproduire une copie exacte du programme tel que le programmeur l'a écrit; autrement dit, il s'agit d'écrire un programme source qui, une fois compilé et exécuté, produit le programme source).

Ken Thomson, l'auteur de ce problème et un des créateurs d'UNIX, a écrit une version en langage C de ce programme; celui-ci

comporte 233 lignes d'un code pas très attrayant (voir [VALD91]). La version Pascal est plus courte (28 lignes) mais également peu lisible.

En Lisp, ce programme ne prend que 2 lignes et est d'une lecture aisée pour qui comprend un peu de Lisp:

((LAMBDA (X) (LIST X (LIST 'QUOTE X)))

'(LAMBDA (X) (LIST X (LIST 'QUOTE X))))

L'évaluation de cette expression Lisp donne donc à nouveau la même expression, soit:

((LAMBDA (X) (LIST X (LIST 'QUOTE X)))

(4)

=>

((LAMBDA (X) (LIST X (LIST 'QUOTE X)))

'(LAMBDA (X) (LIST X (LIST 'QUOTE X)))) Commentaires

Cette expression Lisp se lit comme l'application de la fonction anonyme (LAMBDA (X) (LIST X (LIST 'QUOTE X)) à l'argument (LAMBDA (X) (LIST X (LIST 'QUOTE X)). L'argument étant lui-même un programme (c'est-à-dire une expression Lisp), on a ici l'exemple d'un programme manipulant un programme.

Voyez dans le chapitre 3 la notion de fonction anonyme, ainsi que l'annexe 1 pour les notions relatives à l'application d'une fonction et aux opérateurs LIST et QUOTE (ou à l'abréviation de QUOTE par le caractère macro apostrophe - ' -).

Chapitre 5. Exemple 3 : Manipulation symbolique et

extension du langage

5.1. Une fonction de menu générique

L'exemple de ce chapitre illustre divers aspects du Lisp dont les fonctions génériques, la manipulation symbolique et l'extension du langage par des macros.

STRUCTURE-MENU est une fonction qui prend en argument une instance de structure quelconque et génère un menu de type formulaire permettant de visualiser et de modifier les champs de cette instance de structure (voir la figure de la page suivante). Dans ce chapitre, l'objectif n'est pas de détailler comment cette fonction est programmée, mais d'illustrer certaines techniques de programmation à travers cette fonction et de montrer combien il est simple, pour un programme client, d'utiliser cette fonction.

Bien que plus riche, la notion de structure en Lisp est similaire à celle de structure, de record ou d'enregistrement que l'on trouve dans d'autres langages; généralement, les autres termes associés à cette notion sont :

 instance ou incarnation d'une structure,

champ ou composant (slot en anglais) d'une structure,

 valeur d'un champ d'une instance de structure.

En Lisp, DEFSTRUCT est l'opérateur de définition d'une structure; dans l'exemple ci-dessous, la structure créée a PERSON comme nom et possède 4 champs (NAME, SEX, TITLE et AGE).

(5)

(DEFSTRUCT PERSON NAME

SEX TITLE AGE)

Pour créer une instance de cette structure, il suffit de faire appel au constructeur MAKE-PERSON (automatiquement défini par

le DEFSTRUCT, lors de la définition de la structure PERSON). L'instance est affectée à la variable PERSON-1 via l'opérateur d'affectation SETF (voir l'annexe 1 pour plus de détails à propos de SETF) : (SETF PERSON-1 (MAKE-PERSON :NAME 'SMITH :SEX 'M :TITLE 'MR :AGE 35))

Une fois la fonction STRUCTURE-MENU programmée, pour faire apparaître le menu-formulaire permettant de voir et de modifier le contenu des 4 champs de l'instance de structure PERSON-1, il suffit d'écrire :

(STRUCTURE-MENU PERSON-1)

La figure ci-dessous montre un exemple d'interaction de l'utilisateur avec ce menu : l'utilisateur a sélectionné le champ TITLE; une

fenêtre de saisie apparaît où il peut introduire une nouvelle valeur pour ce champ, soit Dr.

La fonction STRUCTURE-MENU est générique : elle est

utilisable pour n'importe quelle instance de n'importe quelle structure. Cela signifie aussi qu'il est possible d'ajouter un champ à la définition de la structure PERSON sans pour autant devoir modifier les programmes qui utilisent le menu en question (le nouveau champ apparaissant automatiquement dans le menu). Commentaires

1. Les structures Lisp définies par DEFSTRUCT sont sophistiquées; en voici quelques caractéristiques :

o En plus de définir un constructeur

(MAKE-PERSON), DEFSTRUCT définit un copieur, des accesseurs pour chacun des champs, etc. Par exemple, dans le cas de la structure PERSON, sont définis le copieur COPY-PERSON et l'accesseur PERSON-AGE pour le champ AGE :

(6)

(PERSON-AGE PERSON-1) => 33

o Pour chaque champ, on peut spécifier un type et une valeur

par défaut. Il n'y a pas de restriction sur les types de valeur que peut prendre un champ.

o La définition d'une structure peut inclure la définition d'une

autre structure (héritage des champs).

2. En pratique, la fonction STRUCTURE-MENU sera plus élaborée; entre autres:

o elle autorise la définition de fonctions de validation explicites

(voir le paragraphe 5.2. ci-dessous);

o elle peut prendre des arguments optionnels pour, par

exemple, spécifier la position du menu sur l'écran ou indiquer que certains champs de l'instance ne peuvent figurer au menu.

3. Remarquons qu'il n'est pas nécessaire d'allouer explicitement de la mémoire pour créer l'instance PERSON-1.

5.2. Fonction de validation : manipulation symbolique

Lorsque la valeur d'un champ est modifiée dans le menu, il y a une première validation de la nouvelle valeur qui est faite en fonction du type de donnée que peut recevoir le champ (si ce type a été spécifié dans la définition de la structure). A cela on peut vouloir ajouter une validation plus précise, comme par exemple dans le cas du champ AGE où on voudrait vérifier que l'âge est compris entre 0 et 120 :

(DEFUN VALIDATE-PERSON-AGE (NEW-AGE)

;; NEW-AGE: la nouvelle valeur introduite via le menu pour ;; le champ AGE d'une instance de structure PERSON.

;; Renvoie T si NEW-AGE est compris entre 0 et 120, sinon ;; renvoie NIL (NB: les symboles T et NIL représentent les 2 ;; valeurs de vérité VRAI et FAUX).

(IF (AND (>= NEW-AGE 0) (<= NEW-AGE 120)) T

NIL))

Par quel mécanisme cette fonction est-elle appliquée lorsque

l'utilisateur modifie le champ AGE dans le menu? En fait, la fonction STRUCTURE-MENU vérifie l'existence de la fonction dont le nom est la concaténation de VALIDATE- et du nom de la fonction accesseur

(7)

du champ courant (soit ici PERSON-AGE); si cette fonction existe, elle est appliquée. Ceci pourrait être programmé ainsi :

(DEFUN STRUCTURE-MENU (STRUCTURE-INSTANCE) ...

(SETF VALIDATION-FUNCTION-NAME

(MAKE-VALIDATION-FUNCTION-NAME STRUCTURE-NAME SLOT-NAME)) (WHEN (FBOUNDP VALIDATION-FUNCTION-NAME)

(SETF OK? (VALIDATION-FUNCTION-NAME NEW-VALUE))) (IF OK? (...) (...)) ...) Commentaires

1. MAKE-VALIDATION-FUNCTION-NAME est la fonction qui génère le nom de la fonction de validation (par concaténation de symboles), soit, ici, VALIDATE-PERSON-AGE.

2. FBOUNDP est un prédicat qui vérifie qu'il y a une valeur fonctionnelle associée à VALIDATION-FUNCTION-NAME (FBOUNDP est l'acronyme de Function-BOUND-Predicate).

3. (VALIDATION-FUNCTION-NAME NEW-VALUE) est l'application de la fonction de validation à la nouvelle valeur introduite via le menu. La valeur de vérité (vrai ou faux, T ou NIL en Lisp) renvoyée par cette fonction est assignée à la variable OK? via l'opérateur SETF.

4. (IF OK? (...) (...))

Ceci correspond à la structure de contrôle bien connue : SI OK? = vrai

ALORS ... SINON ...

5. Remarquons le style déclaratif de ce type de programmation : si le programmeur veut ajouter une validation, il ne doit pas modifier le code existant (ce qui est un facteur d'erreur); il doit simplement définir une nouvelle fonction.

5.3. Extension du langage

Pour écrire la fonction STRUCTURE-MENU, il est nécessaire

d'accéder aux noms des champs de la structure; il n'existe pas de langage qui fournisse une primitive permettant d'accéder aux noms

(8)

des champs d'une instance. Cependant, en Lisp cela se réalise aisément, grâce au caractère auto-programmable du Lisp.

On peut définir une macro, EXTENDED-DEFSTRUCT, qui aura deux rôles, celui de définir la structure et celui de rendre accessibles les noms des champs de la structure; la définition de la structure PERSON, s'écrira alors comme ceci :

(EXTENDED-DEFSTRUCT PERSON NAME SEX TITLE AGE) => ...

L'évaluation de la forme ci-dessus (l'application de

la macro EXTENDED-DEFSTRUCT aux arguments PERSON NAME ...) a pour conséquence la définition de la structure comme précédemment et l'accès possible aux noms des champs comme ceci :

(GET 'PERSON 'SLOT-NAMES) => (NAME SEX TITLE AGE)

EXTENDED-DEFSTRUCT est défini comme une macro à l'aide de l'opérateur DEFMACRO; pour les lecteurs intéressés, voici cette macro :

(DEFMACRO EXTENDED-DEFSTRUCT (NAME &REST SLOT-NAMES) `(PROGN

(PUTPROP (QUOTE ,NAME) (QUOTE ,SLOT-NAMES) 'SLOT-NAMES)

(DEFSTRUCT ,NAME ,@SLOT-NAMES))) Commentaires

1. Il y a deux phases dans l'application d'une macro : premièrement, génération de code et deuxièmement évaluation du code généré. Dans notre exemple, le code généré est :

(PROGN

(PUTPROP 'PERSON '(NAME SEX TITLE AGE) 'SLOT-NAMES)

(DEFSTRUCT PERSON NAME SEX TITLE AGE)) où on retrouve la définition de la structure PERSON.

(9)

PUTPROP associe une propriété SLOT-NAMES au symbole PERSON, propriété dont la valeur est la liste des noms de champs.

GET est l'opérateur qui permet d'accéder à la valeur d'une propriété d'un symbole.

2. La syntaxe du DEFSTRUCT est plus fouillée que ne le montre l'exemple, notamment parce qu'il est possible de spécifier le type d'un champ. Pour tenir compte de la syntaxe complète

de DEFSTRUCT, la macro EXTENDED-DEFSTRUCT sera donc un peu plus complexe; d'autre part, elle permettra aussi de retrouver le type associé à un champ.

Chapitre 7

Synthèse et Conclusion

On reconnaît à chaque langage une certaine spécificité : le Fortran facilite le traitement numérique, ADA permet de développer d'importantes applications temps réel, le C est un langage machine de haut niveau convenant aux

applications proches du système opératoire. Le langage Lisp quant à lui, porte l'abstraction à un très haut niveau, l'abstraction étant comprise comme la faculté de représenter simplement des problèmes complexes.

UN LANGAGE A HAUT POUVOIR D'EXPRESSION EFFECTIF

Du point de vue de la théorie du calcul, tous les langages de programmation usuels sont équivalents (plus précisément Turing équivalent, du mathématicien anglais Alan Turing). Ceci concerne le pouvoir d'expression théorique d'un langage de programmation mais n'exprime en rien le pouvoir d'expression effectif d'un langage, c'est-à-dire la facilité avec laquelle un langage permet de programmer un problème donné.

Il n'y a pas de test formel pour mesurer le pouvoir d'expression effectif d'un langage. Cependant, il est clair que les moyens d'abstraction particuliers du Lisp contribuent à son pouvoir d'expression. On peut mesurer le pouvoir d'expression effectif d'un langage de façon informelle, par la facilité relative avec laquelle il permet la résolution de certains problèmes fondamentaux réputés difficiles.

En 1983, un problème de ce type fût proposé par Ken Thomson lors d'un exposé dans le cadre de la chaire Turing (Ken Thomson est sans doute plus connu comme un des auteurs du système opératoire UNIX); ce "défi", lancé à la

communauté informatique, était d'écrire un programme n'ayant d'autre fonction que de s'auto-reproduire et qui soit le plus court possible (l'exécution de ce programme doit donc reproduire une copie exacte du programme tel que le programmeur l'a écrit; autrement dit, il s'agit d'écrire un programme source qui, une fois compilé et exécuté, produit le programme source).

Ken Thomson a écrit une version en langage C de ce programme; celui-ci comporte 233 lignes d'un code pas très attrayant. En Lisp, ce programme ne

(10)

prend que 2 lignes et est d'une lecture aisée dès que l'on comprend un peu de Lisp.

Bien que ce type de démonstration de la puissance effective d'un langage puisse paraître académique et sujette à caution, l'expérience montre que ces résultats sont extrapolables aux applications réelles. On constate en effet que de

nombreux progiciels très élaborés mettent à profit le pouvoir d'expression

effectif du Lisp, comme par exemple, Interleaf, un progiciel destiné à l'édition de documents techniques complexes. La version 5 de ce progiciel comporte

1.500.000 lignes de code C et 250.000 lignes de code Lisp; ce

code Lisp représente environ 25% des fonctionnalités du système et, selon les concepteurs d'Interleaf, apporte un gain considérable en terme de taille de code. AutoCAD et l'éditeur EMACS sont 2 autres exemples de mise à profit des caractéristiques du Lisp.

UN LANGAGE POUR LES DEVELOPPEURS

Ce haut pouvoir d'abstraction, en réduisant le temps consacré à la

programmation, fait du Lisp un langage particulièrement intéressant pour les développeurs, c'est-à-dire toutes personnes chargées non seulement de la programmation mais aussi de l'analyse et du design d'une application. Dans les Divisions Ingénierie de TRACTEBEL, sont ainsi concernés les nombreux

ingénieurs qui, dans les tâches de conception et d'études, cumulent les rôles d'utilisateur et de développeur.

UN LANGAGE FONCTIONNEL, UN LANGAGE ELEGANT

Le Lisp, comme la plupart des langages fonctionnels (voir ci-dessous), est un langage élégant, c'est-à-dire un langage qui permet d'écrire des

programmes succints et faciles à lire par d'autres programmeurs. Selon D. Ince, qui définit ainsi l'élégance d'un langage de programmation, il n'est pas rare d'écrire un programme dans un langage de très haut niveau (comme le Lisp), qui soit dix fois moins long que le programme équivalent écrit dans un langage conventionnel (voir [INCE88]).

Contrairement à une idée répandue, le Lisp est un langage plutôt facile à

apprendre (voir les paragraphes 2.4): on constate ainsi que le Lisp remplace de plus en plus souvent le Pascal comme langage d'apprentissage de la

programmation. En outre, le Lisp simplifie la tâche de programmation: c'est un corollaire du haut pouvoir d'abstraction et de la flexibilité de ce langage.

Quelles sont les caractéristiques du Lisp qui lui confèrent ce pouvoir d'expression inégalé? D'une part le modèle d'exécution du Lisp répond au modèle

fonctionnel : tout programme Lisp s'exprime en termes d'applications de fonctions à des objets (c'est-à-dire des données). Une fonction prend zéro ou plusieurs objets comme arguments, effectue un traitement et renvoie un ou plusieurs objets comme résultats (un argument pouvant être lui-même une application de fonction). D'autre part, en Lisp, il y a équivalence entre le programme et les données : les fonctions Lisp peuvent donc être traitées comme n'importe quelle donnée (par exemple être passées en argument d'un appel de fonction ou encore être stockées dans une structure de données). Il s'ensuit que le Lisp est un langage de programmation

auto-programmable, c'est-à-dire un langage dont il est possible d'étendre la syntaxe et la sémantique (voir le paragraphe 5.3 du chapitre 5; voir aussi [HASE89], où

(11)

il est montré qu'il est possible de créer un langage à objets - pour faire de la programmation orientée objet - à partir du Common Lisp en seulement 250 lignes de code - ce langage à objets disposant de l'héritage multiple).

UN LANGAGE GENERAL

Le Lisp est un langage général : son domaine d'application est large, loin de se limiter aux applications de l'"intelligence artificielle". Aujourd'hui, la vulgarisation de la mémoire centrale fait du Lisp un langage utilisable sur de nombreux types de machines, y compris les micro-ordinateurs. La standardisation de ce langage simplifie le portage des applications (voir l'annexe 3 pour la portabilité du Common Lisp); cette simplification concerne aussi le portage des interfaces hommes-machines, grâce aux efforts de standardisation en matière de

gestionnaires d'interface (CLIM, Common Lisp Interface Manager). Le Lisp jouit d'une bonne intégration dans les autres langages : un

programme Lisp peut invoquer un programme C ou Fortran et échanger des données avec ces programmes (par exemple, voir la description de

l'application Tropic dans l'annexe 2).

UN LANGAGE ADAPTABLE

L'histoire du Lisp montre comment il s'est adapté rapidement aux nouvelles idées; cela a été le cas avec la programmation structurée et avec la

programmation orientée objet (CLOS - Common Lisp Object System). Dans les années 70, dès l'apparition du concept de la programmation structurée et grâce au caractère auto-programmable du Lisp, les utilisateurs du Lisp ont pu intégrer immédiatement de nouvelles structures de contrôles à leurs programmes. Lorsque les langages Simula et Smalltalk eurent démontré l'intérêt de la programmation orientée objet dans la quête de la maîtrise de la complexité, diverses extensions objet de Lisp apparurent rapidement (dont, vers

1975, LOOPS et FLAVORS, les ancêtres de l'actuel système d'objets de Common

Lisp - CLOS). Dès ce moment, ces extensions objet de Lisp allaient plus loin

que Smalltalkou même que le récent C++ (par exemple en permettant l'héritage multiple et la combinaison de méthodes).

Un phénomène similaire s'est produit lors de la disponibilité des architectures parallèles : en effet, il existe aujourd'hui diverses extensions de Common

Lisp permettant de profiter de ces nouvelles architectures (voir [MURR 90]); de

plus l'aspect auto-programmable du Lisp permet aux utilisateurs de ces

architectures de définir des instructions parallèles de haut niveau d'abstraction, appropriées à une application spécifique.

UN LANGAGE POUR AUJOURD'HUI ET DEMAIN

Cette adaptabilité est un gage de la pérennité du Lisp, qui, de fait, est un des plus vieux langages de programmation (1956). Toujours à propos de l'avenir du Lisp (et de ses descendants), il est significatif que le ministère américain de la défense ait déterminé le Common Lisp (dont CLOS) comme un des trois langages de programmation officiels pour ses applications (les deux autres étant ADA et CPL, un langage de prototypage). Si la niche occupée par

le Lisp dans le monde des langages de programmation continue de croître, ce n'est pas sans raisons. Ce dossier aura su, nous l'espérons, en rendre compte avec quelque clarté.

(12)

Une expression symbolique (ou expression) est soit un atome, soit une liste.

Atomes :

 atomes numériques : 12, 4.1, etc;

 atomes symboliques (ou symboles): poire, +, *, etc. Listes :

Une liste est une parenthèse gauche suivie de 0 ou plusieurs

éléments, suivis d'une parenthèse droite; un élément d'une liste est une expression symbolique (donc un atome ou une liste). Exemples : (+ 1 2) (+ 1 2 3) (* 1 2) (+ 1 2 (* 1 2 3)) Remarques

1. Les listes ci-dessus sont toutes des expressions correctes (c'est-à-dire bien formées) du langage : toutes ces

expressions respectent la syntaxe du langage (autrement dit, ce sont des programmes). La syntaxe générale de

l'application d'une fonction à des arguments s'écrit : (<fonction> <argument1> <argument2> ...)

2. En plus d'être une expression bien formée comme ci-dessus, une liste peut être simplement une donnée du type liste, comme dans la liste (1 2 3). En fait, il n'y a pas de distinction entre les programmes et les données : (+ 1 2 3) est lu

comme un programme ou comme une donnée. A ce stade, on peut considérer les données du Lispcomme étant l'ensemble des objets atomiques (numériques et symboliques) et des listes. Exemples de données : 0 pomme (0 1 2 3) (0 (1) (2 (3))) (+ 1 2 3)

(13)

3. La liste vide est représentée par () ou par le symbole NIL. 4. Notez que la définition d'une liste est une définition récursive.

2. L'évaluation d'un expression Lisp <="" td=""> Home <="" td=""> Up <="" td=""> Previous <="" td=""> Next

A toute expression Lisp bien formée correspond une valeur; le calcul de la valeur d'une expression s'appelle l'évaluation

d'une expression (en général on utilise le mot forme pour

désigner une expression Lisp correcte; on parle de la valeur

renvoyée par une forme).

Supposons que nous soyons devant la console d'un ordinateur et qu'un interpréteur Lisp soit prêt à interagir; introduisons à l'aide du clavier l'expression (la forme) suivante :

(+ 2 3)

L'interpréteur Lisp affichera la réponse "5" à l'écran (le résultat de l'évaluation de cette expression). Dans la suite, ce type d'interaction sera représenté comme ceci :

(+ 2 3) => 5

Remarque

Ceci correspond à un cycle de ce qui est appelé la boucle READ-EVAL-PRINT de l'interpréteur Lisp : 1. READ : lecture de l'expression,

2. EVAL : évaluation de l'expression,

3. PRINT : impression d'un résultat (à l'écran). 2.1. L'évaluation d'un atome

Certains atomes s'évaluent à eux-mêmes; c'est le cas des atomes numériques :

23.5 => 23.5

De même, les 2 symboles spéciaux T et NIL (les 2 valeurs de vérité, vrai et faux):

NIL

(14)

<pT

=> T </p

Une valeur (une donnée) peut être associée à un atome symbolique (ou symbole; voir ci-dessous, le paragraphe 4, l'opérateur d'affectation). L'évaluation d'un symbole produit la valeur associée (s'il n'y a pas de valeur associée,

l'évaluation d'un symbole produit une erreur): Vitesse

=> 60

2.2. L'évaluation d'une liste

Quand l'évaluateur évalue une liste, il "s'attend" à trouver un opérateur - autrement dit une fonction - en tête de liste (le premier élément de la liste). L'opérateur est suivi de zéro ou plusieurs arguments. Cet opérateur est

généralement un symbole auquel est associée une valeur fonctionnelle (ou procédure, c'est-à-dire la spécification pas à pas de ce qu'il y a à faire). Ainsi dans l'évaluation :

(+ 1 2 3) => 6

L'opérateur est le symbole + auquel est associée la procédure d'addition. Il est suivi de trois arguments. Voici une description plus détaillée du processus d'évaluation d'une liste :

1. retrouver la procédure associée au premier élément de la liste;

2. évaluer chacun des éléments restants de la liste (un élément de liste étant une expression, soit un atome ou une liste);

3. appliquer la procédure trouvée en 1 aux valeurs trouvées en 2.

On voit que ce processus est récursif; ainsi dans l'évaluation de

(+ 12.5 (* 2.5 2)) Les étapes sont :

1. retrouver la procédure associée au symbole + 2. évaluer l'argument 12.5, soit :

(15)

3. évaluer l'argument (* 2.5 2), soit : 12.5 => 12.5

3.1. retrouver la procédure associée au symbole * 3.2. évaluation du 1er argument

2.5 => 2.5

3.3. évaluation du 2ème argument 2 => 2

3.4. l'application de la procédure * aux deux arguments donne 5

4. appliquer la procédure + aux valeurs 12.5 et 5, soit 17.5. En résumé, la règle générale et complète de l'évaluation d'une expression (d'une forme) peut s'écrire :

1. si l'expression est un atome, renvoyer sa valeur 2. si l'expression est une liste :

2.1. retrouver la procédure associée au premier élément de la liste

2.2. évaluer chacun des éléments restants de la liste (un élément de liste étant une expression, soit un atome ou une liste)

2.3. appliquer la procédure trouvée en 2.1. aux valeurs trouvées en 2.2.

L'opérateur <ifirst< i="">renvoie le premier élément d'une liste; supposons que l'on désire obtenir le premier élément de la liste (a b c) :</ifirst<>

(FIRST (a b c)) => erreur!

<>En effet, l'évaluateur cherche la valeur fonctionnelle de "a", or "a" n'a pas de valeur fonctionnelle associée ("a" n'est pas une fonction), d'où l'affichage d'un message d'erreur. Par contre : (FIRST (QUOTE (a b c))

(16)

L'opérateur QUOTE prend un argument. Contrairement à la règle d'évaluation d'une expression énoncée ci-dessus, l'argument de QUOTE n'est pas évalué (QUOTE fait exception à la règle et est dit fonction spéciale). L'application de QUOTE à un argument renvoie simplement l'argument non évalué (QUOTE bloque l'évaluation de l'argument). Ainsi :

(QUOTE (a b c)) => (a b c)

(QUOTE a) => a

En pratique, l'opérateur QUOTE étant souvent utilisé, on a adopté une abréviation (celle du caractère macro '):

'(a b c) => (a b c)

(FIRST '(a b c)) => a

L'opérateur LIST est l'opérateur de construction d'une liste : (LIST 'a 'b 'c 1 2 3)

=> (a b c 1 2 3) Remarques

1. C'est le reader qui convertit '(a b c) en (QUOTE (a b c)) (voir ci-dessus la boucle READ-EVAL-PRINT). De façon générale, le reader convertit '<expression> en (QUOTE<expression>) . Un programmeur Lisp peut aisément définir ses propres

caractères macros.

Cette possibilité d'attacher un comportement fonctionnel à un caractère permet de définir aisément de nouveaux caractères syntaxiques: c'est une des propriétés du Lisp très appréciée pour implanter les special purpose programming languages (SPPL - voir le paragraphe 1.1, chapitre 1).

2. Dans l'exemple 1 (chapitre 3), on a vu un autre cas

de caractères macros, #' dans #'(LAMBDA ...). #'(LAMBDA ...) est convertit en (FUNCTION (LAMBDA

...)), FUNCTION étant un opérateur Lisp annonçant une fonction.

(17)

On a vu qu'il était possible d'associer une valeur à un symbole; cela peut se faire via l'opérateur d'affectation SETF. Supposons

qu'aucune valeur ne soit encore affectée au symbole Vitesse : Vitesse => erreur! (SETF vitesse 60) => 60 Vitesse => 60 (SETF vitesse 100) => 100 Vitesse => 100

L'opérateur SETF a 2 particularités :

1. Il n'évalue pas le premier argument, donc il ne respecte pas la règle générale d'évaluation d'une expression vue plus

haut. SETF, comme QUOTE et DEFUN, l'opérateur de

définition d'une fonction utilisateur, fait partie des opérateurs dits spéciaux : ceux-ci ont leurs propres règles d'évaluation. Parmi les primitives du Lisp (c'est-à-dire les opérateurs définis par la norme du langage), ces opérateurs spéciaux sont

minoritaires. Bien entendu, les fonctions définies à l'aide de DEFUN respectent la règle générale d'évaluation.

2. Après avoir renvoyé une valeur, l'opérateur SETF a un effet persistant en ce sens qu'il a associé de façon permanente une valeur à un symbole : c'est le principe de l'affectation; on dit aussi que SETF a un effet de bord. C'est une liberté qui est prise par rapport au modèle de calcul purement fonctionnel où les fonctions n'ont pas d'effet de bord (la majorité des

fonctions Lisp n'ont pas d'effet de bord, ce qui implique qu'elles n'altèrent pas leurs arguments).

Figure

Updating...