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.
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.
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 :
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.
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.
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 :
Voici la procédure appelée ci-dessus par call-with-input-file
:
(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.
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
:
(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).
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.
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.
(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
.
(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