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

9. Quelques pas vers le monde réel

 

9.1 Entrées-sorties : notions générales

Les lignes qui suivent élargissent le propos d'un paragraphe antérieur 2.8 page [*] qui avait déjà introduit quelques moyens offerts à l'homme pour communiquer avec un programme.

Les programmes sont généralement destinés à traiter des données issues du monde extérieur, auquel ils sont censés restituer des résultats, et pour cela ils effectuent des entrées-sorties, c'est à dire qu'ils commandent l'acheminement de données convenablement représentées le long de filières ou de canaux qui communiquent avec des dispositifs physiques adéquats.

Pour le système d'exploitation Unix comme pour la norme Scheme [CKR98], les données externes sont supposées être sous forme de caractères.

L'abstraction qui désigne en Scheme le transit de données le long de ces filières ou canaux de communication se nomme « flux9.1 » (port en anglais). Un flux d'entrée est un objet Scheme qui peut délivrer des caractères lorsqu'on le lui demande, et un flux de sortie est un objet Scheme auquel on peut envoyer des caractères et qui les acceptera. Un tel objet répond positivement au prédicat de type port?.

Le « monde extérieur » peut globalement se présenter sous deux espèces : constitué d'humains occupés à scruter des écrans et à manipuler claviers et souris, ou d'appareils informatiques capables de garder l'enregistrement des caractères qu'on leur aura envoyés et de le restituer.

Pour commander ces opérations d'entrée-sortie le langage de programmation doit permettre au programmeur de définir des objets de type adéquat : en Scheme ce sont les flux qui subiront les manipulations symboliques destinées à déclencher les entrées-sorties matérielles. Il existe aussi une notion pour désigner un ensemble de données enregistrées sur un support extérieur (tel que disque magnétique) : c'est le « fichier ».

Avant de faire des entrées-sorties il faut décrire flux (objet propre au programme) et fichier (objet externe, nommé par une chaîne de caractères) et les mettre en correspondance, c'est la définition et l'« ouverture » du fichier.

 

9.2 Traitement de fichier : préliminaires

Le lecteur se reportera à [CKR98] pour l'inventaire complet et la syntaxe détaillée des procédures dont nous allons illustrer le fonctionnement à partir d'un exemple.

 

9.2.1 La banque de séquences de protéines Swiss-Prot

Nous nous proposons de lire des séquences de protéines dans la banque Swiss-Prot dont on lira la description détaillée dans [Bai96]. Plus précisément nous travaillerons sur un extrait de la banque pour faire nos essais. Il nous suffit pour l'instant de savoir ce qui suit :

 

 

9.2.2 Ouverture de fichier

Du point de vue du programmeur, l'ouverture d'un fichier consiste à mettre en correspondance le flux d'entrée-sortie qu'il a défini dans son programme pour y acheminer des données et un fichier physique désigné généralement par une chaîne de caractères, le nom du fichier.

Une fois cette correspondance établie, des actions physiques pourront être perpétrées sur le fichier.

Scheme fournit plusieurs procédures pour ouvrir des fichiers, celles dont nous préconisons l'usage, pour des raisons dont l'exposé suit, sont les deux suivantes :

 

(call-with-input-file string proc)

(call-with-output-file string proc)

proc doit être une procédure qui accepte un argument et string une chaîne de caractères correspondant à un nom de fichier.

Pour call-with-input-file le fichier doit déjà exister et il est ouvert pour des opérations de lecture ; dans le cas de call-with-output-file le résultat est indéterminé si le fichier existe déjà, sinon il est ouvert pour des opérations d'écriture.

Ces procédures appellent proc avec un argument, le flux obtenu par l'ouverture du fichier désigné par string. La procédure appelée pourra donc effectuer des opérations d'entrée-sortie sur ce flux.

Si le fichier ne peut pas être ouvert une erreur est signalée.

Si la procédure retourne alors le flux est fermé automatiquement.

Nous pouvons écrire le début de notre programme de lecture de la banque :


(define (choisir-le-fichier un-fichier)
   (call-with-input-file un-fichier lire-des-sequences))

La procédure choisir-le-fichier reçoit en argument le nom du fichier contenant la banque, ce nom est fourni par l'utilisateur. Ce fichier est ouvert par call-with-input-file, la procédure lire-des-sequences est appelée et reçoit en argument un flux d'entrée « branché » sur la banque.

 

9.2.3 Assurer l'intégrité des fichiers

Nous avons dit qu'un fichier ouvert était susceptible de subir des actions physiques, c'est à dire qu'il était d'une certaine façon vulnérable, ouvert aux actions voulues par le programmeur mais aussi aux erreurs et aux incidents extérieurs.

Il est de ce fait important que le programme, après avoir exécuté les opérations voulues sur le fichier, abolisse la correspondance établie par l'ouverture entre flux et fichier : cette abolition est nommée la fermeture9.2 du fichier, elle évite que des incidents n'altèrent de façon indésirée le contenu du fichier.

Il est donc hautement souhaitable que, même si notre programme suit un cheminement indésiré, l'opération de fermeture ait lieu.

C'est parce que les procédures call-with-input-file et call-with-output-file garantissent dans une certaine mesure la fermeture ultime du fichier que nous en conseillons l'usage.

 


9.3 Lire un fichier séquentiel

Notre programme sera organisé de la façon suivante : une procédure lire-des-sequences commandera la consultation du fichier de séquence en séquence ; pour ce faire elle appellera une procédure lire-une-sequence qui lira le fichier ligne par ligne en appelant une procédure lire-une-ligne qui exécutera l'action élémentaire de lire les caractères du fichier un par un.

Lorsqu'on lit un fichier séquentiel, la première précaution à prendre pour ne pas avoir un programme faux est de s'assurer que l'on n'est pas arrivé à la fin du fichier, parce qu'une tentative de lecture au-delà de la fin de fichier déclencherait une erreur. C'est la procédure de plus bas niveau, lire-une-ligne, qui détectera la fin du fichier (nous verrons comment un peu plus bas). Dans ce cas elle renverra, par convention, la chaîne de caractères *EOF* à la procédure appelante lire-une-sequence, qui a son tour la renverra à lire-des-sequences, qui en rendant la main à call-with-input-file lui permettra de fermer le fichier.

Voici le détail des opérations :

 


9.3.1 Lire des séquences

Voici la procédure appelée ci-dessus par call-with-input-file :

Lire des séquences

(define (lire-des-sequences flux)
   (let boucle ((sequence (lire-une-sequence flux))) ; \label{lisp:ldseq}
      (if (string=? (car sequence) "*EOF*")
          #f
          (begin
             (imprime sequence)
             (boucle (lire-une-sequence flux))))))

74

Lire des séquences, c'est répéter un certain nombre de fois l'opération de lire une séquence, ce que nous allons faire.

C'est un processus essentiellement itératif, nous allons donc utiliser le style de programmation exposé en 5.3.3 page [*]. Pour cela nous utilisons en [*] un let nomé boucle qui se rappelle récursivement jusqu'à ce qu'elle tombe sur la fin du fichier. À chacune de ses applications elle lit une séquence.

On note que ce que renvoie la procédure est peu intéressant, ce qui est intéressant dans les entrées-sorties, ce sont les effets de bord, en l'occurrence les choses lues et écrites. À chaque séquence lue est appliqué un traitement, ici particulièrement sommaire : l'imprimer, mais on pourrait imaginer des tas de choses.

 

9.3.2 Lire des lignes

Lire une séquence, puisqu'elle est constituée de lignes, c'est répéter l'opération de lire une ligne jusqu'à la rencontre d'une ligne de type « fin de séquence ». La procédure lire-une-sequence va ressembler à lire-des-sequences :

Lire une séquence

(define (lire-une-sequence flux)
   (let boucle ((L '())
                (ligne (lire-une-ligne flux)))
      (if (string=? ligne "*EOF*")
          (list ligne)
          (let ((type-ligne (substring ligne 0 2)))
             (if (string=? type-ligne "//")
                 (reverse (cons ligne L))            ; \label{lisp:luseq}
                 (boucle (cons ligne L)
                         (lire-une-ligne flux)))))))

74

Chaque ligne est une chaîne de caractères retournée par lire-une-ligne. Nous construisons itérativement une liste de lignes en ajoutant la ligne juste lue en tête de la liste L (d'où le reverse en ligne [*] avant de renvoyer le résultat de l'évaluation).

cons crée une nouvelle liste à chacun de ses appels, ce qui n'est guère efficace et devrait être amélioré dans un programme en exploitation réelle. Ceci dit une séquence de protéine comportera au maximum quelques centaines de lignes, ce qui ne risque pas de faire échouer notre programme (avec une banque nucléique ce serait peut-être différent).

 

9.3.3 Test de fin de fichier

Le prédicat eof-object? permet de tester la fin de fichier. La procédure peek-char renvoie le prochain caractère du flux d'entrée mais sans le retirer du flux (c'est à dire que la lecture suivante retournera le même caractère), et s'il n'y a plus de caractère dans le flux elle renvoie une fin de fichier. Donc en ligne [*] du programme 9.3.3.1 l'expression (eof-object? (peek-char flux)) renvoie #t si la fin du fichier est atteinte, soit que la précédente lecture en ait lu le dernier caractère, soit qu'il ne comporte aucun caractère.

Si la fin du fichier est atteinte, nous renvoyons *EOF* et rendons la main à la procédure appelante, sinon nous allons pouvoir en lire la suite.

 

9.3.3.1 Lire des caractères

En descendant l'échelle des détails, nous arrivons aux éléments les plus atomiques : il faut lire chaque ligne caractère par caractère.

Lire une ligne

(define (lire-une-ligne flux)
   (if (eof-object? (peek-char flux))
       "*EOF*"
       (let boucle ((ligne "")
                    (c (read-char flux)))   ; \label{lisp:lula}
          (if (char=? c #\newline)
              ligne
              (boucle (string-append ligne (string c))
                      (read-char flux))))))  ; \label{lisp:lulb}

74

La procédure read-char est cousine de peek-char, mais elle « retire » (logiquement) du flux d'entrée le caractère qu'elle vient de lire de sorte qu'à son prochain appel nous lirons le caractère suivant.

On peut se représenter le fichier comme une file de caractères, lors de son ouverture un curseur est placé sur le premier caractère, chaque invocation de read-char renvoie le caractère placé sous le curseur et déplace le curseur jusqu'au caractère suivant, chaque invocation de peek-char renvoie le caractère placé sous le curseur sans déplacer le curseur.

Le caractère #\newline signale le début d'une nouvelle ligne, dont la procédure lire-une-ligne commence la construction par un caractère nul "". Puis à chaque appel récursif (ligne [*]) le caractère juste lu vient se placer et sera suivi de ceux amenés par le prochain appel.

Il faut bien dire que cette élégante procédure récursive n'est guère efficace, elle sera avantageusement remplacée en exploitation, par exemple par le procédure Bigloo non-standard9.3 mais rapide read-line.

 

9.3.3.2 Écrire le résultat

Imprimer une séquence

(define (imprime seq)
   (if (null? seq)
       (print "séquence vide")
       (if (null? (cdr seq))
           (print (car seq))
           (begin
              (print (car seq))
              (imprime (cdr seq))))))

74

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