Continuations pour la programmation de ... - LIRIS - CNRS

L'article est structuré comme suit : la section 2 introduit les continuations, leurs ..... messages asynchrones, pour lesquels une conversation sera constituée d'une série ... formes Bond (Bölöni et al., 2000), Zeus (Nwana et al., 2000), Jade ..... la classe FSMBehavior qui permet de définir une machine à états finis de façon.
361KB taille 6 téléchargements 775 vues
Continuations pour la programmation de comportement d’agent – intégration à la plate-forme Jade Denis Jouvin LIRIS (CNRS UMR 2508), Bâtiment Nautibus, université Claude Bernard Lyon I, 43 boulevard du 11 novembre 1918, 69621 Villeurbanne [email protected] RÉSUMÉ.

Les continuations sont un concept de programmation bien établi permettant de capturer explicitement l’état du programme en cours. Elles sont présentes dans des langages de programmation fonctionnelle (par exemple Scheme), dans le modèle d’acteurs de Hewitt, et depuis peu dans des langages dynamiques (tels que Ruby, Smalltalk, Python, et même Javascript ou Java). Elles ont été historiquement appliquées à la programmation d’automates, aux threads coopératifs, à des techniques de compilation, et ont dernièrement suscité un regain d’intérêt pour la programmation d’applications Web. Cet article montre comment ce concept s’avère particulièrement utile et élégant pour programmer le comportement d’agents (ou leurs composants comportementaux), au point d’en révolutionner l’écriture et la lisibilité. L’approche proposée, appliquée ici à la plate-forme multi-agents Jade, facilite notamment l’implémentation modulaire de protocoles d’interactions, une des difficultés majeures de l’ingénierie d’agents conversationnels. ABSTRACT.

Continuations are a well established programming concept that allows capturing and resuming the current program state. They can be found in several functional programming languages (such as Scheme), in Hewitt actor model, and more recently in dynamic programming languages (such as Ruby, Smalltalk, Python, and even Javascript or Java). They have been historically applied to automaton programming, cooperative threads, compilation techniques, and have lastly raised interest for web application programming. This paper shows how this concept happens to be especially useful and elegant to program agent behaviors (or behavioral components), by increasing code readability and ease of writing. It is shown that the proposed approach, applied here to the Jade multi-agents platform, facilitates the implementation of interaction protocols in a modular way, one of the main difficulties in conversational agent engineering.

MOTS-CLÉS :

continuations, systèmes multi-agents conversationnels, génie logiciel orienté agent, composants comportementaux, automates à base de continuations.

KEYWORDS:

continuations, conversational multi-agent systems, agent oriented software engineering, behavioral component, continuation-based automatons.

1

RIA. Numéro spécial SMA - 2006

2

RIA. Numéro spécial SMA - 2006

1. Introduction Les systèmes multi-agents (SMA) et la programmation orientée agents sont une approche de programmation particulièrement adaptée à modéliser des systèmes complexes. Dans un SMA, les agents interagissent selon des modèles d’interactions plus ou moins élaborés, normalisés, plus riches que dans les modèles de programmation plus classiques, tels que ceux des systèmes distribués orientés objets ou composants. Un des principaux enrichissements est notamment la réification dans les messages, ou dans l’environnement support de l’interaction, d’éléments du contexte local de l’interaction (initiateur, destinataires, identification de la conversation en cours, identification d’une réponse, etc.), auxquels pourra faire référence une sémantique de la communication, apportée par un éventuel langage de communication entre agents (ACL pour Agent Communication Language). Les actes de langages, et la réification du contexte local de l’interaction, permettent notamment de définir des normes ou des protocoles d’interaction, voire de raisonner sur la base de la sémantique associée, selon des modèles cognitifs plus ou moins sophistiqués. Cet enrichissement est un avantage dans le cas de systèmes fortement dynamiques ou adaptatifs, car il permet une plus grande dynamicité et une plus grande flexibilité dans l’organisation du système, tout en préservant la forte autonomie des agents et leur interopérabilité (Jennings et al. 1998) (Luck et al. 2003). Mais il constitue également une source de difficulté importante dans la mise en œuvre d’un SMA (Jouvin, 2000). Pour pallier ces difficultés, de nombreuses plates-formes multi-agents proposent des approches componentielles, afin de maximiser la réutilisation de composants comportementaux abstraits définissant partiellement le comportement de l’agent par rapport à un type de conversation donné. Dans cet article, notre objectif sera de mettre en lumière comment les continuations, un concept de programmation assez ancien mais ayant suscité récemment une certaine attention, peut faciliter considérablement l’écriture de tels composants. Nous montrerons également comment intégrer cette approche à la plate-forme multiagents Jade, à titre d’exemple, et proposerons une implémentation possible deux composants comportementaux abstraits permettant de gérer des conversations bilatérales et multi bilatérales à l’aide de continuations. L’article est structuré comme suit : la section 2 introduit les continuations, leurs variantes et applications courantes. La section 3 identifie les difficultés de mise en œuvre de SMA évoquées plus haut, leur cause, et les réponses apportées dans la littérature. La section 4 présente notre approche par continuations, de l’implémentation d’un automate à la définition d’un composant comportemental d’agent basé sur des continuations. La section 5 présente l’intégration de cette technique à la plate-forme Jade, en détaillant les deux composants abstraits proposés. La section 6 illustre cela d’une expérimentation effectuée avec notamment un protocole d’enchère, accompagnée de mesures de performance. Enfin la section 7 conclut et présente les limites et les perspectives de cette approche.

Continuations pour la programmation d’agents

3

2. Introduction aux continuations Les continuations sont un concept de programmation assez ancien (Strachey et al. 1974), que l’on retrouve notamment dans des langages comme Scheme, ou ML, dans le modèle d’acteurs (Hewitt, 1977), et dans différentes algèbres de processus. Le principe consiste à capturer dans une variable ou un objet manipulable programmatiquement, appelé continuation, l’état du programme en cours, puis à être capable de reprendre son exécution, à partir de cet état, en activant la continuation. Plus précisément, nous désignerons par contexte d’exécution l’état courant ainsi capturé, par analogie avec le contexte d’exécution d’un thread ou d’un processus, et par continuation l’objet ou artefact permettant de réactiver ce contexte. Des définitions informelles des continuations, comme par exemple « le reste du programme », ou encore « un goto avec des paramètres », sont fréquentes dans la littérature. Toutefois, aucune n’est réellement satisfaisante : la première sous-entend une exécution linéaire du programme, qu’il faudrait préalablement « dérouler », et la seconde n’exprime pas le fait que le contexte d’exécution ne se limite pas à la position dans le programme, mais capture aussi la pile d’appel et les variables locales. REMARQUE – Les continuations ont plusieurs points communs avec les threads, mais sont par nature non préemptives. Le contexte d’exécution d’un thread, tout comme celui d’une continuation, nécessite de mémoriser la pile, et la position dans le programme. Une continuation sera généralement plus économe, en ne stockant qu’une petite partie de la pile, et permettra des changements de contexte plus rapides. Bien entendu, les spécificités exactes dépendent des implémentations. 2.1. Variantes Ce concept peut être décliné en différentes variantes ou restrictions, comme les co-routines, ou les générateurs. Un générateur se comporte comme un itérateur, mais s’écrit comme une fonction retournant successivement plusieurs valeurs (Mertz, 2001). Le mot clé yield renvoie une valeur, tout comme return, mais provoque aussi la mémorisation de l’état dans lequel se trouve le générateur, de sorte que la prochaine invocation reprendra à partir de cet état. L’intérêt d’un générateur est que l’état de l’itérateur n’a pas à être géré explicitement par le programmeur, ce qui en simplifie l’écriture : il est alors possible d’utiliser les structures de contrôle naturelles du langage pour gérer les transitions entre les états de l’automate implicite sous-jacent (Mertz, 2002). Une co-routine est une fonction qui mémorise son état courant lorsqu’elle appelle une autre co-routine. Contrairement aux générateurs, une co-routine appelée ne « retourne » pas. Elle doit explicitement appeler une autre co-routine, qui sera alors réactivée, en lui passant éventuellement des paramètres. Les continuations restent la forme la plus générale, comme le montrent (Haynes et al. 1986) et (Allison, 1988). Elles permettent en effet de définir très simplement co-routines ou générateurs, alors que les générateurs sont plus limités.

4

RIA. Numéro spécial SMA - 2006

2.2. Implémentations et exemples A titre illustratif, nous emprunterons des exemples des langages incluant nativement les continuations, comme Scheme ou Ruby, ainsi qu’une restriction simplifiée des continuations : les générateurs de Python. 2.2.1. Générateurs Python La figure 1 présente un exemple de générateur dans la colonne de gauche : en haut la définition du générateur – il s’agit d’une fonction contenant une ou plusieurs instructions yield –, et en dessous une boucle affichant les valeurs retournées itérativement par ce générateur, avec en italique la sortie console du programme. Python supportant les clôtures, ce générateur pourrait aussi référencer une variable du contexte englobant. Exemple simple de générateur Python def simple_gen(max): i = 1 yield "Let's count.." while i < max: yield "Odd %d" % i i = i+1 yield "Even %d" % i i = i+1 yield "The end"

Exécution et sortie console

Equivalent n’utilisant pas de générateur class without_gen: def __init__(self, max): self.isEven = False self.i = 0 self.limit = max + 1 def __iter__(self): return self def next(self): if self.i > self.limit: raise StopIteration() if self.i == 0: r = "Let's count.." elif self.i == self.limit: r = "The end" elif self.isEven: r = "Even %d" % self.i self.isEven = False else: r = "Odd %d" % self.i self.isEven = True self.i = self.i + 1 return r Exécution et sortie console (identique)

>>> for s in simple_gen(4): print s

>>> for s in without_gen(4): print s

Let's count.. Odd 1 Even 2 Odd 3 Even 4 The end

Let's count.. Odd 1 Even 2 Odd 3 Even 4 The end

Figure 1. Exemple de générateur en Python

Continuations pour la programmation d’agents

5

La colonne de droite de la figure 1 montre un programme équivalent en Python, définissant un itérateur mais sans utiliser de générateur. La classe ainsi définie doit alors comporter des attributs stockant l’état de l’itérateur (les attributs isEven et i), et cet état doit être géré explicitement (tests sur les valeurs avec un if … elif …, incrémentation de i en avant dernière ligne, et affectations de isEven), ce qui a pour effet de rendre le code plus difficile à lire et à écrire. 2.2.2. Continuations en Ruby Dans les langages à objets, la capture du contexte d’exécution a pour effet de bord d’interrompre le flot de contrôle, pour le ramener au point d’appel de l’objet exécutable considéré comme « continuable », de la même manière que yield dans un générateur. La continuation est en quelque sorte locale à cet objet exécutable. Dans les langages fonctionnels (et en Ruby), en revanche, une continuation est globale au programme. Elle est créée implicitement par une primitive qui appelle une procédure ou expression lambda, et lui passe la continuation en paramètre. Coroutine par continuations en Ruby def task(c1, name) puts "Step 1 in "+name c2 = callcc{|cc| c1.call(cc)} puts "Step 2 in "+name callcc{|cc| c2.call(cc)} end

Programme équivalent par threads def task(name) puts "Step 1 in "+name Thread.pass puts "Step 2 in "+name Thread.pass end

Exécution et sortie console

Exécution et sortie console

b = lambda{|x| task(x,"bar")} task(b, "foo")

f = Thread.new do task("foo") end b = Thread.new do task("bar") end f.join

Step Step Step Step

Step Step Step Step

1 1 2 2

in in in in

foo bar foo bar

1 1 2 2

in in in in

foo bar foo bar

Figure 2. Exemple de continuation en Ruby La colonne de gauche de la figure 2 présente un programme définissant une coroutine triviale en utilisant les continuations de Ruby, avec en dessous la sortie console en italique. La primitive callcc (pour call with current continuation) appelle la clôture anonyme définie par le bloc entre accolades, ici l’appel à la continuation c1 passée en paramètre de la fonction task, et lui passe la continuation courante en paramètre. L’activation de la continuation c1, c1.call(), a pour effet d’activer l’autre coroutine, qui sera exécutée là où elle avait été capturée, et renverra sa propre continuation, transmise par callcc et affectée à la variable locale c2. NOTE. — Ce programme peut se traduire facilement en Scheme, en utilisant la primitive Scheme call-with-current-continuation à la place de callcc.

6

RIA. Numéro spécial SMA - 2006

La colonne de droite de la figure 2 présente quant à elle un programme équivalent mais utilisant des threads. Cet exemple est simpliste et ne comporte pas de problèmes de synchronisation : l’écriture du code est comparable à la version par continuation. Toutefois dans le cas général l’utilisation de threads apporte certaines difficultés dans la programmation (par exemple de synchronisation sur les zones critiques), et fait l’objet de limites intrinsèques au niveau système (nombre limité de threads, encombrement mémoire, lenteur des changements de contexte). 2.2.3. Autres langages A part Scheme et Ruby, d’autres langages dynamiques répandus supportent nativement les continuations. Citons par exemple Smalltalk, ML, Haskel, Perl version 6, ou encore une implémentation alternative de Python, appelée Stackless Python. Assez récemment, les continuations ont été ajoutées, par extensions ou via de simples librairies, à des langages hôtes comme C, Javascript ou Java, qui ne les supportent pas nativement. Ce type d’extension, lié à des aspects fondamentaux du langage comme la gestion de la pile et le contrôle du flot d’exécution, nécessite soit une manipulation directe de la pile, soit un interpréteur de code modifié, soit des techniques de modification dynamique de classe ou de transformation de code, comme le montrent (Pettyjohn et al., 2005). Nous pouvons citer notamment : − Flowscript, une version modifiée de Rhino, un interpréteur Javascript en Java, comprise dans le framework d’applications web Cocoon1 ; − La librairie C setjmp, avec les fonctions setjmp et longjmp, qui permet d’implémenter des co-routines limitées ; − Le framework RIFE2, qui permet l’usage de continuations en Java, grâce à une technique de modification dynamique de classe ; − en enfin Javaflow3, une librairie expérimentale permettant les continuations en Java, par modification dynamique de classe, que nous utiliserons par la suite. La figure 3 présente une implémentation de générateur Java, utilisant Javaflow, proche des générateurs Python. La fonction de capture de la continuation courante, Continuation.startWith(..), prend en paramètre un objet Runnable. À la différence de Ruby ou Scheme, la continuation représente l’état du Runnable, et non l’état de l’appelant. On notera dans cet exemple qu’il est nécessaire de stocker temporairement les valeurs de retour du générateur en dehors de la méthode run(), ici par l’attribut result, car Javaflow ne véhicule pas de valeur de retour. La figure 4 reprend l’exemple de la figure 1, traduit cette fois en Java, et utilisant la classe Generator introduite figure 3. Il produit les mêmes sorties console. Pour fonctionner, ces deux classes doivent être instrumentées par Javaflow au moment du chargement des classes ou à la compilation. 1 2 3

http://cocoon.apache.org/2.1/ http://rifers.org/ http://jakarta.apache.org/commons/sandbox/javaflow/

Continuations pour la programmation d’agents

7

public abstract class Generator implements Runnable, Iterator, Iterable { /** utilisé pour transférer la valeur de retour */ transient private T result; /** continuation capturant l’état courant */ private Continuation current = Continuation.startWith(this); /** renvoie la prochaine valeur */ public T next() { if (!hasNext()) throw new NoSuchElementException(); T retval = result; current = Continuation.continueWith(current); return retval; } /** joue le rôle du mot-clé yield en python */ protected void yield(T yieldedValue) { result = yieldedValue; Continuation.suspend(); } /** permet d’implémenter {@link Iterable} */ public Iterator iterator() { return this; } /** une continuation nulle signifie la fin du générateur */ public boolean hasNext() { return current != null; }

}

/** pas de sens car pas de collection sous-jacente */ public void remove() { throw new UnsupportedOperationException();}

Figure 3. Implémentation simplifiée des générateurs en Java, utilisant Javaflow public class SimpleGenerator extends Generator { private final int max; public SimpleGenerator(int max) { this.max = max ; }

}

public void run() { yield("let's count.."); for(int i=1; i