Itérateurs

Les itérateurs ne sont pas un concept propre à ruby. Ils sont courants dans les langages orientés objet.. Ils sont aussi utilisés en Lisp, bien qu’ils ne s’y appellent pas comme cela. Cependant le concept d’itérateur est sûrement peu familier pour beaucoup d’entre vous, aussi allons nous l’expliquer plus en détail.

Le verbe itérer signifie faire la même chose plusieurs fois, vous le savez; donc un itérateur est quelque chose qui fait la même chose plusieurs fois.

Quand on écrit des programmes, on a besoin de boucles dans diverses situations. En C, on les code avec des for ou des while. Par exemple,


char *str;
for (str = "abcdefg"; *str != '\0'; str++) {
 /* ici on traite un caractère */
}

La syntaxe C for(...) fournit une abstraction pour aider à la création d’une boucle, mais le test de *str vis à vis d’un caractère nul requiert du programmeur une connaissance de la représentation interne des chaînes. C’est ce qui fait percevoir le C comme un langage de bas niveau. Les langages de plus haut niveau sont caractérisés par leur support plus souple des itérations. Considérons le script shell sh suivant :


#!/bin/sh

for i in *.[ch]; do
 # ... ici on ferait quelque chose sur chaque fichier
done

Tous les fichiers sources C et header du répertoire courant sont traités, et la commande shell traite les détails relatifs à la collecte et à la substitution des noms de fichiers les uns après les autres. Voilà quelque chose de plus haut niveau que le C, non ?

Mais il y a plus: c’est bien de trouver dans un langage de quoi faire des itérations sur les types d’objets prédéfinis, mais c’est décevant s’il faut revenir aux boucles de bas niveau pour ses propres types d’objets. En programmation orientée objet, les développeurs définissent souvent les objets à la pelle, aussi le problème y devient-il sérieux.

C’est pourquoi tous les langages orientés objet fournissent ce qu’il faut pour les itérations. Certains fournissent une classe spéciale pour cela, ruby quand à lui permet de définir des itérateurs directement.

Le type String (chaîne) en ruby offre des itérateurs très utiles :


 ruby> "abc".each_byte{|c| printf "<%c>", c}; print "\n" 
 <a><b><c>
   nil

each_byte est un itérateur pour chacun des caractères de la chaîne. Chaque caractère est placé successivement dans la variable locale c. Ceci peut être traduit par quelque chose qui ressemble beaucoup à du C…


 ruby> s="abc";i=0
   0
 ruby> while i<s.length
    |    printf "<%c>", s[i]; i+=1
    | end; print "\n" 
 <a><b><c>
   nil

... mais l’itérateur each_byte est à la fois conceptuellement plus simple et plus enclin à continuer de fonctionner même si la classe String devait être radicalement modifiée dans le futur. Un des bénéfices des itérateurs est qu’ils tendent à être robustes face à de tels changements, ce qui est d’ailleurs une caractéristique du code sain en général. (Patience, on va bientôt parlez des classes.)

Un autre itérateur de String est each_line.


 ruby> "a\nb\nc\n".each_line{|l| print l}
 a
 b
 c
   nil

Tout ce qui ferait le gros du travail en C (chercher les délimiteurs de ligne, générer des sous-chaînes, etc.) est facilement réalisé avec les itérateurs.

L’instruction for qui apparaissait au chapitre précédent accomplit l’itération à l’aide d’un each. Les each sur les chaînes travaillent de la même façon que each_line, alors réécrivons l’exemple ci-dessus avec for :


 ruby> for l in "a\nb\nc\n" 
    |   print l 
    | end
 a
 b
 c
   nil

Nous pouvons utiliser une structure de contrôle retry en conjonction avec une boucle, et elle réessayera l’itération courante depuis le début de la boucle.


 ruby> c=0
   0
 ruby> for i in 0..4
    |   print i
    |   if i == 2 and c == 0
    |     c = 1
    |     print "\n" 
    |     retry
    |   end
    | end; print "\n" 
 012
 01234
   nil

yield (fournir) apparaît parfois dans la définition d’un itérateur. yield passe le contrôle au bloc de code qui est associé à l’itérateur (on verra ça plus en détail au chapitre sur les objets procedures. L’exemple suivant définit un itérateur repeat, qui répète un bloc de code le nombre de fois indiqué en argument.


 ruby> def repeat(num)
    |   while num > 0
    |     yield
    |     num -= 1
    |   end
    | end
   nil
 ruby> repeat(3) { print "foo\n" }
 foo
 foo
 foo
   nil

Avec retry, on peut définir un itérateur qui fonctionne comme while, mais c’est très lent.


 ruby> def WHILE(cond)
    |   return if not cond
    |   yield
    |   retry
    | end
   nil
 ruby> i=0; WHILE(i<3) { print i; i+=1 }
 012   nil

Comprenez-vous maintenant ce qu’est un itérateur? Il y a quelques restrictions, mais on peut écrire son propre itérateur, et en fait, à chaque fois qu’on définit un nouveau type de donnée, il est généralement commode de définir les itérateurs qui vont avec. Sous cet angle, les exemples ci-dessus ne sont pas très utiles. Nous parlerons d’itérateurs vraiment utiles lorsque nous aurons une meilleure compréhension de ce que sont les classes.

Guide de l’utilisateur Précédent : GDU : Structures de contrôle Suivant : GDU : Penser orienté-objet