Tags: Scala, CPS | July 16, 2010 |
Une des nouveautés de Scala 2.8 est le plugin qui ajoute le support des continuations. Je vous présente un exemple dans la suite de ce post.
L’exemple qui suit est repris depuis ici
import scala.continuations._
import scala.continuations.ControlContext._
object Test {
var producerCont : (Unit => Unit) = null
var consumerCont : (Int => Unit) = null
def produce(i : Int) : Unit @suspendable =
shift {
(k : Unit => Unit) => {
producerCont = k
consumerCont(i)
}
}
def consume : Int @ suspendable =
shift {
(k : Int => Unit) => {
consumerCont = k
if (producerCont != null)
producerCont()
}
}
def main(args: Array[String]) {
reset {
println("Consuming: "+consume)
println("Consuming: "+consume)
println("Consuming: "+consume)
}
reset {
println("Producing: 1")
produce(1)
println("Producing: 2")
produce(2)
println("Producing: 3")
produce(3)
}
}
}
Ce code affiche
Essayons de comprendre pourquoi. Le premier code exécuté (la fonction “main”) est :
reset {
println("Consuming: "+consume)
println("Consuming: "+consume)
println("Consuming: "+consume)
}
Ce qui est équivalent à :
reset {
val a = consume
println("Consuming: "+a)
println("Consuming: "+consume)
println("Consuming: "+consume)
}
Ce qui est équivalent à :
reset {
val a = shift {
(k : Int => Unit) => {
consumerCont = k
if (producerCont != null)
producerCont()
}
}
println("Consuming: "+a)
println("Consuming: "+consume)
println("Consuming: "+consume)
}
Rappelons qu’un “shift” ne peut être rencontré que dans le contexte d’un “reset”. Lorsqu’un shift est rencontré, une fonction est créée de façon à ce qu’un argument remplace l’appel à “shift” :
def myCont1(arg: ?): Unit = {
val a = arg
println("Consuming: "+a)
println("Consuming: "+consume)
println("Consuming: "+consume)
}
Si le type de retour de la fonction peut être inféré de façon classique (“println” donc “Unit”), c’est plus compliqué pour le type de l’argument “arg”. Or cette fonction que j’ai appelée “myCont1” va être passée comme argument à la fonction contenue dans le “shift” qui prend en argument “k : Int => Unit”. Donc le type de “arg” est “Int”.
Maintenant ce qui se passe est simple. La fonction contenue dans “shift” est appelée avec en argument la fonction “myCont1”. Si on regarde le code :
On s’aperçoit que l’argument “k” qui référence “myCont1” est sauvegardé mais jamais utilisé. Comme “producerCont” est null, il ne se passe rien d’autre. Après ça, le code exécuté est celui situé après le “reset”, c’est-à-dire l’autre “reset” :
reset {
println("Producing: 1")
produce(1)
println("Producing: 2")
produce(2)
println("Producing: 3")
produce(3)
}
Le premier “println” affiche “Producing: 1”. Ensuite, la fonction “produce” est appelée. Si on la remplace, comme précédemment, par sa définition, on obtient la fonction suivante :
def myCont2(arg: Unit) : Unit = {
// Déjà fait : println("Producing: 1")
arg // Cet argument ne sert à rien car il ne fait rien (type Unit)
println("Producing: 2")
produce(2)
println("Producing: 3")
produce(3)
}
Cette fonction est passée à la fonction contenue dans le “shift” défini dans la fonction “produce”. Si on regarde le code :
On constate que la fonction “myCont2” est sauvegardée dans “producerCont”. Puis on appelle la fonction “consumerCont”. Rappelez-vous, nous avions sauvegardé dans “consumerCont” la fonction “myCont1”. C’est donc cette dernière qui est exécutée avec pour argument “i” qui est égal à 1. Si on regarde le code de “myCont1”, on voit que “println” est appelé pour afficher “Consuming: 1”.
Le reste de “myCont1” s’exécute et un nouvel appel à “consume” est effectué. Comme précédemment, une fonction “myCont3” sera créée. Elle contiendra la suite de “myCont1”, c’est-à-dire :
def myCont3(arg: Int): Unit = {
// Déjà fait : println("Consuming: "+consume)
val a = arg
println("Consuming: "+a)
println("Consuming: "+consume)
}
Cette fois-ci cependant, “producerCont” ne sera pas “null” puisqu’elle référence “myCont2”. “myCont2” sera donc appelée, permettant ainsi la production d’une nouvelle valeur (“produce(2)”) qui elle-même appellera “consumerCont” qui consommera une valeur (“consume”) et qui elle-même appellera “producerCont”, permettant ainsi la production d’une nouvelle valeur, etc. La condition d’arrêt est quand “producerCont” est appelée alors que toutes les valeurs ont été produites :
def myContN() : Unit = {
// Déjà fait : println("Producing: 1")
// Déjà fait : produce(1)
// Déjà fait : println("Producing: 2")
// Déjà fait : produce(2)
// Déjà fait : println("Producing: 3")
// Déjà fait : produce(3)
}
Arrivé à ce stade, la suite de la fonction “main” est exécutée et le programme se termine.
Voilà, j’espère que ce petit aperçu vous a montré la puissance de cette construction syntaxique. On a ainsi implémenté des co-routines très facilement.