Pour introduire les idées de la programmation nous allons utiliser un langage fonctionnel nommé Scheme. Ce langage peut être traduit en langage machine par un compilateur ou par un interprète ; comme le même logiciel peut parfois être utilisé soit comme interprète soit comme compilateur, nous parlerons aussi de système de programmation2.1. Nous nous intéresserons dans un premier temps à l'utilisation de Scheme par l'intermédiaire d'un interprète.
Un interprète est un programme qui simule une machine virtuelle dont le langage machine serait le langage de programmation utilisé (ici Scheme). Nous lui soumettrons pour qu'il les « lise » des expressions rédigées en Scheme, si elles sont bien formées l'interprète les évaluera (c'est à dire demandera au processeur de notre ordinateur d'effectuer les actions nécessaires au calcul qu'elle décrit et d'en donner le résultat), affichera le résultat et sera prêt à interpréter une autre expression du langage. Si notre expression est mal formée, l'interprète nous signalera que nous avons commis une erreur de syntaxe. On appelle ce processus la « boucle d'interrogation » ou « boucle d'interaction » (« read-eval-print loop44 », ou « REP loop44 »).
Un langage de programmation n'est pas seulement un moyen de demander à l'ordinateur le résultat d'un calcul : le texte d'un programme est aussi un moyen de noter et d'organiser ses idées, puis de les partager avec d'autres personnes. Les premières expressions que nous rédigerons en Scheme seront très brèves, mais un programme peut comporter des milliers de lignes, réparties en chapitres et en paragraphes. Pour permettre la rédaction harmonieuse de programmes éventuellement volumineux et complexes, un langage doit posséder les mécanismes suivants :
Le chapitre1 nous a appris que la mémoire d'un ordinateur contenait des données et des programmes, et qu'entre les deux il n'y avait d'ailleurs qu'une différence sémantique, d'interprétation si l'on veut. Cette distinction se reflète dans Scheme où nous avons à notre disposition deux sortes d'objets : des données et des procédures. Les données sont en quelque sorte la matière qui constitue les objets que nous voulons traiter, et les procédures sont les descriptions des traitements que nous voulons infliger aux données. La rédaction de procédures et leur application aux données réalise le miracle de l'informatique : écrire c'est faire. Au pied de la lettre.
En fait Scheme nous donne comme le langage-machine les moyens d'accéder aux éléments physiques de l'ordinateur (le processeur et la mémoire) mais au travers d'une représentation plus abstraite (de plus haut niveau) qui nous facilite l'expression de nos idées et permet de leur donner une formulation plus générale.
Nous allons étudier les règles de construction des procédures.
Si nous lançons l'exécution de l'interprète Scheme sur l'ordinateur, il nous répond par une invite, qui indique qu'il attend que nous lui soumettions une expression Scheme pour qu'il la lise, l'évalue, et en affiche la valeur. Voici l'invite avec Scheme sous Unix (Bigloo) :
1:=>
:
Cette invite indique que nous sommes au plus haut niveau de la « boucle d'interrogation », nous dirons aussi la boucle d'interaction. Nous dirons aussi (le sens de ces expressions et les nuances entre elles se préciseront au fil de l'exposé) que nous sommes au Top-level44 .
Commençons par une expression particulièrement simple, une expression arithmétique :
1:=> 42
42
Scheme2.2 imprime la valeur de l'expression arithmétique « 42 », qui est 42. La valeur d'une expression arithmétique réduite à un nombre est le nombre qu'elle dénote, on dit que les nombres sont des expressions auto-évaluantes2.3.
Si Scheme imprime la valeur de l'expression « 42 », ce n'est pas sur notre ordre explicite : dans la boucle d'interaction, au Top-level44 , l'interprète imprime la valeur de la dernière expression qu'il a évaluée. La même expression évaluée dans un autre contexte (par un programme compilé, ou dans une sous-expression par exemple) ne donnerait pas lieu à impression. Il faut bien distinguer le fait que l'évaluation d'une expression retourne une valeur (c'est la règle générale en Scheme) du fait que cette valeur soit imprimée (ce qui n'est pas toujours le cas).
Voici d'autres nombres donnés sous forme littérale (les signes qui les dénotent). Les valeurs de telles expressions littérales sont des constantes numériques :
1:=> -4
-4
1:=> -4.
-4.00000000000
1:=> .9
0.900000000000
1:=> 3.14
3.14000000000
1:=> 4e-3
0.00400000000000
1:=> 4.2e3
4200.00000000
Notez qu'un nombre peut s'écrire précédé de son signe ou du point décimal tout seul, équivalent anglo-saxon de notre virgule. La présence d'un point décimal ou d'un exposant précédé de la lettre « e » permet la distinction syntaxique entre nombres entiers et nombres fractionnaires (« avec des chiffres après la virgule »). Le nombre noté 4.2e3 est 4.2 x 103.
Les caractères sont écrits avec la notation suivante :
1:=> #\
a
a
Les caractères sont des objets auto-évaluants . On peut noter la différence entre la forme interne de l'objet (celle qu'imprime l'évaluateur) et sa forme externe (celle que nous écrivons à l'invite du lecteur). Certains caractères ont un nom :
#\
space
#\
newline
Les chaînes de caractères (string en anglais) sont des suites de caractères que l'on soumet à l'interprète entre guillemets ; ce sont aussi des objets auto-évaluants. Attention, une chaîne de un caractère n'est pas une expression de même type qu'un caractère :
1:=> "a"
a
1:=> "Le soleil brille"
Le soleil brille
Nous avons vu qu'un langage de programmation devait fournir au programmeur un moyen de choisir, en fonction de résultats obtenus précédemment, la suite à donner au calcul, l'action suivante à effectuer. Le cours de logique nous apprend que le formalisme approprié pour effectuer commodément ce genre de choix consiste à tester la valeur de vérité d'une proposition (est-elle vraie ou fausse ?). Il est utile de disposer pour ce faire d'objets pour exprimer le vrai et le faux. Nous nommerons de tels objets « booléens » en mémoire du logicien George Boole. Les valeurs booléennes qui dénotent le vrai et le faux (true et false) sont notées en Scheme #t et #f :
1:=> #t
#t
1:=> #f
#f
Les objets booléens sont auto-évaluants.
Les identificateurs jouent le même rôle en Scheme que les mots dans la langue française. Ils ont trois usages :
Ces trois usages seront précisés dans les pages qui suivent.
Est un identificateur valide une séquence de lettres, de chiffres et de signes diacritiques qui commence par un caractère qui ne peut pas être le premier caractère d'un nombre (chiffre, signe, point) et qui n'est pas l'espace blanc. Scheme ne distingue pas les minuscules des majuscules dans un identificateur. Voici des identificateurs :
foo a+ deux-en-un string->symbol *ici+
Les symboles sont des objets dont l'utilité repose sur la propriété que deux symboles sont identiques si et seulement si leurs noms s'écrivent de la même façon. Cette propriété permet notamment d'utiliser les symboles pour nommer des objets (ils représentent alors des identificateurs), mais ce n'est pas leur seul usage.
Il importe de noter qu'en Scheme un nombre n'est pas un symbole.
Les symboles ne sont pas auto-évaluants. Ils sont même souvent utilisés, comme nous allons le voir, pour représenter d'autres objets.
Les commentaires ne sont pas des expressions, au contraire ce sont des textes que nous voulons soustraire à l'évaluation. Il est recommandé d'introduire dans les programmes des indications pour en faciliter la compréhension au lecteur humain.
Le début d'un commentaire se note par un point-virgule, et tout ce qui suit jusqu'à la fin de la ligne sera commentaire.
; le programme qui suit calcule le triangle de
; Pascal, il est couvert par un copyright au nom
; de Blaise P.
1
1 1
1 2 1 ; l'algorithme est original
Au lancement d'un système Scheme un certain nombre d'opérations sont disponibles, notamment des procédures primitives que nous pouvons utiliser pour nos calculs. Nous pouvons en première approximation les considérer comme des programmes déjà tout écrits.
Les expressions représentant des nombres peuvent être combinées avec une expression représentant une procédure primitive (comme + ou *) pour former une expression composée qui représente l'application de la procédure à ces nombres. Par exemple :
1:=> (+ 67 24)
91
1:=> (- 48 67)
-19
1:=> (/ 20 12)
1.66666666667
De telles expressions formées d'une liste d'expressions encadrées de parenthèses dénotent l'application d'une procédure. L'élément le plus à gauche de la liste est appelé opérateur ; les autres opérandes ; l'opérateur est une expression qui dénote une procédure. La valeur d'une telle expression composée est obtenue en appliquant la procédure représentée par l'opérateur aux arguments, qui sont les valeurs des opérandes. L'exemple suivant illustre ce mécanisme d'application :
1:=> (* 2 (+ 4 5))
18
Dans l'expression ci-dessus l'opérateur est représenté par le symbole de multiplication, les opérandes sont 2 et une expression arithmétique, (+ 4 5). Les arguments sont 2 et la valeur de (+ 4 5), qui sera obtenue par l'application de l'addition à 4 et 5. Au passage nous avons appris à composer des actions, ici les deux opérations arithmétiques d'addition et de multiplication.
Une procédure est un objet Scheme qui peut appliquer un traitement à des arguments. La procédure dénotée par « + » applique la fonction d'addition à ses arguments. Certains auteurs emploient le terme fonction aussi pour désigner les procédures ; ce n'est pas illégitime, mais nous réserverons plutôt le terme fonction à l'entité mathématique qui décrit la relation entre l'ensemble de définition des arguments et l'ensemble des valeurs de la fonction considérée, et le terme procédure à l'objet informatique qui décrit les opérations à effectuer pour obtenir le résultat voulu lorsqu'on l'applique aux arguments.
Les appels de procédure peuvent être évoqués sous le nom d'application.
On aura remarqué que pour les opérations de l'arithmétique élémentaire Scheme adopte une disposition de l'opérateur et des opérandes différente de la notation usuelle. Scheme utilise la notation dite préfixée parenthésée, par opposition à la notation usuelle, dite infixe :
La parenthèse ouvrante se place avant le nom de la procédure. Les arguments sont séparés par des blancs.
Cet usage systématique par Scheme de la notation préfixée parenthésée peut dérouter. Il a l'avantage de bien faire apparaître l'opérateur d'addition (par exemple) comme une expression qui représente la fonction d'addition. Ceci dit, cet usage répond aussi à des préoccupations théoriques fondamentales qui seront exposées ultérieurement.
Cette notation offre d'autres avantages, par exemple de pouvoir être utilisée sans modification syntaxique avec un nombre quelconque d'arguments qui peuvent être eux-mêmes des expressions composées. Ainsi, pour calculer l'expression (b2-4ac)/2a avec a=2, b=3 et c=5, on écrira :
1:=> (/ (- (* 3 3) (* 4 2 5)) (* 2 2))
-7.75000000000
La lecture d'une telle expression, un peu rebutante pour un humain, est très confortable pour un interprète Scheme, à cause de l'uniformité et de la simplicité des règles syntaxiques. On améliorera la lecture humaine par une présentation plus déliée :
(/
(- (* 3 3)
(* 4 2 5))
(* 2 2))
Les expressions les plus intérieures (celles à calculer en premier) sont celles dont la parenthèse ouvrante est placée le plus à droite. Les parenthèses ouvrantes des expressions de même niveau sont alignées verticalement. La lecture des opérandes d'un opérateur s'effectue horizontalement et à la suite s'ils sont élémentaires, verticalement et en retrait de l'opérateur si certains d'entre eux sont composés. Avec un éditeur de texte convenable (c'est à dire doté d'un mode spécial pour Scheme) il suffit de taper les expressions et les retours à la ligne, l'éditeur réalise automatiquement le décalage des alignements verticaux (appelé indentation ) et va jusqu'à vérifier qu'aucune parenthèse n'a été oubliée.
Puisque cette disposition est à l'usage des humains, elle peut être adaptée au goût de chacun, mais il importe au programmeur d'adopter un style en ce domaine et de s'y tenir. Un programme doit pouvoir être lu par un interprète Scheme, mais aussi par un humain, ne serait-ce que son auteur. En fait, un texte de programme sera lu beaucoup plus souvent par des humains que par des ordinateurs.
Les expressions Scheme bien formées (syntaxiquement correctes) sont appelées S-expressions44 . Nous pouvons dire, sous réserve de détails supplémentaires à venir, qu'une S-expression peut être :
Nous avons vu qu'une liste pouvait être le moyen de représenter l'application d'une procédure à ses arguments, mais nous verrons d'autres usages des listes. Les S-expressions qui ne sont pas des listes sont parfois appelées atomes.
Nous avons examiné plusieurs types d'expressions Scheme qui nous permettent de construire des expressions plus complexes qui réalisent des traitements, mais qui ne dépassent pas vraiment ce que nous pourrions faire avec une calculette. Pour aller plus loin il nous faudrait la possibilité d'obtenir des résultats de calculs et de les réutiliser pour des calculs ultérieurs, en les remémorant.
Un procédé auquel on peut penser pour permettre de remémorer une valeur, c'est de lui associer un nom selon un mécanisme tel que l'évocation ultérieure du nom donne accès à la valeur. L'objet abstrait ainsi construit, doté d'un nom et auquel on sait faire correspondre une valeur, s'appelle une variable. La correspondance entre une variable et sa valeur s'appelle une liaison.
Pour donner des noms aux variables, on utilise en Scheme des symboles. Ainsi on fait correspondre une entité syntaxique (un symbole) à une entité sémantique (une variable). Comme les symboles sont des S-expressions, nous serons en mesure d'introduire des variables dans nos constructions sitôt que nous saurons les créer. Un symbole utilisé pour nommer une variable est un identificateur, appelé simplement nom.
La combinaison des notions de symbole, de nom, de valeur, de variable et de liaison peut sembler complexe, et elle l'est. Nous pouvons la rapprocher du progrès que Ferdinand de Saussure a fait accomplir à la linguistique au début du siècle en montrant que le sens des signes du langage humain ne consistait pas en une association entre « une chose et un nom », mais résidait dans un processus mettant en jeu un signifiant et un signifié, le signifiant étant dans ce cas une image acoustique et le signifié l'idée d'une chose (et non pas la chose elle-même). Soit dit en passant il convient de remarquer que le seul endroit où puisse jamais se manifester une idée, c'est l'esprit humain, et d'en tirer les conséquences.
La première façon que nous avons, en Scheme, d'établir une liaison entre une variable et une valeur est la forme define :
(define masse 2)
Soumettre cette forme à Scheme a pour effet de créer (si elle n'existe déjà) une variable sous le nom masse et de la lier à la valeur 2. Désormais si je soumets à l'interprète le symbole masse il saura l'évaluer :
1:=> masse
2
Nous avons dit plus haut qu'une forme composée telle que : (+ 2 3) représentait l'application d'une procédure à des arguments. Le cas de (define masse 2) est une exception à cette règle. define n'est pas une procédure mais une forme spéciale . Scheme possède un petit nombre de formes spéciales pour réaliser des opérations particulières nécessitant des syntaxes particulières, impossibles à exprimer avec des procédures. C'est le cas ici, en effet masse ne peut pas être envisagé comme un opérande dont on prendrait la valeur, puisque qu'au moment du define, masse n'a pas encore de valeur (et pour cause, elle n'existe pas encore).
Notons aussi que la forme spéciale define décrit une définition, et que ce n'est pas une expression2.4. Il y a par ailleurs des formes spéciales qui décrivent des expressions.
define est un mot-clé syntaxique du langage Scheme, c'est à dire que sa mention a une signification bien précise. define est de surcroît un mot-clé réservé, c'est à dire que nous n'avons pas le droit de l'utiliser pour désigner des objets de notre cru. Scheme comporte une trentaine de mots-clés réservés.
Avec define nous créons des objets ; nous avons dit qu'une variable était un objet sémantique abstrait, ce qui existe concrètement c'est la liaison entre la variable et sa valeur. Où existe concrètement la liaison ? Nous avons dit au chapitre 1 que toute chose dotée d'un état existait dans la mémoire de l'ordinateur, et c'est une première réponse, qui est exacte, mais pas au bon niveau d'abstraction. Nous avons entrepris de décrire la programmation des ordinateurs à travers un modèle abstrait qui est le langage Scheme. Il nous faudrait une réponse dans les termes de ce modèle. Nous allons en introduire la notion.
En Scheme l'ensemble des liaisons visibles en un point du programme est nommé l'environnement en vigueur en ce point. L'endroit où un programme peut créer des liaisons à un instant donné est l'environnement courant2.5. Nous verrons qu'il peut y avoir de multiples environnements au cours du déroulement d'un programme, mais pour l'instant nous n'en avons qu'un à notre disposition, auquel nous avons accès lorsque l'interprète Scheme nous présente son invite et où nous retrouvons les liaisons dont nous avons besoin, qui s'appelle l'environnement global ou l'environnement proéminent (Top-level environment44 ) 2.6.
En écrivant (define masse 2) nous créons la variable masse dans l'espace abstrait des variables (un espace de noms, donc) et, dans l'environnement global, une liaison concrète entre la variable masse et la valeur 2. En invoquant le nom de la variable masse, nous sommons l'interprète d'aller chercher dans l'environnement courant (qui est à cet instant l'environnement global) une liaison concernant une variable nommée masse, s'il la trouve de nous renvoyer la valeur correspondante, et sinon de nous indiquer notre erreur :
1:=> (define une-masse 2)
UNE-MASSE
1:=> une-masse
2
1:=> toto
*** ERROR:bigloo:eval:
Unbound variable - TOTO
Nous n'avons pas défini d'objet nommé toto, il n'y a donc pas de liaison qui permette de retrouver sa valeur. On dit que la variable toto n'est pas liée, ce que nous dit en anglais notre interprète.
Notons au passage que Scheme ne distingue pas entre minuscules et majuscules. Il n'est pas recommandé d'utiliser dans un identificateur des caractères accentués ou, plus généralement, composés, certaines implémentations du langage peuvent ne pas les accepter.
Voici d'autres définitions de variables :
1:=> (define une-vitesse 100)
UNE-VITESSE
1:=> E-cinetique
10000
Nous pouvons modifier l'environnement en redéfinissant une variable déjà définie :
1:=> (define une-vitesse 30) 92 97
*** WARNING:bigloo:eval
redefinition of variable - UNE-VITESSE
UNE-VITESSE
On remarque que l'interprète que nous utilisons nous avertit de cette redéfinition. En effet, nous aurions pu par erreur réemployer un nom déjà utilisé, et ainsi perdre sans le désirer la valeur précédemment liée à la variable désignée par ce nom. Supposons ici que cette redéfinition est volontaire.
Notons aussi que cette redéfinition n'affecte pas la valeur des variables qui avaient été calculées en fonction de une-vitesse à l'époque où sa précédente définition était en vigueur :
1:=> E-cinetique
10000
Mais si nous redéfinissons E-cinetique le calcul tiendra compte de la nouvelle valeur de vitesse :
1:=> (define E-cinetique
(/
(* une-masse (* une-vitesse
une-vitesse))
2))
*** WARNING: ...
1:=> E-cinetique
900
La définition page lie une variable à une valeur spécifiée par une expression composée comportant elle-même des références à des variables définies au préalable. Pour réaliser cette liaison, l'interprète ira chercher dans l'environnement courant les valeurs liées à masse et à vitesse, effectuera les calculs décrits et liera E-cinetique au résultat.
La formulation page élargit les moyens d'expression à notre disposition en permettant d'effectuer des calculs sur des objets définis au préalable et désignés par des identificateurs. Mais la modification de vitesse par page et les avatars consécutifs de E-cinetique montrent que cette façon de procéder risque de n'être pas très pratique : les noms des objets du calcul sont fixés une fois pour toutes, la modification de la valeur qui leur est liée ne modifie pas la valeur associée à l'expression E-cinetique, en définitive le résultat du calcul dépend de l'ordre des définitions.
Il serait préférable de pouvoir définir une expression plus générale, comportant des paramètres, qui se comporterait comme une procédure primitive avec ses opérandes : ceci nous conduit à élargir les possibilités de define à la création de procédures. Pour nommer une expression paramétrable, qui sera une procédure personnelle du programmeur, Scheme propose la syntaxe suivante :
masse et vitesse sont les paramètres de la procédure E-cinetique.
Nous avons créé une procédure E-cinetique, à deux paramètres (on dit aussi paramètres formels) masse et vitesse. Ce faisant nous avons procédé à l'abstraction de masse et vitesse, c'est à dire que les paramètres formels masse et vitesse figurent dans la définition pour tenir la place des arguments (on dit aussi paramètres effectifs) que nous procurerons à la procédure lorsque nous l'invoquerons, par exemple ainsi :
Les éléments de la nouvelle syntaxe sont les suivants : la parenthèse qui suit le mot-clé define constitue une liste dont le premier élément est le nom de la nouvelle procédure et les éléments suivants les noms des paramètres. Cette liste peut être vue comme le prototype ou la spécification de la procédure, on entend par là qu'elle donne le modèle de la façon dont il faudra en rédiger l'invocation.
La seconde parenthèse marquée est une S-expression (ce pourrait être un atome) appelée corps de la procédure. La valeur du corps sera calculée lors de l'invocation de la procédure et retournée comme résultat.
L'expression est un appel à la procédure E-cinetique. On peut dire aussi une application de procédure, un appel fonctionnel...
Le calcul est effectué de la façon suivante. Soit une procédure définie ainsi (par une notation expliquée § 4.1 page ) :
(define (<nom de la procédure> p1 p2 ... pk)
<corps de la procédure>)
Son appel s'écrit ainsi :
(nom-procedure arg1 arg2 ... argk)
Le nom de la procédure permet de trouver dans l'environnement courant la procédure créée par la définition. La conformité de la liste d'arguments de l'appel à la liste de paramètres de la définition est vérifiée ; en cas de discordance une erreur est signalée et l'évaluation s'arrête. Chaque argument argi est évalué et sa valeur substituée dans le corps de la procédure à chaque occurrence du paramètre pi correspondant. Pour être plus schemien et plus précis, nous dirons que la valeur de chaque argument argi est liée, pour la durée de l'exécution de la procédure, au nom du paramètre pi correspondant.
Si les arguments sont pertinents et si la syntaxe de la procédure est correcte, le corps est évalué. Si l'appel avait lieu à l'invite de l'interprète, la valeur obtenue est affichée, suivie de l'invite. Si l'appel de la procédure était enfoui dans une expression ou une définition, par exemple comme argument d'un appel à une autre procédure, la valeur est retournée à l'appelant. Ainsi :
1:=> (define (carre x) (* x x))
1:=> (define (E-cinetique masse vitesse)
(/ (* masse (carre vitesse)) 2))
1:=> (E-cinetique 4 10)
200
Nous disposons maintenant d'outils variés pour construire des programmes, nous savons définir des objets, et mettre en uvre des procédures qui les manipulent et qui appellent d'autres procédures. Mais par rapport au programme fixé en 1.5 page il nous manque encore la capacité de choisir entre deux actions selon le résultat des calculs précédents, sans laquelle notre pouvoir d'expression reste très limité.
Le langage Scheme précise un peu plus la nature du choix que nous pouvons introduire dans nos capacités de programmation : il s'agira d'évaluer soit l'une soit l'autre de deux expressions selon le résultat de l'évaluation d'une expression préalable. Cette expression préalable pourra avoir l'une de deux valeurs, vrai ou faux. En Scheme, une expression dénote le faux si son évaluation retourne #f , et le vrai pour tout autre résultat.
Une procédure qui retourne un résultat booléen (vrai ou faux), se nomme un prédicat. Une expression dont l'évaluation résulte en vrai ou faux sera aussi désignée prédicat. En voici quelques exemples :
1:=> (define x 5)
1:=> (number? x)
#t
1:=> (procedure? +)
#t
1:=> (symbol? x)
#f
1:=> (= x (+ 3 2))
#t
1:=> (< x 5)
#f
1:=> (<= x 5)
#t
1:=> (string? "toto")
#t
1:=> (string=? "toto" "TOTO")
#f
La construction Scheme qui nous permettra de choisir entre deux traitements possibles selon la valeur fournie par un prédicat est la forme spéciale if , dont la syntaxe est la suivante :
if est une forme spéciale parce que l'évaluation des expressions qu'elle permet de construire suit un processus particulier. Il y a d'abord évaluation de l'expression <condition> (le prédicat de test). Si la valeur retournée est #f alors l'expression retourne la valeur de <expression-si-faux>, dans les autres cas elle retourne la valeur de <expression-si-vrai>.
Contrairement à ce qui se passe lors d'un appel de procédure, il n'y a pas évaluation préalable de tous les arguments, ce qui permet par exemple l'écriture de procédures telles que :
1:=> (define (division-sure a b) ;; 92 97
(if (= 0 b)
"impossible :
division par zéro"
(/ a b)))
1:=> (division-sure 7 2)
3.50000000000
1:=> (division-sure 7 0) 92 97
impossible : division par zéro
Si if était une procédure, l'évaluation de l'expression comporterait l'évaluation des sous-expressions obtenues en substituant dans le texte les valeurs 7 et 0 aux paramètres a en b, en l'occurrence dans la dernière ligne, ce qui provoquerait une erreur, alors qu'ici notre procédure a analysé les arguments et effectué un traitement particulier (assez sommaire dans les cas présent, mais que nous pourrions imaginer plus raffiné) pour un cas aberrant.
Les crochets qui encadrent <expression-si-faux> signifient que cette expression est facultative : si elle est absente et que le test retourne #f, if retourne un résultat indéfini :
1:=> (define (division-gardee a b)
(if (not (zero? b))
(/ a b)))
1:=> (division-gardee 7 3)
2.33333333333
1:=> (division-gardee 7 0)
#unspecified
Retourner un résultat noté « #unspecified » n'est pas une erreur, c'est une réponse légitime de Scheme. Ici notre procédure ne spécifie effectivement pas le résultat d'une division par zéro, la réponse de l'interprète est conforme à notre attente.
Nous avons déjà abondamment utilisé des symboles, mais ils représentaient toujours un autre objet, par exemple en servant de nom à une variable :
1:=> (define a 10)
1:=> (symbol? a)
#f
1:=> (number? a)
#t
1:=> a
10
a est bien un symbole, mais dans les expressions (symbol? a) et (number? a) le prédicat ne s'applique pas au symbole a mais à la valeur liée à la variable qu'il dénote (précisément ici au type de cette valeur).
Si nous voulons citer le symbole a en évitant son évaluation, Scheme nous fournit une forme spéciale dont la fonction consiste à simplement empêcher l'évaluation de son argument et à le retourner tel quel. Cette forme spéciale s'appelle quote :
1:=> (quote a)
A
1:=> (symbol? (quote a))
#t
Cette forme spéciale possède une abréviation équivalente :
1:=> 'a
A
Lorsque la forme 'a est soumise à Scheme, elle est transformée par le lecteur en (quote a).
Cette forme spéciale nous permet d'utiliser les symboles comme valeurs de variables :
1:=> (define ma-couleur 'vert)
1:=> ma-couleur
VERT
Attention, la valeur de la variable ma-couleur est de type symbole, ce qui diffère totalement du type chaîne de caractères :
1:=> (symbol? ma-couleur)
#t
1:=> (string? ma-couleur)
#f
De manière générale, quote nous sera indispensable dès lors que nous voudrons noter directement la valeur d'un objet non auto-évaluant. Nous n'avons pas encore étudié la nature des listes, mais nous pouvons déjà en noter :
: '(1 2 3)
(1 2 3)
: '(a b c)
(a b c)
Les programmes que nous avons vus jusqu'à présent communiquent avec nous de façon très limitée : ils affichent sur notre écran la valeur de la dernière expression qu'ils ont évaluée. Nous pourrions souhaiter plus de loquacité, par exemple voir affichées les valeurs de quelques paramètres ou résultats intermédiaires. En effet, avec ce système, seule la valeur d'une expression définie au Top-level44 est accessible.
En outre, lorsque nous utiliserons un compilateur et non plus un interprète, nous ne serons plus dans un contexte où les résultats s'affichent, il faudra le prévoir explicitement. Nous souhaiterons aussi peut-être conserver des résultats pour les utiliser plus tard, et pour cela les enregistrer dans une mémoire permanente, un fichier sur disque en l'occurrence. Ces fonctions sont l'affaire des procédures d'entrée-sortie.
Nous n'introduirons pour l'instant que les fonctions les plus simples d'affichage et d'entrée.
(display obj) affiche une représentation de la valeur d'obj :
Comme son nom l'indique, va à la ligne.
(read), sans argument, attend en réponse la représentation externe d'un objet Scheme (une S-expression) et la convertit en cet objet lui-même (mais ne l'évalue pas). C'est le lecteur de la boucle Read-Eval-Print.
1:=> (read)
1
1
Une expression comme (display obj) ne renvoie pas à proprement parler de valeur, elle produit un effet, afficher des caractères sur l'écran. Il convient de distinguer entre :
et :
voire :
La ligne dénote l'évaluation d'une expression. Le contexte particulier de cette évaluation fait qu'elle se manifeste par l'affichage du résultat, mais dans un autre contexte, par exemple insérée dans une expression englobante plus complexe, cette valeur serait utilisable pour des calculs ultérieurs.
À l'inverse, les lignes et ne commandent qu'une action, en l'occurrence un affichage, et ne fournissent pas de valeur utilisable. De telles actions sont nommés des effets de bord, parce qu'elles se manifestent par autre chose que le retour de la valeur de l'expression qui les désigne.
L'affichage du résultat par Bigloo traduit d'ailleurs bien cette situation : la ligne « 3#unspecified41 » comporte bien 3, qui est l'effet produit par display41 , mais comme nous sommes au Top-level44 elle comporte aussi la valeur renvoyée par display, qui est bien sûr #unspecified. À la ligne nous avions utilisé Gambit, un Scheme qui adopte un comportement différent, de nature à nous induire en erreur (croyons-nous).
Depuis que nous savons afficher autre chose que des résultats d'évaluation, nous pouvons souhaiter modifier une procédure telle que celle que nous avions définie en page :
(define (division-sure a b)
(if (= 0 b)
"impossible : division par zéro"
(/ a b)))
afin par exemple de lui faire afficher les opérandes dans le cas où la division est possible (c'est simpliste, juste un exemple). Mais if n'admet qu'une expression par branche de l'alternative :
(if <condition>
<expression-si-vrai>
[<expression-si-faux>])
Pour résoudre ce problème Scheme dispose de la forme spéciale begin41 , qui permet de combiner en une entité syntaxique unique une séquence d'expressions à évaluer successivement et de renvoyer comme résultat la valeur de la dernière :
(define (division-sure a b)
(if (= 0 b)
"impossible :
division par zéro"
(begin
(display
"valeur des opérandes")
(newline)
(display
a) 92 97
(display
#\
space) 92 97
(display
b) 92 97
(newline)
(/ a
b))))
1:=> (division-sure 7 3)
valeur des opérandes
7 3
2.33333333333
On pourrait croire que les display41 des lignes , et sont superflus, puisque lorsque je soumets à l'interprète des noms de variables préalablement définies il m'en affiche obligeamment la valeur :
: (define i 8)
i
: i
8
Mais là il n'en est rien :
(define (nouvelle-division-sure a b)
(if (= 0 b)
"impossible :
division par zéro"
(begin
(display
"valeur des opérandes")
(newline)
a #\
space b
(newline)
(/
a b))))
: (nouvelle-division-sure 7 3)
valeur des opérandes
2.33333333333
en effet les expressions a, #\space
et b ne sont pas
mentionnées au Top-level et l'interprète ne retourne ni
n'affiche leur valeur en revenant au Top-level. display est bien
indispensable à notre propos.
Une procédure peut utiliser des variables auxiliaires qui n'ont d'intérêt que pendant le temps du calcul. Elle peut aussi avoir besoin de résultats intermédiaires utilisés à plusieurs reprises, et qu'il serait agréable de ne calculer qu'une fois. Pour ce type de besoin il existe la forme spéciale let2.7, qui permet de définir des objets locaux qui seront créés au début de l'exécution de la procédure et détruits à sa terminaison.
1:=> (define (surface-cercle rayon)
(let ((pi 3.14159))
(* pi (carre rayon))))
Nous obtenons ainsi une définition plus robuste de la procédure surface-cercle : elle incorpore sa propre définition de p, qu'elle ne propage pas dans l'environnement global, ce qui pourrait avoir des effets indésirables pour d'autres procédures.
La forme générale de let est :
(let (<liaisons>) <corps>)
où <liaisons> est de la forme :
(p1 exp1)
...
(pk expk)
ce qui signifie que, pendant l'évaluation du corps, la variable p1 est
liée à la valeur de l'expression exp1, la variable p2
à la valeur de l'expression exp2, ainsi de suite.
Attention, aucune hypothèse n'est faite sur l'ordre de construction des liaisons, c'est à dire que pendant l'évaluation d'un expj la valeur d'aucun pi, n'est accessible, même si i < j.
L'étape de construction des liaisons d'un let peut être envisagée comme un changement de décor entre deux actes au théâtre : pendant l'entracte le rideau est baissé et on ne voit rien, ce que font les machinistes n'est pas accessible au spectateur.
Pour utiliser en construisant une liaison la valeur d'une des liaisons précédentes de la même forme il faut utiliser la forme voisine let* qui construit les liaisons dans l'ordre et permet pour l'évaluation de tout expj de faire appel aux pi tels que i < j. Par exemple :
(define (volume rayon hauteur) (let* ((pi 3.14159) (carre-r (* rayon rayon)) (surface-base (* pi carre-r))) (* hauteur surface-base)))
74
Les formes let et let* 41 permettent d'établir des liaisons entre des variables et des valeurs, ce sont des lieurs. Nous reviendrons de façon plus approfondie sur leur signification en 7.1.3 page , en attendant voici une autre illustration (programme 2.10).
(define (afficher-2-choses u v) (display u) (display " ") (display v) (newline)) (define (equation-degre-2 a b c) (let ((discriminant (- (carre b) (* 4 a c)))) (if (negative? discriminant) "Désolé, pas de racines réelles. " (if (zero? discriminant) (/ (- b) (* 2 a)) (let ((racine-1 (/ (- (sqrt discriminant) b) (* 2 a))) (racine-2 (/ (- (+ (sqrt discriminant) b)) (* 2 a)))) (afficher-2-choses racine-1 racine-2))))))
74
Dans cet exemple, let est utilisé pour donner des noms évocateurs à des valeurs d'expressions. La même procédure, écrite sans let en remplaçant les symboles par les expressions, serait parfaitement équivalente mais à peu près illisible. Nommer les choses fait plus qu'améliorer la lecture, cela éclaire l'intention du programmeur.
Les adeptes des langages à structure de blocs héritiers d'Algol remarqueront que let permet une écriture analogue à celle des blocs en question. Les variables du let sont les variables locales du bloc, oubliées dès que l'on sort de celui-ci.
Munis d'opérations d'entrée-sortie et de quelques moyens d'assembler des programmes un peu complexes nous pouvons construire des programmes interactifs qui permettent de résoudre une série de problèmes analogues sans qu'on ait à les relancer. Pour cela nous allons anticiper sur la suite de ce cours en introduisant une méthode de programmation dont le mécanisme ne sera dévoilé qu'au chapitre 5.
L'intérêt des ordinateurs repose en partie sur leur capacité à répéter rapidement un grand nombre de fois la même opération. Supposons qu'il nous faille calculer les racines carrées de beaucoup de nombres, il serait agréable que le calcul se relance tout seul :
Un calcul de racine carrée interactif
(define (racine) (print "Entrez le nombre dont vous voulez calculer") (display "la racine carrée (ou 0 pour finir) : ") (let ((n (read))) (if (zero? n) ; (display "Au revoir") (begin (print "La racine carrée de " n " est " (sqrt n)) (racine))))) ;
74
Remarquons le mécanisme de répétition du calcul : la procédure racine s'appelle elle-même quand elle a fini, en . Ce procédé s'appelle la récursion ; il semble simple, et il l'est en fait, à quelques problèmes près. Ainsi, notre procédure ne reçoit pas d'arguments et nous ne nous intéressons pas à la valeur qu'elle renvoie : le chapitre 5 abordera ces sujets.
Notons aussi que la procédure peut se terminer : l'alternative située en comporte une branche qui n'appelle pas racine.
print est une procédure de Bigloo, non-standard2.8 mais souvent plus pratique que display :
(print obj1 obj2 ... ) affiche une représentation des valeurs d'obj1, obj2, etc. et ajoute un newline à la fin.
Nous disposons désormais d'un outillage relativement riche pour construire des programmes. Un programme Scheme sera une suite de définitions et d'expressions. Retenons la différence entre les deux, une définition n'est pas une expression.
Les expressions sont des constructions susceptibles d'être évaluées (de retourner une valeur), ce sont soit des expressions auto-évaluantes, soit des symboles, soit des listes. Dans ce dernier cas, ce sont soit des appels de procédure, soit des formes spéciales.