13. Itérateurs▲
Les itérateurs sont la version impérative des flux. Comme eux, les itérateurs décrivent des listes potentiellement infinies, mais il n'existe pas de structure de données contenant leurs éléments. Les itérateurs permettent plutôt de progresser dans une séquence à l'aide de deux méthodes abstraites, next et hasNext :
2.
3.
4.
trait
Iterator[+A]
{
def
hasNext: Boolean
def
next: A
}
La méthode next renvoie les éléments successifs de l'itération tandis que la méthode hasNext indique s'il reste des éléments pouvant être renvoyés par next. Les itérateurs disposent également d'autres méthodes que nous présenterons plus loin.
Voici, par exemple, une application qui affiche le carré de chacun des nombres de 1 à 100 :
2.
3.
4.
5.
val
it: Iterator[Int]
=
Iterator.range
(
1
, 100
)
while
(
it.hasNext) {
val
x =
it.next
println
(
x *
x)
}
13-1. Méthodes des itérateurs▲
Outre next et hasNext, les itérateurs ont un grand nombre de méthodes dont la plupart imitent la fonctionnalité correspondante des listes.
append. La méthode append construit un itérateur qui continuera avec l'itérateur indiqué lorsque l'itérateur courant sera terminé.
2.
3.
4.
def
append[B >: A]
(
that: Iterator[B]
): Iterator[B]
=
new
Iterator[B]
{
def
hasNext =
Iterator.this
.hasNext ||
that.hasNext
def
next =
if
(
Iterator.this
.hasNext) Iterator.this
.next else
that.next
}
Les termes Iterator.this.next et Iterator.this.hasNext dans la définition de append appellent les méthodes respectives définies dans la classe Iterator du récepteur. Sans le préfixe Iterator, les appels hasNext et next invoqueraient les méthodes définies dans le résultat de append, ce qui n'est pas ce que l'on recherche.
map, flatMap, foreach. La méthode map construit un itérateur qui renvoie tous les éléments de l'itérateur initial transformés par la fonction f indiquée.
2.
3.
4.
def
map[B]
(
f: A =>
B): Iterator[B]
=
new
Iterator[B]
{
def
hasNext =
Iterator.this
.hasNext
def
next =
f
(
Iterator.this
.next)
}
La méthode flatMap est comme la méthode map, sauf que la fonction de transformation f renvoie un itérateur. Son résultat est l'itérateur produit par la concaténation de tous les itérateurs renvoyés par les appels successifs à f.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
def
flatMap[B]
(
f: A =>
Iterator[B]
): Iterator[B]
=
new
Iterator[B]
{
private
var
cur: Iterator[B]
=
Iterator.empty
def
hasNext: Boolean
=
if
(
cur.hasNext) true
else
if
(
Iterator.this
.hasNext) {
cur =
f
(
Iterator.this
.next); hasNext }
else
false
def
next: B =
if
(
cur.hasNext) cur.next
else
if
(
Iterator.this
.hasNext) {
cur =
f
(
Iterator.this
.next); next }
else
error
(
"next on empty iterator"
)
}
La méthode foreach est étroitement liée à map, car elle applique une fonction donnée à tous les éléments d'un itérateur. Cependant, elle ne construit pas la liste des résultats.
def
foreach
(
f: A =>
Unit
): Unit
=
while
(
hasNext) {
f
(
next) }
filter. La méthode filter construit un itérateur renvoyant tous les éléments de l'itérateur initial qui satisfont le critère donné.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
def
filter
(
p: A =>
Boolean
) =
new
BufferedIterator
[A]
{
private
val
source =
Iterator.this
.buffered
private
def
skip =
{
while
(
source.hasNext &&
!
p
(
source.head)) {
source.next }
}
def
hasNext: Boolean
=
{
skip; source.hasNext }
def
next: A =
{
skip; source.next }
def
head: A =
{
skip; source.head }
}
En fait, filter renvoie des instances d'une sous-classe d'itérateurs « tamponnés ». Un objet BufferedIterator est un itérateur qui a en plus une méthode head qui renvoie l'élément qui aurait été renvoyé par next, mais sans avancer après cet élément. La valeur renvoyée par head sera donc à nouveau renvoyée par le prochain appel à head ou à next. Voici la définition du trait BufferedIterator :
2.
3.
trait
BufferedIterator
[+A]
extends
Iterator[A]
{
def
head: A
}
Les méthodes map, flatMap, filter et foreach existant pour les itérateurs, il s'ensuit que les for en compréhension ou les boucles for leur sont également applicables. Le code qui affiche les carrés des nombres compris entre 1 et 100 pourrait donc s'exprimer de la façon suivante :
for
(
i <-
Iterator.range
(
1
, 100
)) println
(
i *
i)
zip. La méthode zip prend un second itérateur en paramètre et renvoie un itérateur formé des paires d'éléments correspondants renvoyés par les deux itérateurs.
2.
3.
4.
def
zip[B]
(
that: Iterator[B]
) =
new
Iterator[(A, B)]
{
def
hasNext =
Iterator.this
.hasNext &&
that.hasNext
def
next =
(
Iterator.this
.next, that.next)
}
13-2. Construction d'itérateurs▲
Les itérateurs concrets doivent fournir les implémentations des deux méthodes next et hasNext de la classe Iterator. L'itérateur le plus simple est Iterator.empty qui renvoie toujours une séquence vide :
2.
3.
4.
5.
object
Iterator {
object
empty extends
Iterator[Nothing]
{
def
hasNext =
false
def
next =
error
(
"next on empty iterator"
)
}
Un itérateur plus intéressant est celui qui énumère tous les éléments d'un tableau. Il est construit par la méthode fromArray, qui est également définie dans l'objet Iterator :
2.
3.
4.
5.
6.
7.
8.
def
fromArray[A]
(
xs: Array
[A]
) =
new
Iterator[A]
{
private
var
i =
0
def
hasNext: Boolean
=
i <
xs.length
def
next: A =
if
(
i <
xs.length) {
val
x =
xs
(
i); i +=
1
; x }
else
error
(
"next on empty iterator"
)
}
Un autre itérateur énumère un intervalle d'entiers. La fonction Iterator.range renvoie un itérateur qui parcourt un intervalle de valeurs entières :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
object
Iterator {
def
range
(
start: Int
, end: Int
) =
new
Iterator[Int]
{
private
var
current =
start
def
hasNext =
current <
end
def
next =
{
val
r =
current
if
(
current <
end) current +=
1
else
error
(
"end of iterator"
) r
}
}
}
Tous les itérateurs que nous venons de présenter finissent par se terminer. Vous pouvez également définir des itérateurs infinis. L'itérateur suivant, par exemple, renvoie tous les entiers à partir de la valeur value (7) :
2.
3.
4.
5.
def
from
(
start: Int
) =
new
Iterator[Int]
{
private
var
last =
start -
1
def
hasNext =
true
def
next =
{
last +=
1
; last }
}
13-3. Utilisation des itérateurs▲
Voici deux exemples supplémentaires d'utilisation des itérateurs. Le premier affiche tous les éléments d'un tableau xs :
Iterator.fromArray
(
xs) foreach (
x =>
println
(
x))
Ou, avec un for en compréhension :
for
(
x <-
Iterator.fromArray
(
xs)) println
(
x)
Voici un second exemple qui recherche les indices des éléments d'un tableau de double supérieurs à limit :
2.
3.
4.
5.
import
Iterator._
fromArray
(
xs)
.zip
(
from
(
0
))
.filter
(
case
(
x, i) =>
x >
limit)
.map
(
case
(
x, i) =>
i)
Ou, avec un for en compréhension :
2.
3.
import
Iterator._
for
((
x, i) <-
fromArray
(
xs) zip from
(
0
); x >
limit)
yield
i