Article publié dans Linux Magazine 72, mai 2005.
Copyright © 2005 - Philippe Bruhat et Jean Forget
goto
goto
Depuis vos premiers cours de programmation (en C ou en Pascal), on vous
serine de ne pas utiliser goto
. Mais connaissez-vous la raison de cet
interdit dogmatique ? Cet article vous expliquera d'où vient le
goto
, pourquoi personne ne l'aime et comment l'utiliser. En Perl.
goto
On trouve une commande goto
dans presque tous les langages de
programmation (FORTRAN, Algol, COBOL, SNOBOL, BASIC, C, C++, Perl...).
Le principe de ces commandes est de faire sauter l'exécution du programme
du point où se trouve le goto
au point indiqué en paramètre. En BASIC,
ce sera un numéro de ligne, mais dans la plupart des langages (qui n'ont
pas de lignes numérotées) le pointeur de programme saute à un endroit
indiqué par une étiquette (label en anglais).
Le saut dans un flot d'instructions est une instruction tellement importante qu'elle existait déjà à l'âge de la programmation en langage machine. On la trouve encore dans tous les micro-processeurs. En assembleur, le mnémonique correspondant est généralement JMP (pour jump) ou BRA (pour branch).
GO TO
de FORTRANEn FORTRAN, il existe plusieurs formes de GO TO
: le GO TO
classique (unconditional GO TO), le GO TO
calculé (computed GO
TO) et le GO TO
assigné (assigned GO TO).
Note : Les explications que je donne dans les paragraphes suivants sont valables pour FORTRAN 77. Les programmes d'exemple ont été testés avec f77, le compilateur FORTRAN 77 du projet GNU.
GO TO
label
Comme prévu, l'instruction GO TO
transfère le contrôle à l'instruction
portant l'étiquette label. Pour mémoire, les étiquettes sont des
nombres (situés dans les colonnes 1 à 5), qui doivent être uniques dans
le programme.
En guise d'exemple, un petit programme qui ajoute au total en cours
un entier passé sur l'entrée standard. Le programme s'arrête quand on lui
passe 0
(zéro).
TOTAL=0 10 READ *, I IF (I.NE.0) THEN TOTAL=TOTAL+I PRINT *, TOTAL GO TO 10 END IF STOP END
À l'exécution, on obtient :
$ f77 goto01.f ; ./a.out 1 1. 2 3. 3 6. 4 10. 5 15. 0
Anecdote amusante : en FORTRAN, les étiquettes n'étant pas des numéros de ligne, leur ordre n'a aucune importance.
GO TO (
liste )
[,
] expression
Le GO TO
calculé permet de transférer le contrôle à l'une des étiquettes
d'une liste en fonction de la valeur de l'expression. Si l'expression
vaut 1
le contrôle est transféré à la première étiquette de la liste,
à la seconde si elle vaut 2
, etc.
Si l'expression ne correspond à la position d'aucune étiquette, le programme
continuera à l'instruction qui suit le GO TO
.
! Exemple de computed GO TO 10 PRINT *, 'Entrez un entier entre 1 et 8:' READ *, i GO TO (100,200,300,400,500,600,700,800) i PRINT *, 'Ce nombre ne correspond pas' GO TO 10 ! redemande un nombre, ad lib. ! Les règles du Perl Club viennent de ! http://www.dave.org.uk/perlclub.html 100 PRINT *, 'The First Rule of Perl Club' PRINT *, 'You do not talk about Perl Club' GOTO 1000 200 PRINT *, 'The Second Rule of Perl Club' PRINT *, 'You do not talk about Perl Club' GOTO 1000 300 PRINT *, 'Third Rule of Perl Club' PRINT *, 'A laptop crashes, breaks, runs down. '// &'The hack is over' GOTO 1000 400 PRINT *, 'Fourth Rule of Perl Club' PRINT *, 'Only two programmers to a pair' GOTO 1000 500 PRINT *, 'Fifth Rule of Perl Club' PRINT *, 'One bug at a time' GOTO 1000 600 PRINT *, 'Sixth Rule of Perl Club' PRINT *, 'No Java, no VB' GOTO 1000 700 PRINT *, 'Seventh Rule of Perl Club' PRINT *, 'Hacks will go on as long as they have to' GOTO 1000 800 PRINT *, 'Eighth, and Final Rule of Perl Club' PRINT *, 'If this is your first night at Perl Club, '// &'you have to hack' GOTO 1000 1000 STOP END
L'exécution du programme donne :
$ f77 goto02.pod ; ./a.out Entrez un entier entre 1 et 8: 0 Ce nombre ne correspond pas Entrez un entier entre 1 et 8: 9 Ce nombre ne correspond pas Entrez un entier entre 1 et 8: 6 Sixth Rule of Perl Club No Java, no VB
En FORTRAN 95, le computed GO TO statement est obsolète.
GO TO
variable
Le GO TO
assigné transfère le contrôle à l'étiquette contenue dans
la variable entière qui suit. L'étiquette ne peut être affectée à la
variable en question qu'avec l'instruction ASSIGN
(d'où le nom de
ce GO TO
).
ASSIGN 20 TO I ... GO TO I ... 20 PRINT *, 'Etiquette 20!'
Avec cette forme de goto
on ne peut affecter qu'une étiquette à la variable.
Notez qu'on ne peut pas utiliser le =
de l'affectation entière pour
stocker l'étiquette dans la variable.
Je ne suis pas assez calé en Fortran pour pouvoir dire quel est l'intérêt
de cette écriture, sinon pour les effets à distance qu'elle permet
(voir le cas de ALTER
en COBOL, section suivante).
COBOL se distingue non pas par son GO TO
, mais
par son instruction ALTER
, qui permet en quelque
sorte d'établir une déviation. Voici un
exemple :
* Début du programme, toutes les déclarations, * définitions et autres. Verbeux comme c'est * l'habitude en COBOL ALTER etiq1 TO GO TO etiq2. * quelques centaines de lignes de code, également verbeuses GO TO etiq1. * encore quelques centaines de lignes etiq1. DISPLAY "salut" UPON CONSOLE. GO TO suite. etiq2. DISPLAY "coucou" UPON CONSOLE. GO TO suite. * et la suite
Comme vous l'avez deviné, ce n'est pas salut
qui s'affichera à l'écran. Mais l'auriez-vous deviné
si j'avais réellement listé le source complet du
programme ? Cela dit, l'utilisation de
ALTER
n'est pas monnaie courante...
goto
Il existe des langages de programmation où le goto
a été
laissé de côté ou, peut-être, oublié. En voici quelques uns,
que les auteurs ont utilisés, le plus souvent de façon
éphémère.
Vous connaissez le shell. Vous avez déjà écrit quelques scripts pour
enchaîner quelques commandes qui reviennent régulièrement. Certains
de vos scripts utilisent même des variables, des boucles et des tests
camouflés en expressions logiques avec &&
et ||
ou des tests
explicites avec if then elif else fi
. Mais aviez-vous remarqué
qu'il n'existe aucune instruction goto
? (Avez-vous au moins
lu la page de man ?)
Un autre langage incontournable pour quiconque utilise Unix de façon
un peu poussée, c'est awk. Là aussi, lisez la page de manuel ou
un tutoriel, vous ne verrez jamais mentionnée d'instruction goto
.
Emacs est basé sur le langage E-lisp et il comporte donc un interpréteur
E-lisp incorporé ainsi que la documentation du langage. Celui des deux
coauteurs qui utilise Emacs pour taper cet article l'a vérifié,
la documentation ne fait référence à aucun branchement goto
donc
cette instruction est vraisemblablement interdite (il
existe des instructions dont le nom comporte goto
, mais il s'agit
de déplacer le pointeur de lecture dans le tampon en cours d'édition,
pas de modifier la position du pointeur d'instruction).
Quant aux autres variantes de Lisp et aux langages dérivés,
qui n'ont même pas l'excuse d'être incorporés (embedded)
dans un éditeur de texte, ils ignorent complètement ce mot et
ce concept.
Plus curieux : le langage de programmation des calculatrices
HP-48 ne comporte aucun goto
. Pourtant, la HP-48 est l'héritière
de la HP-41 dans laquelle le goto
(orthographié GTO
) était
indispensable dès qu'un programme devait effectuer un traitement
itératif.
Si vous grepez la documentation de Ruby à la recherche
d'un GOTO
, vous verrez que c'est un nom ou un prénom assez répandu
au Japon, mais que ce n'est pas une instruction du langage.
En Java, goto
est un mot réservé, mais l'instruction ne fait pas
partie du langage.
Edsger W. Dijkstra
Edsger Wybe Dijkstra (1930-2002) est un chercheur en informatique hollandais qui a eu un rôle important dans le monde de la recherche informatique. Il a travaillé sur le premier compilateur ALGOL 60, et est l'auteur d'un algorithme qui porte son nom pour le problème du plus court chemin (vous pouvez trouvez la description de l'algorithme de Dijkstra dans l'article Recherche des chemins optimaux pour les jeux vidéo, de Fabrice Rossi, dans GNU/Linux Magazine 53).
L'article de 1968 sur goto
a eu une influence considérable : il a accéléré la
disparition du goto
des habitudes de programmation, fait progresser
la notion de programmation structurée et amené la création des structures
de contrôle telles que les boucles while
.
En 1968, Dijkstra a lancé un pavé dans la mare,
remettant en question l'ordre établi et proposant de faire table
rase du goto
. Dijkstra a publié un papier intitulé
A case against the goto statement, mais que que Wirth,
alors rédacteur en chef des Communications of the ACM
a changé en courrier au rédacteur en chef, en lui donnant par
la même occasion un titre destiné à devenir célèbre,
Go To Statement Considered Harmful. Dans cet article,
Dijkstra constatait que les goto
nuisent à la compréhension
du programme par un lecteur humain et que la qualité du code varie en
sens inverse de la densité des goto
. Une petite remarque pour ceux
qui voudraient lire cet article : ne vous attardez pas sur la partie
médiane de l'article, qui décrit un concept d'index textuel et d'index
dynamique. Je doute fort qu'un quelconque programmeur ait eu recours
à cette méthode pour décortiquer le fonctionnement d'un programme.
L'essentiel est écrit au début et à la fin de l'article, le goto
est indésirable et il doit disparaître.
Un peu plus tard, Donald Knuth a publié un autre article,
Structured Programming with go to
Statements.
Le titre donne l'impression d'être un pamphlet destiné
à répondre avec véhémence à l'article de Dijkstra.
En lisant l'article, on constate que ce n'est pas du tout le cas.
Knuth le dit lui-même : il ne prend pas partie dans la
guerre sainte des partisans et des adversaires du goto
,
il veut simplement raconter comment il programme et
quelle place le goto
a dans son code. Il ajoute
que les adversaires du goto
liront des arguments allant
dans le sens de l'éradication du goto
et que les partisans
du goto
liront des arguments justifiant l'existence du goto
.
Et effectivement, à la lecture de son article, on peut constater
que Knuth aborde le sujet avec un esprit ouvert et serein,
sans idée préconçue et sans œillères.
L'article de Knuth montre qu'il n'y a pas besoin d'insérer des goto
pour obtenir un programme illisible. Il montre comment émuler
les goto
avec une boucle et une structure conditionnelle et
il fait remarquer à juste titre que le programme obtenu est
encore moins lisible que le programme d'origine avec des goto
(construction due à Jacopini).
Remarquons que Knuth réfute ainsi la partie médiane de l'article
de Dijkstra en montrant que le système d'index textuels et dynamiques
ne permet pas à un humain de mieux comprendre le fonctionnement
du programme.
Les deux gourous ont tiré des conclusions différentes, que je présenterai au moyen d'une métaphore. Pour Dijkstra, puisque certaines personnes se sont intoxiquées en ingérant de l'eau de Javel, il faut interdire totalement la commercialisation de ce produit et le réserver aux chimistes professionnels. Pour Knuth, il faut vendre l'eau de Javel dans des récipients avec des bouchons de sécurité tels qu'un enfant de moins de 7 ans ne puisse pas l'ouvrir et il faut que les parents instruisent leurs enfants pour leur présenter les dangers de l'eau de Javel et les précautions d'emploi.
Quand on y réfléchit bien, nous sommes entourés d'ustensiles
et d'engins qui, mal utilisés, sont dangereux voire létaux mais qui, dans des
conditions normales d'utilisation, sont inoffensifs ou presque :
couteaux, ciseaux à bouts pointus, tondeuses, prises électriques, etc.
Pour les programmeurs, il ne faut pas oublier d'ajouter goto
à la
liste. ;-)
La notoriété de l'article de Dijkstra a eu un effet secondaire, l'expression considered harmful qui, comme nous l'avons vu, n'est pas due à Dijkstra mais à Wirth, a été maintes fois et est finalement passée dans le langage courant (des informaticiens, s'entend). Si vous googlez cette expression, vous obtenez 65 000 pages, dont plus de 6 000 si vous restreignez la recherche aux titres. Dans deux cas, les auteurs ont voulu élever le débat au niveau supérieur, avec "Goto statement considered harmful" considered harmful, publié par l'ACM et cité dans le Jargon File et "Considered harmful" essays considered harmful, disponible sur le web.
Et puisque le GO TO
est mauvais, l'inverse doit être bon !
C'est ce que Lawrence Clark a dû se dire lorsqu'il a spécifié
une nouvelle instruction, le COME FROM
. C'est également
ce que se sont dit les auteurs d'INTERCAL, lorsqu'ils ont
effectivement implémenté le COME FROM
. À noter que Clark
avait également spécifié le computed COME FROM
et le
assigned COME FROM
.
En 1987, l'un des lauréats de l'IOCCC (International Obfuscated
C Code Contest) fut Spencer Hines, grâce à un programme qui
contenait une bonne vingtaine de goto
. De plus, ce programme
déclarait quelques variables entières toog
, togo
, oogt
, ootg
,
otog
, des variables chaînes de caractères toog
, ogto
,
tgoo
et ainsi de suite. Quant aux labels vers lesquels pointent
les goto
, je n'ai pas besoin de vous faire un dessin. Et finalement,
le style d'indentation mérite d'être appelé « style », mais pas
« indentation ».
goto
Puisque programmer avec des goto
est si néfaste, il a fallu proposer
d'autres solutions aux programmeurs pour qu'ils arrêtent de cuisiner
du spaghetti code. Nous allons vous en présenter quelques unes,
que vous avez certainement déjà rencontrées.
Il y a la construction de Jacopini, déjà citée ci-dessus, mais c'est un exemple à ne pas suivre. Vous pourrez en juger par vous-mêmes vers la fin de cet article, nous vous en donnons un échantillon.
La programmation par exceptions permet également de supprimer nombre
de goto
. Jusqu'alors, lorsqu'une erreur était rencontrée, on avait
le choix entre la traiter immédiatement (en encombrant ainsi le code
principal avec du code de traitement d'erreur souvent aussi gros que
le code du traitement principal, si ce n'est plus) ou bien renvoyer
l'exécution (par un goto
) vers une section du programme prévue à cet
effet (mais en transformant le code en un peu ragoûtant plat de spaghetti,
avec des renvois dans toutes les directions).
Comme nous l'avons dit précedemment, le langage Java n'a pas de goto
.
Ceci s'explique par le fait
que les instructions break
et continue
(avec étiquettes) remplacent
la plupart des utilisations importantes et légitimes de goto
et que
le mécanisme des exceptions remplace les autres.
Le mécanisme des exceptions permet de définir un bloc d'essai (dit try) dans lequel le programme va exécuter des appels de méthodes qui risquent de planter. Pour indiquer l'apparition d'une condition exceptionnelle, le bloc de code ou la méthode à laquelle il a fait appel va lever (throw) une exception : le traitement cesse alors et l'exception va se propager jusqu'à sa capture. Les exceptions se propagent au niveau des blocs lexicaux, puis le long de la pile d'appel des méthodes. La capture des exceptions se fait dans le premier bloc catch rencontré.
On peut alors soit traiter les cas d'erreurs attendus, soit lever de nouvelles exceptions qui seront éventuellement capturées au niveau supérieur. Toute exception qui passe au travers des mailles des différents filets (euh, blocs catch) mis sur son chemin finira par remonter jusqu'à l'utilisateur, qui verra alors son programme planter à cause d'une exception non capturée.
Ainsi, au lieu de s'inquiéter de ce qui peut mal se passer à chaque ligne de code, on va centraliser le traitement des erreurs associées à un bloc logique de code dans un seul bloc de traitement.
Ce qui rend ce mécanisme très intéressant, c'est que les méthodes des bibliothèques standards donnent la liste exhaustive des exceptions qu'elle peuvent lever et que le compilateur impose de préciser dans la signature de ces méthodes les exceptions qu'elles peuvent lever. Ainsi, si une méthode de votre classe fait appel à une méthode qui lève une exception d'entrée/sortie et que votre code ne la traite pas, il vous imposera de déclarer que vous risquez de la propager.
Bien sûr, cette technique a aussi ses limites. En Java, nombre de programmeurs paresseux (dans le mauvais sens du terme) ne traitent pas toutes les exceptions qui peuvent se produire et se contentent de les progager. De proche en proche, les exceptions remontent toute la pile d'appels, et c'est l'utilisateur qui se retrouve démuni face à une pile d'appels longue comme un jour sans pain.
Il existe d'ailleurs un module Perl qui se moque gentiment des programmes
« professionnels » qui plantent en générant un message désespérément
long et détaillé : Acme::JavaTrace
.
La programmation littéraire (Literate Programming, inventée par Don Knuth vise à écrire des programmes comme on écrit des livres. Le programme est présenté en langue naturelle, d'une façon logique, avec de multiples renvois vers le code effectuant les opérations.
Le code de traitement d'erreurs va être typiquement relégué à une autre partie du fichier source et ne va pas encombrer l'esprit du programmeur qui essaye déjà de comprendre ce que fait le bloc de code qu'il est en train de lire. Ce projet s'appelle WEB (son nom date d'avant la création du World-Wide Web, et Knuth expliquait ce choix par le fait que c'était l'un des rare acronymes de trois lettres non encore utilisé) et est au coeur de TeX et LaTeX. Ainsi les fichiers sources LaTeX contiennent leur propre documentation (en LaTeX) et la description de leur code (accompagnée du code lui-même). Lors de la génération des fichiers associés, la documentation est compilée et mise à disposition au format .dvi tandis que le code lui même (fichiers .sty en général) est installé là où LaTeX ira le chercher.
WEB a donné naissance à CWEB, qui est un ensemble de programmes permettant de produire la documentation et les binaires à partir de code source C écrit selon la méthode literate.
goto
goto
est utilisé à plusieurs endroits du noyau Linux (et dans de
nombreux autres systèmes et logiciels complexes). Est-ce que les
développeurs du kernel sont de mauvais programmeurs ? Non, bien
sûr ; goto
est souvent le seul moyen de se sortir de cas
complexes, typiquement pour la gestion d'erreurs.
Les analyseurs syntaxiques (et les machines à états) sont aussi un
cas d'utilisation légitime de goto
. Nous vous invitons par exemple
à examiner le code du module HTML::Parser 2.25 (disponible sur CPAN),
dernière version pur Perl de cet analyseur HTML (les suivantes sont
en XS, pour des raisons de performance).
De nos jours, il existe des alternatives structurées aux utilisations
légitimes de goto
les plus courantes. Aux constructions de boucles
while
ou until
sont associées les instructions break
, redo
ou continue
qui permettent de quitter la boucle par le début ou la fin,
et d'effectuer un traitement à chaque tour de boucle, même si celle-ci
a été interrompue.
goto
You can also "goto hell" if you like, which will of course work better if you've defined the label "hell".
-- Larry Wall, The Perl Conference, 20 août 1997.
Quand on y réfléchit, qu'est-ce qu'un goto
? C'est un saut à un
autre point du programme, en général conditionné par un test. Ceci se
fait sans aucune initialisation (contrairement à l'appel d'un
sous-programme). Le test de la condition ne fait bien sûr pas partie
du goto
.
Nous allons donc nous intéresser aux différentes manières de sauter d'un point à un autre d'un programme Perl, sans initialisation, ce qui exclut les appels à des sous-programmes et l'utilisation de références à des fonctions (tables de distribution).
goto
structurés »Dans de nombreux cas de traitements en boucle, on souhaite pouvoir définir d'autres points de sortie que la fin de la boucle, répéter une itération sans tester la condition de sortie ou encore sortir de plusieurs niveaux de boucles imbriquées d'un seul coup.
En Perl, les instructions next
, last
et redo
permettent de
résoudre ces différents cas de figure sans faire appel au goto
ni
introduire tests et variables inutiles dans le corps de la boucle.
Ces instructions sont toutes des genres de goto
, puisqu'elles
permettent de sauter à diverses positions dans la boucle (ou juste en
dehors). Le bloc continue
associé à la boucle while
permet quant
à lui d'assurer que la partie finale de la boucle sera exécutée même
quand on la court-circuite.
Quand nous parlons de « genres de goto
», il s'agit bien d'une
comparaison, car comme le précise la documentation
de Perl : A loop LABEL is not actually a valid target for a goto;
it's just the name of the loop. (L'étiquette d'une boucle n'est pas
une cible valide pour un goto
, il s'agit juste du nom de la boucle.)
last
La commande last
permet de sortir immédiatement d'une boucle.
Elle accepte une étiquette optionnelle pour indiquer de quelle boucle
on veut sortir, dans le cas de plusieurs boucles imbriquées. Sinon,
on sort de la boucle la plus petite contenant l'instruction last
.
while(<>) { # sort de la boucle last if /^__END__/; ... } # pour continuer ici
Avec plusieurs boucles imbriquées :
EXTERNE: for my $i ( @indexes ) { INTERNE: for my $j ( @jndexes ) { ... last EXTERNE if $j == 42; } }
last
ne peut pas être utilisé pour sortir d'un bloc eval {}
,
sub {}
ou do {}
(car ceux-ci renvoient une valeur). Il ne peut
pas non plus être utilisé pour sortir du bloc de code exécuté par une
instruction grep()
ou map()
.
next
next
permet de court-circuiter la fin de la boucle (comme last
)
pour reprendre à l'itération suivante. Les instructions associées aux
itérations des boucles sont traitées normalement.
Ainsi, dans le cas d'une boucle for
« à la C », l'expression
de modification et l'expression de test sont exécutées. Pour une boucle
foreach
, l'élément suivant de la liste traitée est utilisé pour
la variable d'indice. Et dans le cas d'une boucle while
, le bloc
continue
est exécuté, ainsi que l'expression de test associée au while
.
# exemple d'analyseur de fichiers while(<>) { # ignore commentaires et lignes blanches next if /^\s*(#|$)/; chomp; ... }
redo
L'instruction redo
ramène le pointeur d'instruction au début du bloc
courant (elle accepte également une étiquette pour les cas de blocs
imbriqués), sans exécuter les calculs de condition de la structure
for
, foreach
ou while
associée.
redo
ne peut pas être utilisé pour ré-essayer un bloc qui renvoie
une valeur, comme eval {}
, sub {}
ou do {}
, ni dans le bloc
associé à une instruction grep
, map
ou sort
.
En revanche, sachez qu'un bloc étant équivalent à une boucle qui ne
s'exécute qu'une seule fois, il est donc possible d'utiliser redo
pour le transformer en construction de boucle.
continue BLOC
Les blocs continue
sont associés aux structures de boucles et sont
toujours exécutés à la fin de chaque tour de boucle (y compris ceux
interrompus par last
ou next
, c'est tout l'intérêt) juste avant
l'évaluation de la clause de condition.
Ceci sert évidement quand on sort prématurément du corps d'une boucle
avec next
. L'instruction redo
ne provoque pas l'exécution du
bloc continue
.
Les instructions next
, redo
et last
peuvent parfaitement
apparaître dans un bloc continue
. redo
et last
se comportent
comme s'ils avaient été exécutés dans le bloc principal de la boucle.
Comme next
provoque l'exécution du bloc continue
, cela peut
provoquer des effets « intéressants » (mais rarement utiles).
Notez qu'on peut tout à fait associer un bloc continue
à une boucle
foreach
.
goto
Malgré l'existence des commandes de sortie de boucle, une commande goto
a tout de même son utilité. C'est ainsi qu'il existe en fait trois
formes de goto
en Perl.
goto ETIQUETTE
La forme goto-ETIQUETTE
retrouve l'instruction qui porte l'étiquette
et continue l'exécution à partir de ce point. Attention, on ne peut pas
s'en servir pour sauter à l'intérieur d'un sous-programme ou d'une
boucle foreach
. De même, on ne peut entrer dans une construction
qui est supprimée par le compilateur à la phase d'optimisation, ou pour
sortir d'un bloc ou d'une routine passée à sort()
.
goto EXPR
Cette forme permet de faire des goto
calculés, à la manière FORTRAN
que nous avons vue plus haut.
goto ("TOTO", "TITI", "TUTU")[$i];
Si le résultat de l'expression est une étiquette qui n'existe pas
nous aurons droit à l'erreur Can't find label LABEL
, tandis que
ce sera goto must have a label
si le résultat de l'expression
est vide (undef
).
Nous ne connaissons personne ayant eu besoin d'utiliser ce type de goto
.
goto &NOM
Ce goto
est assez différent du goto
habituel. Il s'agit ici de
sauter vers une autre fonction de façon transparente, en effaçant
la fonction exécutant le goto
de la pile d'appel.
Cette technique est beaucoup utilisée en combinaison avec la méthode
AUTOLOAD
. En Perl objet, quand on appelle une méthode, Perl va
parcourir le tableau @ISA
de la classe pour trouver une méthode
portant ce nom. Si cette recherche ne donne rien, Perl va alors chercher
si l'une des classes définit une méthode AUTOLOAD
qui sera alors
appelée. Cette méthode va pouvoir exécuter un traitement de substitution
pour pallier l'absence de la méthode recherchée.
Voici un extrait du module HTTP::Message, qui utilise cette technique :
# delegate all other method calls the the _headers object. sub AUTOLOAD { my $method = substr($AUTOLOAD, rindex($AUTOLOAD, '::')+2); return if $method eq "DESTROY"; # We create the function here so that it will not need to be # autoloaded the next time. no strict 'refs'; *$method = eval "sub { shift->{'_headers'}->$method(\@_) }"; goto &$method; }
Ici, les méthodes inconnues sont déléguées de façon transparente à
l'objet associé aux en-têtes du message. La ligne qui précède le
goto
insère une nouvelle référence de code dans la table des
symboles du paquetage en question (ici, HTTP::Message) et le
goto
se charge d'appeler cette fonction de façon transparente,
comme si elle avait toujours existé à cet endroit.
Nous sortons ici du cadre que nous nous étions imposé au début de cette partie, car cette forme de goto est en réalité un appel à un sous-programme.
Les programmeurs qui découvrent AUTOLOAD
laissent souvent libre
cours à leur paresse et l'utilisent pour définir les accesseurs de
leur classe de cette façon. Nous ne recommandons pas cette technique,
qui est à notre avis plus propice à la création de bogues difficiles
à tracer (AUTOLOAD
va par exemple essayer de faire quelque chose
même si la fonction appelée résulte en fait d'une faute de frappe, si
votre AUTOLOAD
ne provoque pas d'erreur dans ce cas, on aura appelé
une fonction qui ne fait probablement rien. Difficile de retrouver la
faute de frappe dans ces conditions).
Nous préférons la technique qui consiste à compiler les accesseurs à l'avance, et à les positionner directement dans la table des symboles du paquetage.
for my $attr (qw( name id height width )) { no strict 'refs'; *{"get_$attr"} = sub { $_[0]->{$attr} }; }
Les accesseurs ainsi créés s'utilisent comme s'ils avaient été définis
avec sub
. Comme ce code est en général dans un module chargé avec
use
, les fonctions sont disponibles dès le use
exécuté.
Ce code s'appuie sur les propriétés des fermetures (closures).
Notre exemple montre des accesseurs de type get()
, mais il est
bien sûr possible d'implémenter des méthodes plus complexes ou
d'utiliser plusieurs boucles d'initialisation.
Il suffit de comparer avec le code équivalent pour voir l'intérêt de cette écriture au niveau de la maintenabilité et de l'évolution du code :
# imaginez une définition plus complexe # et trente fonctions identiques sub get_name { $_[0]->{name} } sub get_id { $_[0]->{id} } sub get_height { $_[0]->{height} } sub get_width { $_[0]->{width} }
Avec notre exemple, un éventuel changement d'implémentation est
instantané, sans risque d'erreur à la recopie, et l'ajout d'une méthode
revient simplement à ajouter un mot dans la liste passée au foreach
.
Maintenant que nous avons exposé l'histoire et les alternatives au
goto
, voici quelques exemples pratiques d'utilisation.
Supposons que l'on cherche l'adresse de divers correspondants. Ces adresses sont stockées à différents niveaux de mémoire, dans une variable du programme, dans un fichier se trouvant sur la machine locale, dans une base de données accessibles par le réseau local ou sur Internet. Dans un but de performances, on privilégiera l'accès le plus rapide.
use DBI; use WWW::Search::PagesJaunes; my $repert = '/home/moi/adresses.txt'; my $db_source = '???'; my $db_user = 'moi'; my $db_passwd = 's3kr3t'; my %adresse; sub recherche { my ($qui) = @_; # Cas idéal, il est déjà en mémoire return $adresse{$qui} if $adresse{$qui}; # Fichier local, peut-être ? open REPERT, $repert or goto ESSAI_BDD; while (<REPERT>) { my ( $lui, $ou ) = split ':'; # Si trouvé, on mémorise et on renvoie return $adresse{$lui} = $ou if $lui eq $qui; } close REPERT; ESSAI_BDD: my $dbh = DBI->connect( $data_source, $db_user, $db_passwd ) or goto ESSAI_INTERNET; my $acces = $dbh->prepare("SELECT adresse FROM personnes WHERE nom = ?"); return $dbh->selectrow_array($acces) or goto ESSAI_INTERNET; ESSAI_INTERNET: my $pj = WWW::Search::Pagesjaunes->new(); $pj->find( nom => $qui, departement => "75" ); my @resultats = $pj->results(); return $resultats[0]->addresse if @resultats; }
Cet exemple montre l'un des cas d'utilisation admise du goto
:
la reprise après erreur. Cette fonction est lisible pour deux
raisons : tous les goto
descendent et la fonction tient sur
une page d'écran (nous sommes désolés si la pagination du magazine
fait que ce n'est pas le cas sur le papier).
La variante sans goto
aurait conduit à utiliser un indicateur booléen
et à enrober chacun des accès élémentaires read
, fetch
ou get
par un test vérifiant le succès de l'accès précédent open
, connect
.
En fait, on peut également se passer complètement de l'utilisation de
goto
pour cet exemple, en utilisant des tables de distribution
(dispatch tables, voir l'article Perles de Mongueurs à ce sujet
dans GNU/Linux Magazine 65). Cette technique a l'avantage d'être
plus facilement extensible.
my %adresse; # table de distribution des méthodes utilisables # l'ordre alphabétique des clés permet de définir # l'ordre de préférence des techniques de recherche my %recherche = ( r00_memoire => sub { return $adresse{$_[0]} } # déclaration en ligne r01_local => \&recherche_fichier, # fonctions définies r02_bdd => \&recherche_bdd, # un peu plus loin r03_internet => \&recheche_internet, # dans le script ); sub recherche { my ($qui) = @_; my $adresse; for my $essai ( sort keys %recherche) { $adresse = $recherche{$essai}->( $qui ); last if $adresse; } # technique de cache pour accélerer les recherches suivantes : # on enrichit le cache local $adresse{$qui} = $address if defined $adresse; return $adresse; } # recherche dans un fichier my $repert = '/home/moi/adresses.txt'; sub recherche_fichier { open my $fh, $repert or return; while (<$fh>) { my ( $lui, $ou ) = split ':'; return $ou if $lui eq $qui; } close $fh; } # recherche dans une base de données use DBI; my $db_source = '???'; my $db_user = 'moi'; my $db_passwd = 's3kr3t'; sub recherche_bdd { # même code que dans l'exemple précédent } # recherche sur Internet use WWW::Search::PagesJaunes; sub recherche_internet { # même code que dans l'exemple précédent }
L'ajout d'une nouvelle technique de recherche se cantonnera donc à l'ajout d'une fonction dans le code et d'une clé dans la table de hachage. Pour changer l'ordre des tentatives, il suffit de renommer les clés de la table de hachage.
Nous prenons un exemple inspiré d'un programme réel conçu par l'un des coauteurs. Je travaillais sur un projet pour écrire des programmes C/SQL qui devaient être déployés dans plusieurs pays. Dès l'origine, il a été convenu de scinder le source de chaque programme en au moins deux fichiers, toto.c qui contient les traitements et toto.h qui contient quelques déclarations du genre :
#define MSG "C'est planté !" char *url = "http://www.mongueurs.net/"; char *actions [] = { "PUT", "GET", "POST" }; char *histoire [] = { "Il demanda : \"Quand ?\"\n", "Elle répondit : \"Demain...\x22\n" };
Et c'est à moi qu'est revenue la tâche de spécifier les utilitaires permettant d'extraire les chaînes à traduire et de réinjecter les chaînes traduites. Comme les fichiers en entrée obéissaient à une syntaxe C très simplifiée, l'extraction pouvait se baser sur un automate à quatre états (dans la réalité un peu plus, les fichiers pouvant contenir des commentaires). Voici ce que cela donne en Perl :
#!/usr/local/bin/perl # # Extraction des libellés d'un source C simplifié # use strict; use warnings; local $/ = \1; # lecture caractère par caractère my $ident; my $chaine; my $car; open IN, $ARGV[0] or die "Problème avec le fichier"; A: $car = <IN>; goto FIN unless defined $car; if ($car =~ /[[:alnum:]]/) { $ident = $car; goto B } if ($car eq '"') { $chaine = ''; goto C } goto A; B: $car = <IN>; goto FIN unless defined $car; if ($car =~ /[[:alnum:]]/) { $ident .= $car; goto B } if ($car eq q(")) { $chaine = ''; goto C } goto A; C: $car = <IN>; goto D if $car eq q(\\); if ($car eq q(")) { store($ident, $chaine); goto A } $chaine .= $car; goto C; D: $car = <IN>; $chaine .= q(\\) . $car; goto C; FIN: print "Fini !\n"; sub store { my ($ident, $chaine) = @_; print "$ident -> $chaine\n" # En fait un ordre SQL }
Au fait, nous vous avions promis un exemple de la méthode de Jacopini
pour éliminer les GOTO
. Voici ce que donnerait cette méthode :
#!/usr/local/bin/perl # # Extraction des libellés d'un source C simplifié # avec la méthode de Jacopini # use strict; use warnings; local $/ = \1; # caractère par caractère my $ident; my $chaine; my $car; open IN, $ARGV[0] or die "Problème avec le fichier"; my $etiq = 'A'; while ($etiq ne 'FIN') { if ($etiq eq 'A') { $car = <DATA>; if (not defined $car) { $etiq = 'FIN' } elsif ($car =~ /[[:alnum:]]/) { $ident = $car; $etiq = 'B' } elsif ($car eq '"') { $chaine = ''; $etiq = 'C' } else { $etiq = 'A' } } if ($etiq eq 'B') { $car = <DATA>; if (not defined $car) { $etiq = 'FIN' } elsif ($car =~ /[[:alnum:]]/) { $ident .= $car; $etiq = 'B' } elsif ($car eq q(")) { $chaine = ''; $etiq = 'C' } else { $etiq = 'A' } } if ($etiq eq 'C') { $car = <DATA>; if ($car eq q(\\)) { $etiq = 'D' } elsif ($car eq q(")) { store($ident, $chaine); $etiq = 'A' } else { $chaine .= $car; $etiq = 'C' } } if ($etiq eq 'D') { $car = <DATA>; $chaine .= q(\\) . $car; $etiq = 'C'; } } print "Fini !\n"; sub store { my ($ident, $chaine) = @_; print "$ident -> $chaine\n" }
Vous serez je pense d'accord avec Dijkstra, Knuth et nous-mêmes, cette version est illisible !
AUTOLOAD
Voici un exemple d'utilisation d'AUTOLOAD
pour définir le plus tard
possible (à l'exécution) le code d'une fonction. C'est le cas usuel
d'utilisation du goto
routine en Perl.
L'exemple de code ci-après est utilisé en production pour définir des
accesseurs associés à des valeurs extraites d'une base de donnée.
Une table rights
définit des associations user_id
, right_id
,
où l'identificateur du droit est une simple chaîne de caractères
(admin
, support
, etc.). Les méthodes is_admin()
, is_support()
,
etc. sont utilisées dans d'autres parties du code et dans des templates
pour déterminer ce à quoi l'utilisateur connecté a accès.
sub AUTOLOAD { # notre classe n'a pas de méthode DESTROY return if $AUTOLOAD =~ /::DESTROY/; # la fonction rights() renvoie la liste des droits # associés à cet utilisateur dans la base if( $AUTOLOAD =~ /::is_(\w+)$/ ) { my $attr = $1; no strict 'refs'; # crée la méthode et l'insère dans la table des symboles # du paquetage en cours (celui de la classe) *{$AUTOLOAD} = sub { $_[0]->rights()->{$attr} }; # appel la méthode de façon transparente goto &{$AUTOLOAD}; } # meurt si une méthode inconnue a été appelée croak "AUTOLOAD: Unknown method $AUTOLOAD"; }
Avec cette technique, les méthodes d'accès aux droits n'ont pas a être ajoutées au fur et à mesure qu'on invente de nouvelles catégories d'utilisateurs, et on garantit qu'elles sont toutes codées de la même façon.
Vous l'avez constaté, il existe de nombreux cas en programmation où
l'instruction goto
est appropriée. Mais depuis la polémique autour
de l'utilisation du goto
les langages de programmation ont adopté
de nouveaux mots-clés pour faciliter la programmation structurée.
C'est pourquoi en Perl, le goto
« classique » est réservé à certains
types de programmes et ne doit pas être utilisé pour la sortie de boucles, la
gestion d'exception ou l'émulation d'une instruction case
.
Le goto
routine de Perl n'est pas couvert par les réticences
d'Edsger Dijkstra : c'est un moyen très courant de manipuler la
pile d'appels, qui est souvent utilisé en programmation objet en Perl.
goto
http://www.acm.org/classics/oct95/
Pendant près de quarante ans, Dijkstra a tenu une correspondance abondante avec ses collègues chercheurs. La plupart de ces manuscrits ont été transcrits et sont disponibles sur l'Edsger W. Dijkstra Archive : http://www.cs.utexas.edu/users/EWD/
go to
Statements
Computing Surveys, Vol. 6, No. 4, December 1974.
Literate Programming (CSLI Lectures Notes 27, 1992) reprend cet article et bien d'autres.
http://pplab.snu.ac.kr/courses/adv_pl05/papers/p261-knuth.pdf
DATAMATION, décembre 1973.
Illustration humoristique de ce qui peut arriver si vous utilisez GOTO
.
http://en.wikipedia.org/wiki/Timeline_of_programming_languages
http://www.csis.ul.ie/COBOL/default.htm http://home.swbell.net/mck9/cobol/cobol.html
Les règles du Perl Club sont listées sur le site de Dave Cross : http://www.dave.org.uk/perlclub.html
Ces règles sont bien sûr inspirées du film Fight Club de David Fincher, avec Edward Norton et Brad Pitt. http://www.imdb.com/title/tt0137523/
Cette page de manuel contient la documentation de goto
, last
, next
,
redo
et continue
.
Le point 4 de la section Making references explique en détail ce que sont les closures.
La FAQ de Perl contient également une définition des closures.
Vous pouvez y accéder directement par la commande perldoc -q closure
.
Chapitre consacré aux exceptions dans le tutoriel Java. http://java.sun.com/docs/books/tutorial/essential/exceptions/index.html
Le premier langage à implémenter le COME FROM
.
http://www.catb.org/~esr/intercal/
Jean Forget pratique Perl depuis 1998 et a quasiment cessé d'utiliser C et awk depuis cette date. Il est secrétaire de l'association Les Mongueurs de Perl, ainsi que membre du groupe de Paris. Il a participé en tant que correcteur à la traduction de la troisième édition de Programming Perl.
Philippe Bruhat est président de l'association les Mongueurs de Perl (http://www.mongueurs.net/), membre des groupes Paris.pm et Lyon.pm. Il est consultant en Perl et en sécurité et l'auteur des modules Log::Procmail, HTTP::Proxy, Regexp::Log et Acme::MetaSyntactic, disponibles sur CPAN.
Dans HTTP::Proxy, il est fier d'avoir utilisé deux des trois formes
de goto
pour créer un mini-analyseur de HTML (goto ETIQUETTE
,
redo
) et pour créer des objets avec des méthodes spécifiques à chaque
instance (goto &CODEREF
).
Il vous invite à venir nombreux à la prochaine conférence Perl francophone, Les Journées Perl 2005, qui auront lieu à Marseille, les 9 et 10 juin 2005.
Si vous avez lu l'article de Knuth, vous n'avez peut-être pas prêté attention à l'expression « four-letter words like goto » utilisée par Knuth. Moi, si. En effet, elle faisait écho à un article que j'ai écrit en d'autres circonstances pour un autre public.
En dehors de la programmation Perl, je pratique les jeux de simulation
historique (wargames). La plupart sont en anglais, mais on trouve
parfois des traductions pour certains. À force de lire des mauvaises
traductions (dont certaines venaient de ma plume :-(
), j'ai décidé de
mettre par écrit un certain nombre de conseils concernant la
traduction de l'anglais vers le français, conseils que l'on n'apprend
pas toujours au collège ou au lycée.
Pour l'extrait suivant, vous aurez peut-être besoin de revoir la première heure de l'Étoffe des Héros et de réviser votre Histoire de France. Ou bien, de chercher les noms propres dans votre moteur de recherche favori (méfiez-vous des homonymes : cherchez « général Pierre Koenig » dans les pages en français).
Si vous avez vu l'Étoffe des Héros, vous vous souvenez peut-être de Pancho Barnes, la tenancière du saloon de la base d'Edwards, un personnage haut en couleurs. Dans son autobiographie, Chuck Yeager la décrit ainsi :
V.O. Pancho would never use a five- or six-letter word when a four-letter word would do. She had the filthiest mouth that any of us fighter jocks had ever heard.
J'ai vu ces phrases traduites ainsi :
Mauvais Elle n'employait jamais un mot de cinq ou six lettres quand un mot de quatre lettres pouvait suffire. Elle avait un langage ordurier auquel même nous, vieux pilotes de chasse, nous n'arrivions pas à nous faire.
La traduction correcte serait plutôt :
Bon Elle n'employait jamais un mot de six ou sept lettres quand un mot de cinq lettres pouvait suffire. Elle avait un langage ordurier auquel même nous, vieux pilotes de chasse, nous n'arrivions pas à nous faire.
Et pourquoi ? Parce que pour les Français, les termes « un mot de cinq lettres » ne désignent pas n'importe quel mot de la longueur indiquée, mais un mot bien particulier. Vous savez, le « mot de Cambronne », que le général Koenig, assiégé à Bir-Hakeim, aurait prononcé quand on lui proposait de se rendre. De même, l'expression « four-letter word » désigne pour les Américains, non pas un mot, mais une série de mots à usage... disons, spécialisé. Cela va du « Nuts » anodin du général McAuliffe, assiégé à Bastogne en 1944 (1) aux deux mots S**t et F**k que vous connaissez bien, en passant par d'autres que je ne vous enseignerai pas. Par conséquent, la véritable traduction de « four-letter word » est « mot de cinq lettres ». Par extension, dans la phrase citée en exemple, il faudra traduire « five or six » par « six ou sept ».
(1) Comme quoi, les généraux commandant des troupes d'élite (Vieille
Garde, Légion Étrangère, Parachutistes) ont parfois les mêmes idées. À
propos, quelqu'un peut-il m'indiquer quel mot grossier fut proféré par
Fingon, le Haut-Roi des Noldor, encerclé à la bataille des Larmes
Innombrables ? ;-)
Pour en revenir à la phrase de Knuth, le nombre de lettres n'a
aucun intérêt. La traduction qui s'impose serait donc :
« des mots obscènes comme goto
».
Copyright © Les Mongueurs de Perl, 2001-2011
pour le site.
Les auteurs conservent le copyright de leurs articles.