debut.gif (172 octets) boutprec.gif (153 octets) boutsuiv.gif (154 octets)

3. Créer, manipuler des programmes

Jusqu'à présent nous savons taper des expressions Scheme à l'invite de l'interprète et regarder apparaître le résultat de leur évaluation. Certes, pour éviter de tout retaper à chaque nouvelle exécution, nous avons sans doute saisi le texte de nos procédures au moyen d'un éditeur de texte, puis, d'un coup de souris, nous l'avons sélectionné dans la fenêtre de l'éditeur pour le « copier » puis le « coller » dans la fenêtre de l'interprète. Il n'échappera pas au lecteur (toujours perspicace) qu'avec de telles méthodes nous ne saurions développer des programmes de grande taille. Ce chapitre introduit quelques moyens propres à faciliter la tâche du programmeur.

 

3.1 Chargement de programmes depuis un fichier

 

3.1.1 La procédure load

Si un fichier contient un texte Scheme valide, la procédure load41 permet de le charger directement dans l'espace de travail de l'interprète (ou du système de programmation) et de l'évaluer :

 

1:=> (load <nom de fichier>)

< nom de fichier> est une chaîne de caractères qui désigne un fichier existant contenant du texte Scheme. La procédure load lit les définitions et les expressions contenues dans le fichier et les évalue en séquence. Voici un exemple d'interaction.

 

3.1.2 Exemple d'usage de load

J'ai écrit (ou on m'a donné) un ensemble de procédures Scheme qui lisent des fichiers de séquences de protéines au format Swiss-Prot. Ces procédures sont sur mon Macintosh dans le fichier lire-swissprot.scm. Au niveau de la boucle d'interrogation je définis une variable de type chaîne de caractères mon-programme dont la valeur est le nom complet du fichier :

 

: (define mon-programme "mon HD:Cours Pasteur:Algo:lire-swissprot.scm")

[ On note au passage la forme du nom complet d'un fichier sur Macintosh : la suite formée dans l'ordre hiérarchique par le nom du disque, les noms des dossiers dans l'ordre du plus englobant au dernier englobé et le nom du fichier proprement dit, séparés par des : ]

Je charge les procédures à partir du fichier en question :

 

: (load mon-programme)

Je définis une variable de type chaîne de caractères ma-banque dont la valeur est le nom complet du fichier qui contient mes séquences :

 

: (define ma-banque "mon HD:Cours Pasteur:Algo:swissprot.extrait")

Évidemment si les fichiers sont dans le dossier courant leurs noms complets ne sont pas nécessaires, le nom court suffit.

La procédure principale du programme s'appelle choisir-le-fichier et elle attend en argument une chaîne de caractères contenant le nom du fichier de séquences :

 

: (choisir-le-fichier ma-banque)

ID 143Z_XENLA STANDARD; PRT; 247 AA.
AC Q91896;
DT 01-FEB-1997 (REL. 35, CREATED)
DT 01-FEB-1997 (REL. 35, LAST SEQUENCE UPDATE)
DT 01-FEB-1997 (REL. 35, LAST ANNOTATION UPDATE)
DE 14-3-3 PROTEIN ZETA.
OS XENOPUS LAEVIS (AFRICAN CLAWED FROG).
OC EUKARYOTA; METAZOA; CHORDATA; VERTEBRATA; TETRAPODA; AMPHIBIA; ANURA.
RN [1]
...

Il se passe la même chose que si j'avais tapé au niveau de la boucle d'interaction la cinquantaine de lignes du programme, en recommençant à chaque faute de frappe ou erreur de programmation.

Cette façon de procéder simplifie la vie, évite notamment que des erreurs de manipulation aboutissent trop facilement à des erreurs de programmation.

Bien sûr, une procédure chargée par load peut elle-même invoquer load pour charger les procédures, les expressions ou les définitions utiles au déroulement du programme, ce qui autorise la construction de programmes à l'organisation assez complexe.

 

3.1.3 Les limites des avantages de load

L'utilisation de load comporte néanmoins des limites qui la cantonnent à de petits programmes :

 

D'autres moyens vont nous permettre de franchir ces limites.

 

3.2 Compilation de programmes Scheme

 

3.2.1 Position de la question

Nous avons dit qu'un interprète était un programme qui simulait une machine virtuelle dont le langage machine serait le langage de programmation utilisé. Un compilateur est, à l'inverse, un programme qui « lit » un texte rédigé dans le langage de programmation utilisé et le traduit dans le langage machine de l'ordinateur 3.1.

Alors que l'interprète nous propose de lui soumettre de façon interactive des expressions Scheme dont il nous rendra la valeur, comme un oracle, l'idée de la compilation consiste à prendre le texte Scheme d'un programme complet pour le traduire, en une fois, en langage machine directement exécutable, c'est à dire que le résultat de la compilation est un fichier exécutable que l'on pourra « lancer » comme une commande Unix ou une application Macintosh.

Lorsque l'on aura dit que la vitesse d'exécution d'un programme compilé est supérieure de un à trois ordres de grandeur à celle du même programme interprété, on imagine le partage du travail entre les deux méthodes : à l'interprète la mise au point des procédures une par une, au compilateur l'intégration de plusieurs procédures pour former un plus gros programme et la réalisation de la version provisoirement définitive.

En d'autres termes, dès que l'on veut faire de vrais programmes qui traitent de vraies données, se pose la question de disposer d'un langage compilé.

Le système de programmation que nous utilisons sous Unix, Bigloo, est à la fois un interprète et un compilateur. Il en va de même de MacGambit sur Macintosh et de Gambit-C sous MS-Windows*.

 


3.2.2 Présentation du compilateur Bigloo

Un compilateur est un programme capable de lire dans un fichier (dit fichier source) le texte (dit texte source) d'un programme (dit programme source) rédigé dans un langage de programmation et de le traduire dans le langage-machine de l'ordinateur sur lequel on espère l'exécuter, dit système-cible.

La compilation va traduire le programme, pas l'exécuter. Les expressions Scheme (ou de tout autre langage source) vont subir une transformation en langage machine, mais elles ne seront pas évaluées. C'est lors d'un processus ultérieur nommé exécution que le programme nous délivrera ses résultats. Mais nous pourrons répéter cette exécution autant de fois qu'il nous plaira, avec des données éventuellement différentes, sans avoir besoin de recompiler le programme.

Le fichier qui contient le résultat de la traduction s'appelle un programme exécutable, ou un fichier exécutable, ou simplement un programme. Si tout s'est passé conformément à nos v\oeux, l'invocation du nom de ce programme (sous certaines conditions de permission, de position dans l'arborescence des fichiers et d'existence des données) suffit à en déclencher l'exécution.

 

 

Figure 3.1: Le processus de compilation
\includegraphics[scale=0.8]{../Images/Compile/machine-compile.epsi}67

 

 

3.2.3 Compiler un programme avec Bigloo

 


3.2.3.1 Commandes pour compiler et exécuter

Pour une description exhaustive des commandes et de la syntaxe, on se reportera au manuel [Ser98]. C'est d'ailleurs une règle générale : l'utilisation d'un logiciel, et surtout d'un système de programmation, ne se conçoit pas sans le manuel à portée de main. Le présent alinéa vise, au contraire, à ne donner de Bigloo que la vision minimum indispensable pour débuter. Un chapitre ultérieur consacré à la compilation séparée poursuivra l'exploration des possibilités de ce compilateur.

Bigloo est à la fois un compilateur et un interprète. Lorsqu'à l'invite (notée #) du shell Unix on tape juste bigloo, Bigloo est lancé en mode interprète. Si on tape :

 

# bigloo <nom de fichier source>

Bigloo va compiler le fichier < nom de fichier source> et le fichier exécutable résultant sera placé dans le répertoire à partir duquel aura été lancée la commande sous le nom a.out. Pour l'exécuter il nous suffira de taper3.2  :

 

# ./a.out

< nom de fichier source> doit se terminer par « .scm » ou « .bgl ». Si le nom de fichier  a.out ne nous plaît pas, nous devons taper :

# bigloo -o <nom d'exécutable> <nom de fichier source>

Ainsi :

 

# bigloo -o mon-exec mon-source.scm

Pour exécuter le programme :

 

# ./mon-exec

 

3.2.3.2 Syntaxes particulières à Bigloo

Afin d'expliquer comment procéder nous allons prendre un programme que nous avons déjà réalisé et le transformer pour l'adapter à la compilation par Bigloo. Prenons par exemple le programme suivant, qui n'est autre que le programme 2.10 écrit au chapitre 2 § 2.10.


(define (equation-degre-2 a b c)
  (let ((discriminant (- (carre b) (* 4 a c))))
    (if (negative? discriminant)
        (print "Désolé, pas de racines réelles. ") ; \label{lisp:eq1}
        (if (zero? discriminant)
            (print (/ (- b) (* 2 a)))              ; \label{lisp:eq2}
            (let ((racine-1
                   (/ (- (sqrt discriminant) b)
                      (* 2 a)))
                  (racine-2
                   (/ (- (+ (sqrt discriminant) b))
                      (* 2 a))))
               (print racine-1 racine-2))))))      ; \label{lisp:eq3}

Remarquons aux lignes [*], [*] et [*] qu'il a fallu remplacer la mention d'une expression à renvoyer comme valeur, qui sous l'interprète était imprimée implicitement, par des impressions explicites, indispensables dans un contexte d'exécution différée d'un programme compilé.

Nous utilisons la procédure non-standard print de Bigloo.

Pour qu'un programme soit compilable par Bigloo il doit être composé de modules, en tout cas au moins un module. Pour un programme constitué d'un seul module, ceci se fait en plaçant en tête du texte du programme une forme de déclaration de module, ainsi :

 

(module nom clause1 clause2 ...)

Nous allons compléter le fichier source qui contient déjà notre procédure de résolution d'équations et, sans doute, notre procédure carre. Il faut en tête du fichier (supposons qu'il s'appelle equateur.scm) une déclaration de module :

 

(module equations (main entrer-coef))

module est un mot-clé, ainsi que main.

equations sera le nom du module.

entrer-coef est le nom d'une procédure, qui sera appelée au début de l'exécution du programme, qu'il nous faut maintenant écrire et qui devra accepter un argument unique, une liste qui contiendra sous forme de chaînes de caractères les arguments passés au programme depuis le shell Unix.

Même si nous ne voulons pas que notre programme exécutable accepte des arguments sur la ligne de commandes, il en recevra au moins un (son propre nom) et nous devons donc écrire ce qu'il faut pour le recevoir, soit :


(define (entrer-coef args)
  (display "Entrez les coefficients a, b et c de l'équation : ")
  (let* ((a (read))
         (b (read))
         (c (read)))
    (equation-degre-2 a b c)))

L'argument args est inutilisé mais obligatoire (il nous sera utile en d'autres circonstances).

Si nous voulons que notre programme résolve des équations à répétition, et qu'il s'arrête par convention si nous lui donnons un coefficient a nul :


(define (entrer-coef args)
  (display "Entrez les coefficients a, b et c de l'équation : ")
  (let* ((a (read))
         (b (read))
         (c (read)))
    (if (zero? a)
        (print "Bonsoir.")
        (begin
          (equation-degre-2 a b c)
          (entrer-coef '(1))))))

entrer-coef attend un argument, comme nous n'en faisons rien peu importe lequel, mais il en faut un et que ce soit une liste. Lors du premier appel, le shell Unix fournit toujours une liste d'au moins un argument, le nom du fichier exécutable.

Ajoutons :

 

(define (carre x) (* x x))

et il n'y a plus qu'à choisir un nom (equateur fera l'affaire), compiler et exécuter :

 

# bigloo -o equateur equateur.scm

# ./equateur

C'est la déclaration de module qui permet de savoir que l'exécution du programme equateur doit commencer par la procédure entrer-coef, appelée point d'entrée du programme. La clause main définit le point d'entrée d'un programme. Le programme complet compilable devient donc :

Le fichier source compilable equateur.scm

(module equations (main entrer-coef))

(define (entrer-coef args)
  (display "Entrez les coefficients a, b et c de l'équation : ")
  (let* ((a (read))
         (b (read))
         (c (read)))
    (if (zero? a)
        (print "Bonsoir.")
        (begin
          (equation-degre-2 a b c)
          (entrer-coef '(1))))))

(define (carre x) (* x x))

(define (equation-degre-2 a b c)
  (let ((discriminant (- (carre b) (* 4 a c))))
    (if (negative? discriminant)
        (print "Désolé, pas de racines réelles. ") ; \label{lisp:eq1}
        (if (zero? discriminant)
            (print (/ (- b) (* 2 a)))              ; \label{lisp:eq2}
            (let ((racine-1
                   (/ (- (sqrt discriminant) b)
                      (* 2 a)))
                  (racine-2
                   (/ (- (+ (sqrt discriminant) b))
                      (* 2 a))))
               (print racine-1 racine-2))))))      ; \label{lisp:eq3}

74

debut.gif (172 octets) boutprec.gif (153 octets) boutsuiv.gif (154 octets)