Outils pour utilisateurs

Outils du site


cours:informatique:dev:programmation_objet_pharo:330_iterateurs

Pharo : les itérateurs

On va revenir sur les collections et voir comment est-ce qu'on peut les parcourir facilement, grâce aux itérateurs en Pharo. Vous allez comprendre la puissance des itérateurs en Pharo, et puis je vais vous donner un panorama des principaux itérateurs que vous pouvez utiliser sur vos collections.

Code compact

Juste un exemple, ci -dessous le code que vous devriez écrire par exemple en Java pour parcourir une collection.

ArrayList<String> strings = new ArrayList<String>();
for(Person person:persons)
  strings.add(person.name());

Je veux parcourir une collection persons, pour extraire la liste des noms de ces personnes, de ces objets Person.

En fait en Pharo, on écrirait plutôt ce code-là:

strings := persons collect: [ :person | person name ]

On utiliserait un itérateur, qu'on reverra dans la suite du cours, collect:, où on va collecter l'ensemble des noms des personnes.

En Java 8 pour l'anecdote, (la dernière version de Java), ils ont introduit la notion de blocks, l'équivalent des blocks SmallTalk, les fermetures lexicales, qui leur permet d'avoir une syntaxe qui est proche de celle de Pharo.

strings = persons.stream().map(person->person.getName())

Sauf qu'en Pharo, on l'a depuis très longtemps, depuis le début et elle est au cœur du langage, ce qui donne une puissance d'expression aux programmeurs assez importante.

L'itérateur collect:

Il existe plein d'itérateurs. Commençons par présenter collect:. A quoi sert collect: quand je l'envoie à une collection?

  1. #(2 -3 4 -35 4) collect: [ :each | each abs ]
  2.  
  3. ">#(2 3 4 35 4)"

Ici on a une collection de nombres (Array déclaré via un tableau littéral), contenant des nombres positifs et des nombres négatifs. J'envoie le message à mot clé collect: à cette collection et je lui passe un block.

A chaque tour de la collection, le paramètre du block vaudra successivement 2, -3, 4, etc. Et puis on va envoyer le message unaire abs, qui calcule la valeur absolue. Donc ça veut dire qu'on va demander la valeur absolue de ce nombre.

Une fois qu'on aura appliqué ce block à chacun des éléments de la collection, on va agréger tous les résultats dans une nouvelle collection. On voit que le résultat rendu par collect c'est une nouvelle collection (ligne 3), telle qu'on a appliqué ce block à chacun des éléments de la collection . On a pris la valeur absolue de 2; la valeur absolue de moins 3, donc 3; la valeur absolue de 4, 4; la valeur absolue de moins 35, ça fait 35 et la valeur absolue de 4 ça fait 4.

Le point à retenir qui est vraiment intéressant, c'est qu'on pense objet. On demande à la collection de faire quelque chose pour nous, donc c'est la collection qui va se parcourir les éléments qu'elle contient elle-même et on va lui fournir le traitement à appliquer sur chacun des éléments. C'est là qu'est le secret des itérateurs.

Voici un nouvel exemple de collect.

#( 16 11 68 19 ) collect: [ :i | i odd ]
 
"> #(false true false true)"

J'ai une collection, je lui envoie le message collect: et dans le block je vais à chaque fois demander “Est-ce que l'élément est impair ?” via le message unaire odd. Je vais agréger tous les résultats dans une nouvelle collection: #(false true false true).

Vous pouvez naturellement écrire ce qu'on aurait l'habitude d'écrire dans un autre langage où les blocks et les itérateurs n'existent pas, donc on pourrait écrire:

  1. |result|
  2.  
  3. aCol := #( 16 11 68 19 ).
  4. result := aCol species new: aCol size.
  5. 1 to: aCol size do:
  6. [ :each | result at: each put: (aCol at: each) odd ].
  7.  
  8. ^ result

Je prends la collection, je vais construire une collection de résultats et je vais parcourir de 1 à la taille de la collection, aCol size. Je vais utiliser un do:. Et puis je vais parcourir la collection et à chaque fois ajouter dans la collection le résultat. L'ensemble de ce code est équivalent au précédent: la concision et la clarté du code présenté initialement est clairement un avantage.

Hiérarchie des collections

Il existe dans la hiérarchie des collections Pharo quelque chose de fondamental, c'est que toutes les collections sont polymorphiques et héritent de la classe collection et on a une API commune.

L'avantage, c'est que les itérateurs aussi vont fonctionner sur la plupart des collections. L'idée, c'est vraiment de penser objet. On demande à la collection d'itérer sur ces éléments pour nous, puisque on ne sait pas si on est en train de manipuler un dictionnaire ou on n'a pas envie de savoir comment sont représentées les clefs, les valeurs, etc.

Donc on va vraiment demander “Collection, parcours-toi et applique ce traitement sur tes éléments.” Il existe beaucoup d'itérateurs qui vont permettre de faire ça.

  • do: (itérer sur les éléments)
  • collect: (itérer et récupérer les résultats)
  • select: (itérer et sélectionner certains éléments)
  • reject: (itérer et rejeter certains éléments)
  • detect: (itérer et retourner le premier élément correspondant au critère)
  • detect:ifNone: (itérer, retourner le premier élément correspondant au critère ou définir une valeur par défaut)
  • includes: (effectuer un test d'inclusion/présence)
  • etc

do: agir sur chaque élément

Le message do:, c'est l'itérateur le plus simple qui permet de parcourir chacun des éléments de la collection:

#( 16 11 68 19 ) do: [ :each | Transcript show: each; cr ]

Ici j' affiche les éléments de la collection littérale dans le Transcript.

select: récupérer les éléments selon un critère

Un nouvel itérateur qui est select:. Je veux récupérer tous les éléments de la collection qui répondent à un critère donné.

  1. #( 16 11 68 19 ) select: [ :i | i odd ]
  2. "> #(11 19)"
  3.  
  4.  
  5. # Équivalent
  6. #( 16 11 68 19 ) select: #odd
  7. "> #(11 19)"

Ici, je veux tous les éléments impairs de la collection. J'envoie select: à la collection. Je passe un block et à chaque fois que ce bloc va s'évaluer à “Vrai”, l'élément en question sera ajouté dans la collection résultats.

Les lignes 1 et 6 sont complètement équivalentes. Quand j'ai un un bloc (lige 1) ou ce qui constitue seulement un envoi de message à l'élément de la collection, le paramètre du block, je peux mettre directement sous la forme d'un symbole le nom du message à envoyer (ligne 6). C'est encore plus concis mais ça ne fonctionne que pour les messages unaires.

reject: rejeter selon un critère

On peut utiliser d'autres types d'itérateurs comme reject. Je veux éliminer tous les éléments de la collection qui sont impairs:

#( 16 11 68 19 ) reject: [ :i | i odd ]
"> #(16 68)"
 
 
# Équivalent
#( 16 11 68 19 ) reject: #odd
"> #(16 68)"

Dans la collection résultat, il ne va me rester que des éléments pairs.

detect: sélectionner le premier élément correspondant au critère

Lorsque je veux détecter le premier élément de la collection qui répond à un critère donné pour lequel l'évaluation du bloc va être vraie, je peux utiliser l'itérateur detect:.

#( 16 11 68 19 ) detect: [ :i | i odd ]
"> 11"
 
 
# Équivalent
#( 16 11 68 19 ) detect: #odd
"> 11"

Je veux le premier élément de la collection qui est impair, donc 11. Ici detect: retourne bien un élément, pas une collection.

Dans certains cas, on veut détecter le premier élément de la collection qui répond à un critère, mais s'il n'y en a pas alors on aimerait bien avoir une valeur par défaut. C'est l'itérateur detect:ifNone: qu'on peut alors utiliser.

#( 16 12 68 20 ) detect: #odd ifNone: [ 0 ]

S'il n'y a aucun élément c'est l'évaluation de ce block là qui va être effectuée, et donc ça va bien nous renvoyer 0.

Autres itérateurs usuels

Il y a d'autres itérateurs encore qui vont encore faciliter la vie du programmeur. Par exemple:

  • anySatisfy: Est-ce qu'il existe dans la collection un élément qui répond à ce critère-là?
  • allSatisfy: lorsque je veux tous les éléments d'une collection qui répondent à un critère donné.
  • reverseDo: lorsque je veux parcourir la collection à l'envers en partant de la fin vers le début.
  • doWithIndex: lorsque je veux parcourir la collection en ayant un curseur d'index.
  • pairsDo: lorsque je veux parcourir les éléments de la collection 2 à 2.
  • permutationsDo: lorsque je veux parcourir toutes les permutations circulaires possibles des éléments d'une collection.

Il y en a beaucoup des itérateurs et on peut en construire des nouveaux, d'ailleurs.

Itérer sur deux structures

Avec l'itérateur with:do:

#( 1 2 3)
  with: #( 10 20 30 )
  do: [ :x :y | Transcript show: (y*x); cr ]

Ici, je veux parcourir une collection #( 1 2 3) couplée avec une deuxième collection. J'envoie le message with:do: à la première collection. Dans mon bloc do:, j'aurai x et y, 2 paramètres. Le premier x, ça sera un élément de la première collection, et y un élément de la deuxième collection. Donc je vais pouvoir multiplier ces éléments entre eux. On obtient dans le Transcript 10, 40 et 90. Et bien évidemment, il faut absolument que les 2 collections aient la même taille avec cet itérateur-là.

do:separatedBy: introduire des séparateurs

On peut avoir d'autres types de parcours: ici, j'utilise le do: separatedBy:.

String streamContents: [ :s |
  #( 'a', 'b', 'c' ) do: [ :each | s << each ]
    separatedBy: [ ', ' ]
]

J'ai une collection, je vais parcourir chacun des éléments et à chaque fois que j'ai parcouru un élément, je vais évaluer un bloc ici qui correspond à l'affichage d'une virgule.

Ça va me permettre de parcourir le 'a', afficher une virgule, afficher le 'b', afficher une virgule, afficher le 'c'. A chaque fois entre chaque élément, je vais avoir effectué une action.

groupedBy: regrouper des éléments

Ici, j'ai un itérateur qui est groupedBy:, qui me permet de grouper les éléments d'une collection en fonction d'un critère.

#( 1 2 3 4 5 6 7 ) groupedBy: #even 
 
"> an OrderedDictionary(false->#(1 3 5 7) true->#(2 4 6)) "

J'envoie le message à la collection #( 1 2 3 4 5 6 7) et je récupère en résultat un dictionnaire. Donc tous les éléments qui ont répondu “Faux” à ce critère, le critère c'était “#even” (éléments pairs), donc on voit que ça contient bien une collection de tous les éléments impairs. Et tout ce qui a répondu “Vrai”, c'est tous les éléments pairs.

flatCollect: aplatir la collection

Quand on a une collection souvent quand on fait des calculs on a tendance à imbriquer des collections dans des collection, et on se retrouve avec des niveaux d'imbrication qui peuvent être importants.

#( #(1 2) #(3) #(4) #(5 6)) collect: [ :each | each ]
"> #(#(1 2) #(3) #(4) #(5 6)) "
 
#( #(1 2) #(3) #(4) #(5 6)) flatCollect: [ :each | each ]
"> #(1 2 3 4 5 6) "

Ici, vous avez un exemple construit à la main où on a des collections qui sont imbriquées dans des collections. Ce qu'on aimerait c'est arriver à aplatir la collection, à mettre tous les éléments au même niveau.

Pour ça, on a quelque chose de facile en Pharo, on a un itérateur qui s'appelle flatCollect:. C'est-à-dire que je vais parcourir les éléments et construire une nouvelle collection dans laquelle j'ai tout aplati. Donc on obtient bien la collection #(1 2 3 4 5 6) ligne 6 dans laquelle on a enlevé tous les niveaux d'imbrication.

Ouvrir la boîte

Le secret, ce n'est pas de vous présenter tous les itérateurs disponibles dans Pharo, ce serait long et fastidieux. L'idée, c'est vraiment de vous montrer qu'il en existe plein et que vous pouvez découvrir ceux qui vous seront utiles en allant lire les méthodes qui existent sur les classes des collections, en allant découvrir ces méthodes.

Un exemple simple, c'est de commencer par les itérateurs que vous connaissez. Par exemple, se poser la question “Comment est-ce qu'est implémenté le do:?” Je cherche le do: dans la hiérarchie des collections, je vais voir qu'il est implémenté dans “SequenceableCollection” comprenant toutes les collections séquençables.

Donc la méthode do:, elle prend en paramètre un block. Et voici l'implémentation de cette collection.

  1. do: aBlock
  2. "Refer to the comment in Collection|do:."
  3. 1 to: self size do:
  4. [:index | aBlock value: (self at: index)]

Pour chaque élément de la collection (ligne 3) je vais évaluer le block qui est passé en paramètre en lui passant l'élément à l'indice index (ligne 4).

Résumé

Les itérateurs sont extrêmement puissants en Pharo, comme on a pu le voir.

  • Toutes les collections supportent ces itérateurs de façon polymorphique. Du point de vue programmeur, on utilise des itérateurs et puis c'est chacune des classes de collections qui vont les implémenter de façon adéquate par rapport à la collection qu'ils représentent.
  • On peut en définir des nouveaux. C'est extrêmement intéressants: je peux définir mes propres itérateurs si j'en ai envie sur les classes de collections.
  • Il y a une subtilité. Pour ceux qui connaissent le Design pattern iterator, en fait la différence c'est que le développeur ne contrôle pas quand il passe à l'élément suivant. C'est la collection, qui en interne, décide de passer à l'élément suivant. On n'envoie pas explicitement le message “next” à l'itérateur. C'est une subtilité pour ceux qui connaissent le Design pattern iterator.

En résumé, les itérateurs sont vraiment puissants et un allié fort du programmeur qui vont faciliter l'écriture des programmes. On l'a vu, ça permet d'écrire du code concis, simple et élégant et ça permet de garantir l' encapsulation des données au sein d'une collection.

cours/informatique/dev/programmation_objet_pharo/330_iterateurs.txt · Dernière modification : 2022/08/22 13:48 de yoann