#Play2 : Les Iteratees expliqués aux humains... francophones!
Disclaimer : Ce qui suit est la traduction d'un article anglophone paru sur le blog mandubian.com
Vous pouvez retrouver l'article original ici
Vous avez probablement remarqué une nouvelle fonctionnalité intrigante de Play2 nommée Iteratee
(ainsi que ses compagnons Enumerators
et Enumeratee
).
Le but de cet article est d'essayer de rendre le concept d'Iteratee compréhensible pour le plus grand nombre avec des arguments simples, en évitant l'approche mathématique / fonctionnelle.
Cet article ne prétend pas tout expliquer à propos des Iteratee / Enumerator / Enumeratee mais traite plutôt les idées qui se cachent derrière.
##Introduction Dans la documentation de Play2, les Iteratees sont présentés comme un super outil permettant de gérer "réactivement" des flux de données en respectant la généricité, la composabilité tout en étant dans un mode "non bloquant".
Cela parait plutôt sympa non? Mais qu’est ce qu'un Iteratee exactement? Quelle différence existe-t-il entre un Iteratee et la notion plus classique d'Iterator, que vous connaissez certainement? Pourquoi l'utiliser, dans quels cas?
Tout ceci vous semble un peu obscur et complexe?
Si vous êtes fainéant et que vous voulez juste comprendre le minimum vital :
- Un Iteratee est l'abstraction d'une itération asynchrone et non bloquante, sur des chunks (morceaux) de données
- Un Iteratee est capable de consommer des chunks d'un certain type provenant d'un producteur appelé Enumerator
- Un Iteratee peut calculer un résultat de manière progressive en procédant à plusieurs itération successives (ex : calcul d'un total avec incrémentations successives)
- Un Iteratee est un objet thread-safe et immuable qui peut être ré-utilisé depuis plusieurs Enumerators
###Premier conseil : ne cherchez pas "Iteratee" sur Google
Quand vous lancez cette recherche sur Google, vous tombez sur des explications plutôt obscures basées sur une approche purement fonctionnelle ou même parfois sur des théories mathématiques. Même la documentation de Play Framework adopte une approche assez bas niveau et peut s'avérer complexe pour des débutants...
En tant que débutant sur Play2, prendre en main ces concepts peut semble un peu difficile, présentés comme une manière très abstraite de manipuler des chunks de données. Au point de vous faire renoncer à les utiliser. C'est vraiment dommage car les Iteratees sont réellement puissant et proposent un nouveau modèle vraiment intéressant pour manipuler des flux de données dans vos applications web.
Nous allons donc essayer d'appréhender ces concepts sous un angle plus simple. Cet article utilise des exemples de code en Scala mais ils devraient être compréhensibles pour toute personne ayant quelques notions de programmation, et nous essaierons d'éviter d'utiliser trop d'opérateurs "bizarres" à part dans le dernier paragraphe... ><> ><> ><> ><>
Les exemple sont basés sur le futur Play 2.1 (branche master), qui simplifie et rationalise grandement le code des Iteratees. Ne soyez donc pas surpris si le code diffère de l'API de Play 2.0 par moments.
##Quelques rappels sur la notion d'itération Avant de plonger dans l'eau profonde des Iteratees, clarifions le principe d'itération afin de passer progressivement du concept d'Iterator à celui d'Iteratee.
Vous connaissez sûrement les Iterator que l'on trouve dans l'API standard Java. Un Iterator permet de parcourir une collection d'éléments et d'effectuer une action à chaque étape de l'itération. Commençons avec une itération très simple à la sauce Java classique, qui calcule la somme d'une liste d'entiers.
val l = List(1, 234, 455, 987)
var total = 0 // contiendra le total final
var it = l.iterator
while( it.hasNext ) {
total += it.next
}
total
=> resXXX : Int = 1677
Sans surprise, itérer sur une collection signifie :
- obtenir un iterator à partir de la collection
- prendre un élément dans l'iterator (si il y en a)
- effectuer une action : ici ajouter la valeur de l'élément au total
- recommencer, etc. jusqu'à ce qu'il n'y ait plus d'élément à consommer dans l'Iterator
Lorsqu'on itère sur une collection, on manipule :
- L'état de l'itération (est elle terminée? Ceci est naturellement lié au fait qu'il y ait encore des éléments ou non dans l'iterator)
- Le contexte mis à jour d'une étape à l'autre
- Une action qui met à jour le contexte
###Ré-écriture avec une boucle for Scala
for( item <- l ) { total += item }
Cette version est un peu plus simple car on a pas besoin d'utiliser un Iterator.
###Ré-écriture avec une approche fonctionnelle
l.foreach{ item => total += item }
On introduit ici la fonction List.foreach
qui accepte en paramètre une fonctionne anonyme de type (Int => Unit) et itère sur la liste : pour chaque élément de la liste, la fonction est appelée et met à jour le contexte (ici le total).
La fonction anonyme contient l'action à exécuter à chaque étape de la boucle durant l'itération.
##Ré-écriture avec une approche plus générique
La fonction anonyme peut être stockée dans une variable afin d'être réutilisable dès qu'on en aura besoin.
al l = List(1, 234, 455, 987)
val l2 = List(134, 664, 987, 456)
var total = 0
def step(item: Int) = total += item
l foreach step
total = 0
l2 foreach step
Vous avez sûrement envie de dire "C'est sale! Cette fonction a des effets de bord et utilise une variable externe, ce n'est pas un bon design du tout et il faut même remettre le total à 0 avant le deuxième appel!!"
Et vous avez tout à fait raison :
Les fonctions avec effets de bord sont dangereuses car elles changent l'état d'éléments qui lui sont externes. Cet état n'est pas exclusif à la fonction et d'autres entités peuvent y accéder, potentiellement dans d'autres threads. Les fonctions avec effets de bord ne sont pas recommandées si l'on cherche à produire du code propre et robuste. Les langages fonctionnels comme Scala tendent à réduire les effets de bord au strict nécessaire (accès aux fichiers par exemple).
Les variables mutables sont également risquées si votre code est exécuté sur plusieurs threads simultanément : si 2 threads essaient de changer la valeur de la variable, qui gagne? Dans ce cas, on a besoin de synchronisation, ce qui signifie un blocage des threads durant l'écriture dans la variable, et donc le reniement d'une des raisons d'être de Play2 (les web apps non bloquantes).
##Ré-écriture avec une approche immuable, sans effets de bord
f foreach(l: List[Int]) = {
def step(l: List[Int], total: Int): Int = {
l match {
case List() => total
case List(elt) => total + elt
case head :: tail => step(tail, total + head)
}
}
step(l, 0)
}
foreach(l)
Le code est un peu plus long non? Mais vous pouvez remarquer que :
-
la variable
total
a disparu -
la fonction
step
est l'action exécutée à chaque étape de l'itération, mais elle fait quelque chose de plus qu'avant : elle gère l'état de l'itération. Elle fonctionne de cette manière :
- si la liste est vide : retourne le total
- si la liste a un élément
elt
, retourne total + elt - si la liste a un plus d'un élément, appelle
step
avec la queue de la liste (tout sauf le premier élément) et le nouveau totaltotal + head
(head est le premier élément)
Donc à chaque étape de l'itération, selon le résultat de l'étape précédente, step
peut choisir entre 2 états :
- continue l'itération car il reste des éléments
- stoppe l'itération car on a atteint la fin de la liste
##Notez aussi que :
step
est une fonction "tail-recursive" (elle ne dépile pas la pile complète à la fin d'une étape et renvoie un résultat immédiatement)step
transmet le reste des éléments de la liste à traiter et le nouveau total à la prochaine étapestep
retourne le total sans aucun effet de bord
Donc oui, ce code consomme un peu plus de mémoire car il recopie des parties de la liste à chaque étape (mais seulement la référence vers les éléments). Cependant il n'a pas d'effet de bords et utilise uniquement des structures de données immuables. Ceci le rend très robuste et distribuable sans aucun problème.
Notez que vous pouvez écrire ce code de manière bien plus concise et élégante en utilisant les fonctions offertes par les collections Scala :
foldLeft(0){ (total, elt) => total + elt }
###Résumons
Dans cet article, on considère une itération comme la propagation de structures immuables à travers des états. Sur ces bases, itérer implique :
- recevoir de l'information provenant d'une étape précédente : contexte et état
- obtenir les éléments courants ou restants
- propager le nouvel état et contexte à l'étape suivante
##Iterator et Iteratees
Maintenant que nous maîtrisons les itérations, revenons à notre Iteratee
!!
Imaginez que vous voulez généraliser le mécanisme d'itération précédent et être capable d'écrire quelque chose comme :
def sumElements(...) = ...
def prodElements(...) = ...
def printElements(...) = ...
l.iterate(sumElements)
l.iterate(prodElements)
l.iterate(printElements)
Oui avec les API Collection Scala on pourrait faire tout ça facilement mais c'est un exemple :)
###Imaginez maintenant que vous voulez composer une première itération avec une deuxième :
def groupElements(...) = ...
def printElements(...) = ...
l.iterate(groupElements).iterate(printElements)
Imaginez que vous voulez appliquer cette itération sur quelque chose d'autre qu'une collection :
- un flux de données produit progressivement par un fichier, une connexion réseau, une base de données...
- un flux de données produit par un algorithme
- un flux venant d'un producteur de données asynchrone, comme un scheduler ou un acteur
Les Iteratees sont prévus exactement pour ça
Juste pour vous donner un aperçu, voici la manière dont s'écrit l'itération vue plus haut avec un Iteratee :
val enumerator = Enumerator(1, 234, 455, 987)
enumerator.run(
Iteratee.fold(0){ (total, elt) => total + elt }
)
Ok, ça ressemble au code précédent et ça n'a pas l'air de faire grand chose de plus...
Ce n'est pas faux mais croyez moi, c'est beaucoup plus puissant.
Au moins, ça n'a pas l'air trop compliqué, non?
Comme vous pouvez le voir, Iteratee
est utilisé avec un Enumerator
, et ces 2 concepts sont intimement liés.
##><> Les Enumerators ><>
Enumerator est un concept plus générique que les collections ou les tableaux
Jusqu'à présent nous avons utilisé des collections dans nos itérations. Mais comme nous l'avons vu plus haut, nous pourrions itérer sur quelque chose de plus générique, produisant simplement des chunks de données, disponibles immédiatement ou dans le futur de manière asynchrone.
Enumerator
est conçu pour cela.
Quelques exemples simples d'enumerators :
// un enumerator de Strings
val stringEnumerator: Enumerator[String] = Enumerate("alpha", "beta", "gamma")
// un enumerator d'Integers
val integerEnumerator: Enumerator[Int] = Enumerate(123, 456, 789)
// un enumerator de Doubles
val doubleEnumerator: Enumerator[Double] = Enumerate(123.345, 456.543, 789.123)
// un Enumerator prevenant d'un fichier
val fileEnumerator: Enumerator[Array[Byte]] = Enumerator.fromFile("myfile.txt")
// an Enumerator généré par un callback
// ceci génère un String contenant l'heure courante toutes les 500 ms
// remarquez le "Promise.timeout" qui permet d'assurer un mécanisme non bloquant
val dateGenerator: Enumerator[String] = Enumerator.generateM(
play.api.libs.concurrent.Promise.timeout(
Some("current time %s".format((new java.util.Date()))),
500
)
)
Enumerator
est un producteur non bloquant
L'idée derrière Play2 est, comme vous le savez peut être, d'être complètement non bloquant et asynchrone. Enumerator et Iteratee reflètent cette philosophie. L'Enumerator produit des chunks de manière complètement asynchrone et non bloquante. Cela signifie qu'un Enumerator n'est pas fortement couplé à un processus actif ou une tache de fond générant des chunks de données.
Vous vous souvenez de l'extrait de code vu plus haut qui générait des dates? Il reflète parfaitement l'aspect asynchrone et non bloquant des Enumerators/Iteratees.
// un Enumerator généré par un callback
// il génere un String contenant l'heure courante toutes les 500 millisecondes
// remarquez le Promise.timeout qui fournit un mécanisme non bloquant
val dateGenerator: Enumerator[String] = Enumerator.generateM(
play.api.libs.concurrent.Promise.timeout(
Some("current time %s".format((new java.util.Date()))),
500
)
)
###Qu'est ce qu'une Promise?
Ce concept nécessiterait un article complet, mais disons que son nom ("promesse") décrit parfaitement le comportement d'un tel objet.
Une Promise[String]
signifie : "l'objet va retourner un String dans le futur (ou une erreur)", c'est tout. Dans le même temps, ceci ne bloquera pas le thread courant, celui ci est simplement libéré.
Enumerator
a besoin d'un consommateur pour produire des données.
En raison de sa nature non bloquante, si personne ne consomme ces données, l'Enumerator ne bloquera rien et ne consommera pas de ressources cachées.
Mais alors qui consomme les données produites par un Enumerator? Vous l'avez compris par vous même : un Iteratee
##><> Revenons aux Iteratee ><>
Iteratee
est une "chose" générique qui peut itérer sur un Enumerator
Résumons en une phrase : Iteratee est la traduction générique du concept d'itération en programmation fonctionnelle pure. Alors qu'un Iterator est construit à partir d'une collection sur laquelle il itère, un Iteratee est une entité générique qui attend qu'un Enumerator fournisse des données sur lesquelles itérer.
Voyez vous la différence entre un iterator et un Iteratee? Non? Pas de problème... souvenez vous juste de ceci :
- Un Iteratee est une entité générique qui peut itérer sur des chunks de données produits par un Enumerator
- Un Iteratee est créé indépendamment de l'Enumerator sur lequel il va itérer, l'Enumerator lui est fourni
- Un Iteratee est immuable, sans état et complètement réutilisable avec plusieurs Enumerators
C'est pour cela qu'on dit que :
un Iteratee est appliqué sur un Enumerator ou est exécuté à travers un Enumerator
Vous souvenez vous de l'exemple précédent permettant de calculer le total de tous les élément d'un Enumerator[Int] ? Voici le même code montrant qu'un Iteratee peut être créé et réutilisé plusieurs fois sur plusieurs Enumerators :
val iterator = Iteratee.fold(0){ (total, elt) => total + elt }
val e1 = Enumerator(1, 234, 455, 987)
val e2 = Enumerator(345, 123, 476, 187687)
// on applique l'iterator sur l'enumerator
e1(iterator) // ou e1.apply(iterator)
e2(iterator)
// on lance l'iterator à travers l'enumerator pour obtenir un résultat
val result1 = e1.run(iterator) // ou e1 run iterator
val result2 = e2.run(iterator)
Enumerator.apply et Enumerator.run sont assez différents et seront expliqués plus tard
Un Iteratee est un consommateur de données actif. Par défaut, l'Iteratee attend un premier chunk de données et lance ensuite immédiatement le mécanisme d'itération. L'iteratee continue de consommer des données jusqu'à ce qu'il considère avoir terminé le traitement. Une fois qu'il est initialisé, l'Iteratee est entièrement responsable de toute l'itération et décide quand il doit s'arrêter.
// crée l'iteratee
val iterator = Iteratee.fold(0){ (total, elt) => total + elt }
// crée un enumerator
val enumerator = Enumerator(1, 234, 455, 987)
// on injecte l'enumerator dans l'iteratee
// et on pousse le premier chunk dans l'iteratee
enumerator(iterator)
// l'iteratee consomme autant de chunks que nécessaire
// ne vous inquiétez pas du résultat on verra ça plus tard
Comme expliqué plus haut, un Enumerator est un producteur de chunks de données qui attend qu'un consommateur consomme ces chunks. Pour être consommé, l'Enumerator doit être injecté dans un iterator, et plus précisément le premier chunk de données doit être poussé dans l'Iteratee. Naturellement, l'Iteratee est dépendant de la vitesse de production de l'Enumerator : si cette dernière est faible, l'Iteratee sera lent lui aussi.
Notez que la relation Iteratee/Enumerator peut être considérée comme respectant les patterns d'inversion de contrôle et d'injection de dépendance
###Iteratee est une fonction qui traite un 1 chunk
L'Iteratee consomme des chunks un par un jusqu'à ce qu'il considère l'itération terminée. En fait, le scope réel d'un Iteratee est limité au traitement d'un chunk. C'est pour cela qu'il peut être défini comme une fonction capable de consommer un chunk de données.
###Iteratee accepte des chunks typés et calcule un résultat typé
Alors qu'un Iterator itère sur des chunks de données venant de la collection qui l'a créé, un Iteratee est un peu plus ambitieux : il peut effectuer un traitement en même temps qu'il consomme des données.
C'est pour cela que la signature d'Iteratee se présente ainsi :
trait Iteratee[E, +A]
// E est le type de données contenu dans les chunks. Il ne peut donc être appliqué qu'à un Enumerator[E]
// A est le résultat de l'itération
Revenons à notre premier exemple : calculer le total de tous les entiers produits par un Enumerator[Int]*
//création de l'Iteratee
val iterator = Iteratee.fold(0){ (total, elt) => total + elt }
val enumerator = Enumerator(1, 234, 455, 987)
// lance l'Iteratee sur l'enumerator et récupère le résultat
val total: Promise[Int] = enumerator run iterator
Remarquez l'utilisation de run
: vous pouvez voir que le résultat n'est pas le total lui même mais une Promise[Int] du total, car nous sommes dans un mode asynchrone.
Pour récupérer le vrai total, on pourrait utiliser les fonctions Await._
de Scala. Mais ce n'est pas une bonne solution car c'est une API bloquante. Comme Play2 est entièrement asynchrone et non bloquant, la meilleure pratique est de propager la promesse de résultat à l'aide de Promise.map/flatMap
.
Mais avoir un résultat n'est pas obligatoire, par exemple pour afficher tous les chunks consommés :
val enumerator = Enumerator(1, 234, 455, 987)
enumerator(Iteratee.foreach( println _ ))
// ou
enumerator.apply(Iteratee.foreach( println _ ))
Le résultat n'est pas nécessairement un type primitif, il peut être par exemple simplement la concaténation de tous les chunks d'une liste :
val enumerator = Enumerator(1, 234, 455, 987)
val list: Promise[List[Int]] = enumerator run Iteratee.getChunks[Int]
###Un Iteratee peut propager les contextes et états immuables à travers les itérations
Pour être capable de calculer le résultat final, l'Iteratee a besoin de propager les totaux partiels à travers les étapes d'itération. Cela signifie que l'Iteratee est capable de recevoir un contexte (le total précédent par exemple) de l'étape précédente, puis de calculer le nouveau contexte avec le chunk de données suivant (nouveau total = total précédent + élément courant). Il peut enfin propager ce contexte à l'étape suivante (s'il y a besoin d'une étape suivante).
###Iteratee est un simple automate à états
Tout ceci est bien sympathique mais comment l'Iteratee sait-il quand il doit stopper l'itération? Que se passe-t-il si il y a une erreur ou un EOF (end of file), ou si on a atteint la fin de l'Enumerator? Avec ce contexte et les informations sur l'état précédent, il décide quoi faire et calcule potentiellement le nouvel état à envoyer à l'étape suivante.
Maintenant, souvenez vous des états de l'itération "classique" décrite plus haut. Pour un Iteratee, il existe à peu près les mêmes états d'itération :
- L'état
Cont
: l'itération peut continuer avec le prochain chuck et potentiellement calculer un nouveau contexte - L'état
Done
: cet état signale qu'on a atteint la fin du processus et que l'Iteratee peut retourner la valeur de contexte en résultant
et enfin un 3ème état qui vient assez logiquement :
- L'état
Error
: il signale une erreur durant l'étape courante et stoppe l'itération
De ce point vue, on peut considérer les Iteratees comme un simple automate à états finis en charge de boucler sur un état Cont
jusqu'à ce qu'il détecte les conditions pour aller vers les états Done
ou Error
Les états Done/Error/Cont sont eux même des Iteratees.
Souvenez vous, l'Iteratee est défini comme une fonction qui traite 1 chunk et son but principal et de passer d'un état à un autre.
Nous avons 3 "States Iteratees" :
-
Done[E, A](a: A, remaining: Input[E])
a:A
le contexte reçu de l'état précédent -remaining: Input[E]
représente le prochain chunk -
Error[E](msg: String, input: Input[E])
Très simple à comprendre également : un message d'erreur et l'entrée sur laquelle il a échoué -
Cont[E, A](k: Input[E] => Iteratee[E, A])
Cont
est l'état le plus compliqué des 3. Il est construit comme une fonction recevant un Input[E]
et retournant un autre Iteratee[E,A]
. Sans aller trop loin dans la théorie, vous pouvez comprendre facilement que Input[E] => Iteratee[E, A]
est juste un moyen de consommer un input et de retourner un nouvel état/Iteratee qui peut consommer un autre input et retourner un autre état/Iteratee etc. ... jusqu'à ce qu'on tombe sur un état Done
ou Error
.
Cette construction assure le mécanisme de d'approvisionnement de l'Iteratee (dans un mode de fonctionnement typique des langages fonctionnels).
Nous avons vu beaucoup de nouvelles notions, vous vous demandez certainement pourquoi on explique tout ça... C'est simplement parce que si vous comprenez ça, vous allez comprendre comment créer vos propres Iteratees personnalisés.
Écrivons par exemple un Iteratee qui calcule le total des 2 premiers éléments contenus dans un Enumerator[Int] :
def total2Chunks: Iteratee[Int, Int] = {
// `step` est la fonction qui consomme et qui reçoit le contexte précédent (idx, total) ainsi que le chunk courant
// context : (idx, total) idx est l'index pour compter les itérations
def step(idx: Int, total: Int)(i: Input[Int]): Iteratee[Int, Int] = i match {
// chunk vaut EOF ou Empty => on stoppe l'itération en déclenchant l'état Done avec le total courant
case Input.EOF | Input.Empty => Done(total, Input.EOF)
// un chunck a été trouvé
case Input.El(e) =>
// si c'est le 1er ou 2ème chunk, appeler `step` à nouveau en incrémentant idx et en calculant le nouveau total
if(idx < 2) Cont[Int, Int](i => step(idx+1, total + e)(i))
// si on atteind le deuxième chunk on stoppe l'itération
else Done(total, Input.EOF)
}
// démarre l'itération en initialisant le contexte et le premier état (Cont)
(Cont[Int, Int](i => step(0, 0)(i)))
}
// utilisation de l'Iteratee
val promiseTotal = Enumerator(10, 20, 5) run total2Chunks
promiseTotal.map(println _)
=> prints 30
Avec cet exemple, vous avez compris qu'écrire un Iteratee revient à déterminer les actions à effectuer à chaque étape selon le type de chunk reçu, puis à retourner le nouveau état/Iteratee
##Bonus pour ceux qui ne se sont pas encore endormis
Enumerator est juste un helper pour travailler avec les Iteratees
Comme vous avez pu le voir, dans l'API Iteratee, les Enumerators ne sont mentionnés nul part. C'est parce que les Enumerators sont juste une aide pour interagir avec les Iteratees : ils peuvent se brancher dans un Iteratee et injecter le premier chunk de données à l'intérieur. Mais vous n'avez pas besoin d'Enumerator pour utiliser les Iteratees même si c'est vraiment plus facile et bien intégré dans Play2.
###La différence entre Enumerator.apply(Iteratee) et Enumerator.run(Iteratee)
Revenons à ce point évoqué plus tôt. Regardez la signature de l'API Enumerator :
trait Enumerator[E] {
def apply[A](i: Iteratee[E, A]): Promise[Iteratee[E, A]]
...
def run[A](i: Iteratee[E, A]): Promise[A] = |>>>(i)
}
###apply retourne le "Iteratee état"
La fonction apply
injecte l'Enumerator dans l'Iteratee qui consomme les chunks, l'Iteratee fait son travail et on récupère une Promise d'Iteratee. A partir de l'explication précédente, vous pouvez en déduire que l'Iteratee retourné peut être le dernier état après avoir fini de consommer les chunks pris dans l'Enumerator.
run
a 3 étapes :
- Appeler la fonction
apply
vu plus haut - Injecter Input.EOF dans l'Iteratee pour être sur que cela se termine bien
- Retourner le dernier contexte de l'Iteratee sous forme de Promise
Voici un exemple:
val iterator = Iteratee.fold(0){ (total, elt) => total + elt }
val enumerator = Enumerator(1, 234, 455, 987)
// on laisse juste l'iterator consommer tous les chunks mais on n'a pas besoin du résultat pour le moment
val totalIteratee: Promise[Iteratee[Int, Int]] = enumerator apply iterator
// exécute l'iteratee sur l'enumerator et récupère le résultat dans une Promise
val total: Promise[Int] = enumerator run iterator
###Aide mémoire
Lorsque vous avez besoin du résultat d'un Iteratee, utilisez run
Lorsque vous devez appliquer un Iteratee sur un Enumerator sans récupérer le résultat, utilisez apply
Encore une chose à savoir à propos d'un Iteratee est que Un Iteratee équivaut à une Promise[Iteratee] par définition.
// converti une Promise[Iteratee] en Iteratee
val promise[Iteratee] to Iteratee
val p: Promise[Iteratee[E, A]] = ...
val it: Iteratee[E, A] = Iteratee.flatten(p)
// converti un Iteratee en Promise[Iteratee]
// pure promise
val p1: Promise[Iteratee[E, A]] = Promise.pure(it)
// utilisation de unflatten
val p2: Promise[Iteratee[E, A]] = it.unflatten.map( _.it )
// unflatten retourne une structure technique appelée Step qui enrobe les Iteratees avec _.it
Iteratee <=> Promise[Iteratee]
**Cela signifie que vous pouvez construire votre code autour des Iteratees de manière très simple : vous pouvez passer d'un Iteratee à une Promise[Iteratee] ou l'inverse dès que vous le souhaitez.
##Un dernier mot sur la notion d'Enumeratee
Vous avez découvert Iteratee, puis Enumerator...
Et maintenant vous tombez sur cet... Enumeratee
???
Quel est ce nouveau truc en XXXtee
?????
2ème conseil : NE PANIQUEZ PAS… Le concept d'Enumeratee est vraiment facile à comprendre
Enumeratee est un simple adaptateur de type "pipe" entre Enumerator et Iteratee
Imaginez que vous avez un Enumerator[Int] et un Iteratee[String, List[String]]. Vous pouvez transformer un Int en String facilement... Vous devriez donc pouvoir transformer les chunks de Int en chunks de String puis les injecter dans l'Iteratee.
Enumeratee est là pour vous sauver.
val enumerator = Enumerator(123, 345, 456)
val iteratee: Iteratee[String, List[String]] = …
val list: List[String] = enumerator through Enumeratee.map( _.toString ) run iteratee
Que vient on de faire?
Vous avez simplement créé un pipe entre un Enumerator[Int] et un Enumeratee[Int, String] pour envoyer le résultat dans un Iteratee[String, List[String]].
En 2 étapes :
val stringEnumerator: Enumerator[String] = enumerator through Enumeratee.map( _.toString )
val list: List[String] = stringEnumerator run iteratee
Vous comprenez donc qu'Enumeratee est un outil très utile pour convertir vos Enumerators en autre chose, pour être utilisés avec les Iteratees génériques fournis par l'API Play2. Vous verrez que c'est certainement l'outil que vous allez utiliser le plus souvent lorsque vous coderez avec l'API Enumerator / Iteratee.
###Enumeratee peut être appliqué à un Enumerator sans utiliser d'Iteratee
C'est une fonctionnalité très utile des Enumeratees. Vous pouvez transformer un Enumerator[From] en Enumerator[To] avec un Enumeratee[From, To].
La signature d'Enumeratee est assez explicite :
Enumeratee[From, To]
Vous pouvez donc l'utiliser ainsi :
val stringEnumerator: Enumerator[String] = enumerator through Enumeratee.map( _.toString )
###Enumeratee peut transformer un Iteratee
C'est une fonctionnalité un peu plus "étrange", vous pouvez transformer un Iteratee[To, A] en un Iteratee[From, A] avec un Enumeratee[From, To]
val stringIteratee: Iteratee[String, List[String]] = …
val intIteratee: Iteratee[Int, List[String]] = Enumeratee.map[Int, String]( _.toString ) transform stringIteratee
###Enumeratee peut être composé avec un Enumeratee
C'est la dernière fonctionnalité d'Enumeratee que nous verrons :
val enumeratee1: Enumeratee[Type1, Type2] = …
val enumeratee2: Enumeratee[Type2, Type3] = …
val enumeratee3: Enumeratee[Type1, Type3] = enumeratee1 compose enumeratee2
On peut donc créer un Enumeratee générique puis le composer avec un Enumeratee custom qui correspond à ce que dont on a besoin pour utiliser nos Enumerators/Iteratees.
##Conclusion
J'espère vous avoir appris des choses sans avoir perdu personne...
La prochaine étape est d'utiliser les notions Iteratee / Enumerator / Enumeratee ensemble. Comprendre clairement ce qu'est un Iteratee est important car cela aide à écrire ses propres Iteratees, mais vous pouvez aussi garder une approche plus superficielle et utiliser les nombreux helpers de l'API Iteratee de Play2.
En effet, la documentation n'est pas encore aussi complète qu'il le faudrait mais c'est entrain de s'améliorer.
##Finalement, pourquoi devrais-je utiliser Iteratee / Enumerator / Enumeratee ?
L'API Iteratee / Enumerator / Enumeratee n'est pas un jouet inventé pour faire plaisir aux fans de programmation fonctionnelle. Ce sont des outils utiles dans beaucoup de domaines.
Note du traducteur : regardez les helpers de Play2 fournis dans les API Comet ou WebSocket pour vous en convaincre
Quand vous commencerez à être familiers avec eux, je vous promets que vous les utiliserez de plus en plus.
Les applications web modernes ne sont plus simplement des pages générées dynamiquement. Vous manipulez maintenant des flux de données provenant de différentes sources, dans différents formats, avec des délais de disponibilité différents. Vous pouvez avoir à servir d'énormes quantités de données à un très grand nombre de clients, tout en travaillant en environnement distribué.
Les Iteratees sont faits pour ces cas d'utilisation car ils sont thread-safe, immuables et vraiment adaptés pour traiter des flux de données en temps réel. Lâchons le buzzword que vous pouvez voir de plus en plus souvent sur le net : “Realtime WebApp”!
###Note sur les opérateurs "bizarres"
Vous verrez sûrement beaucoup de ces opérateurs dans du code utilisant des Iteratees / Enumerators / Enumeratees comme &>, |>>, |>>> et le fameux opérateur poisson ><>
Ne focalisez pas sur ces opérateurs pour le moment, ce sont juste des alias pour de vrais noms de méthodes plus explicites comme through, apply, applyOn ou compose
.
Avec un peu de pratique, certaines personnes les trouvent plus clairs et plus compacts, d'autres préfèrent utiliser les mots correspondant.
Have fun!
Merci, très bien fait.