Les langages de programmation actuellement utilisés par l'ensemble des développeurs, malgré l'amélioration des paradigmes et autres sucre syntaxiques , restent basés sur un certain nombres de primitives très restreint : Affectation d'une valeur en mémoire, calcul arithmétique sur celle-ci, branchement conditionnel.
Muni de ces axiomes, le code exécuté, même dans des langages fonctionnels, objet et autres, travaille sur les données qu'il possède à l'instant T, manipulant des contenus d'adresses mémoire : il s'agit d'une sémantique opérationnelle.
Les projets informatique grossissent, le nombre de bug et de coût de maintien avec, et c'est guère l'amélioration de sucre syntaxique ou paradigmatique des langages utilisés dans l'industrie, qui permettra de tenir la barre le plus longtemps possible.
Il y a deux ans, votre serviteur avait traduit les interview de Jarold Lanier et Victoria Livschitz , tous deux (à l'époque) chercheurs dans les laboratoires de SUN, critiquant le manque de sémantique des langages.
Jarold Lanier, en particulier, relevait judicieusement que les langages que l'on connait, ne sont que la virtualisation des câbles connectant les blocs logiques d'ordinateur primitifs comme l'ENIAC. Celui-ci préconisait une approche plus basé sur la reconnaissance de forme.
Victoria Livschitz répond à son collègue en déclarant que c'est le manque d'intuivité de la sémantique des langages de programmation modernes qui implique la difficulté de maîtrise des gros projets informatique.
Elle cite aussi l'impossibilité pour une brique logiciel de s'adapter à un nouvel environnement, et partant, de la difficulté de créer des architectures pérennes.
Le problème réside donc dans la sémantique des langages, et moins dans les méthodes de génie logiciel qui sont elles très étudiées.
Il n'y a qu'à voir tout l'engouement des journalistes et architectes pour les technologies SOA ( Source Wikipedia : Service_Oriented_Architecture) et autres méta-trucs.
Le talon d'achille de cette approche est que l'on retombe toujours sur du code classique : ce ne sont que des générateurs de code qu'il faut ensuite faire vivre, maintenir... quand le code généré a la qualité d'être lisible, voire non bogué..
Après quelques réflexions, principalement réalisées dans le cadre du projet Isaac, dont le langage offre des possibilités inédites et réjouissantes, j'en suis venu à vous proposer quelques mécanismes, issu de mon expérience personnelles et surtout professionnelle.
Je me contente de proposer quelques primitives de plus, principalement pensé pour des langages objets, mais probablement adaptable pour d'autres langages. Je ne suis pas à l'abri d'avoir réinventé la poudre, conçue ici et là. Certains mécanismes ne sont que la généralisation de concepts vu ailleurs.
Les utilisateurs de langage orientés aspect reconnaitront peut être des choses faisable avec cette approche, mais un des principes majeur de cette réflexion est d'essayer de rendre la programmation plus intuitive. Cela implique souvent des choix qui peuvent paraitre arbitraire : utiliser une syntaxe de type SQL dans un langage objet est sémantiquement équivalent à d'autres syntaxes plus adapté à la logique objet, mais surement moins intuitives, le SQL se rapprochant du langage naturel.
Il faut donc bien avoir à l'esprit que concevoir des briques pour langages de plus haut niveau est autant de l'informatique théorique que de la psychologie humaine : il faut rapprocher la sémantique d'un langage de l'imagination humaine et de sa psychologie.
Je précise un vocabulaire lisaacien que j'utiliserai dans le texte qui suit :
- Un objet ne doit pas être vu comme un objet d'un langage à classe. Il est cloné et non instancié, il peut changer dynamiquement de parents à l'exécution
- Un slot, est un élément d'un objet. C'est soit une donnée, soit une méthode. En Lisaac, une donnée peut devenir du code, et vice-versa.
- Un block est une fonction, ou peut être encore vu comme une liste d'instructions à évaluation retardée. Un block peut prendre plusieurs valeurs en argument et renvoyer plusieurs autres valeurs. Un bloc s'exécute lorsqu'on appel le message value. Bien évidemment, un block peut être promené un peu partout et exécuté comme on veut. Il peut aussi être transformé en donné (du type de données qu'il est censé renvoyer).
- la syntaxe
Ce code sera exécuté à chaque accès (en lecture ou écriture) sur la variable. C'est une sorte de primitive de type aspect. Nous pensons rajouter une primitive dans le compilateur permettant de n'exécuter du code que lorsque l'on écrit ou lit la variable, ce qui permettra d'éviter des traitement inutile. Pour plus d'information, se référer au manuel d'utilisateur de Lisaac
Commençons par le premier que j'ai pompé chez TrollTech. Vous avez sûrement deviné : le système de Slots/Signaux
Ce concept consiste à permettre à des élément d'interface graphique d'être connecté à la méthode d'un objet quelconque, afin de lui envoyer un message lorsqu'un évènement quelconque relatif audit élément survient.
L'intérêt est qu'on a pas à se trimbaler des références d'objet dans les diverses parties du code. On défini une connexion à l'initialisation, et il suffit ensuite d'écrire le code associé.
Ce modèle pose quelques problèmes dans une logique d'objet à classe, mais beaucoup moins dans une logique d'objet à prototype : en effet, un prototype n'est pas une définition formelle d'un objet qui deviendra vivant à l'instanciation, il est d'ors et déjà vivant dès que le programme début son exécution. Il n'y a donc pas de problème, à définir une connexion entre deux objets qui ne s'échangeront jamais leur référence.
Bien utilisé, cela permet une séparation propre du code selon un pattern MVC, en permettant en plus de threader tous les traitements.
Lisaac étant un langage permettant de rendre et prendre en argument des n-uplet typés, on pourrait imaginer des connecter des slot typés.
Par exemple, si l'on connecte un champ éditable d'interface utilisateur, et que l'on connecte l'évènement "modification du texte" à un slot, on pourra utiliser un signal qui renvoi la chaine du champ texte nouvellement modifié. Le slot connecté doit donc prendre une chaîne en argument.
C'est le compilateur qui fera la vérification de cohérence de type à la compilation.
Le mapping directionnel de données avec filtre
Très souvent dans les logiciels que j'ai eu à écrire, j'ai passé pas mal de temps à écrire de la glue entre éléments d'interfaces, voires objets, et organiser un système de rafraichissement, permettant de synchroniser deux couches d'objet, et autres variables. J'imagine que je suis loin d'être le seul...
Tous ces problèmes reviennent en réalité à définir une équation d'équivalence entre données.
Soit à poser axiomatiquement :
Donnée2 = fonction(Donnée1)
Ce mécanisme consiste donc à définir qu'un objet est toujours égal à un autre, via une relation fonctionnelle.
Cela implique, que toutes modifications de Donnée1 implique un traitement du type Donnée2 = fonction(Donnée1) mettant à jour le contenu de Donnée2, où qu'elle se trouve.
Cela peut être implémenté pratiquement grâce à des langages orientés aspect, comme AspectJ par exemple, où, en Lisaac, grâce à la possibilité de définir un contrat sur un slot, donc une variable.
Ici, on défini que liststr est par définition otherobject.unechaine splité pour le ";". On est obligé (de par la syntaxe actuelle de Lisaac) de définir une valeur "de départ" pour ce slot.
Ce genre de détail à un intérêt majeur dans des interfaces de logiciel gestion, ou il suffira de définir des filtres entre les objets de la vue et du modèle, la vue se mettant à jour toute seule.
Une réflexivité sur les arbres
Lorsqu'on code, on travaille beaucoup sur les arbres, et force est de constater que c'est souvent galère.
Caml nous apporte deux outils fabuleux pour simplifier la difficulté : les types somme et le filtrage de type.
On peut définir un arbre très simplement :
type arbre = Feuille of string | Noeud of arbre;
Un filtrage de type nous aide ensuite à trier
let rec affichearbre = function
Feuille(s) -> print s
|
Noeud (n)-> affichearbre n;;
L'ajout de cette primitive éprouvée dans pas mal de langage aiderait souvent. On pourrait ensuite greffer d'autres mécanismes, existantes dans des logiciels comme CodeWorker.
La gestion du temps
En sémantique opérationnelle, le code exécuté à un instant T travail sur l'état de la mémoire à ce même instant T. Ceci implique que l'on doit en permanence mettre de côté des données, gérer des dépendances diverses et variées afin de simuler une possibilité qui nous semble naturelle pour l'esprit humain : recueillir des informations à partir des évènement de la vie passé, dont on se souvient
Un langage de très haut niveau possède une sémantique permettant de gérer la dimension supplémentaire qu'est le temps. On devrait pouvoir demander au programme de nous donner des informations calculée précédemment, mais sans nécessiter de les mettre de côté.
Prenons un exemple : on doit réaliser un traitement sur des milliers de fichiers XML (une petite manipulation de l'arbre). Ce traitement a la particularité de nécessiter deux passes, car le deuxième traitement, nécessite que le premier soit terminé sur tous les fichiers.
En effet, le document global est éclaté en milliers de fichiers qui s'appellent les uns les autres, on cherche à poser des liens hypertextes dans cette documentation à partir des données contenus dans les documents, puis à vérifier que ces liens sont bijectifs (un lien doit toujours renvoyer vers un document qui permet de revenir au document précédent).
On utilise un objet que l'on va créer pour chaque fichier et que le GC détruira pour nous.
Dans la sémantique du langage, on devrait pouvoir exprimer qu'une information donnée se trouve parmi tous les objets exécutés à tel endroit du code.
Lorsqu'un block de code contenant une boucle a exécuté un traitement sur tous les fichiers, on devrait pouvoir interroger ce block de code sur les liens qui ont été posés.
Pour illustrer certains autres mécanismes, je propose de jouer avec du pseudo-code autour de l'écriture de certaines partie d'un petit jeu de sous-marin : BlueWar est un vieux jeu des années 80 qui consistait à manoeuvrer un sous-marin pour descendre tous les bateaux ennemis qui trainait dans les parages. Ce jeu est sorti sur Amiga, Thomson TO8/TO9, et surement d'autres plateforme !
On disposait de plusieurs vues comme l'écran de commande, avec périscope, carburant disponible, lance torpille ou l'indispensable carte dynamique sur laquelle on voyait se déplacer tous les bateaux.
Vous pourrez avoir une idée de ce qu'était la première version de ce jeu sur Thomson TO8, en regardant quelques screenshot : http://www.logicielsmoto.com/viewsoftware.php?softid=57
Selon un modèle Vue-Agents, ce logiciel comporte
Une vue périscopique avec
- Profondeur
- Charge des batteries, niveau de réservoir
- niveau d'oxygène
- 2 indicateurs de torpille
- Direction
- Voyant avarie
Une vue radar
Une carte marine dynamique
On défini le terrain de jeu :
Agents/sma.li (le sytème multi agent dans son ensemble)
Puis le sous-marin que l'on va commander :
Agents/selfsubmarine.li :
Equation d'Etat
Il arrive très souvent que l'on ait besoin de programmer une machine à état plus ou moins complexe. Ce qui est particulièrement pénible en procédurale, l'est beaucoup moins avec un langage objet. Mais cet exercice reste encore souvent fastidieux. On aimerait pouvoir définir un ensemble d'équations d'état, qui, du moment qu'elle sont cohérentes entre elle (et c'est là qu'on déplacera la difficulté), permet de définir un agent animé et autonome.
J'utilise mon exemple de jeu BlueWar, pour décrire, par l'exemple, ce que pourrai être une définition d'état.
La syntaxe, inspirée du langage Lisaac est totalement fictive et n'est même pas une proposition, prière de ne donc pas la commenter.
On définit une section dans le code où l'on va "poser" les "équations"
La section State ne définie pas des fonctions destinées à être appelées, mais des fonctions d'état fonctionnant à la manière d'un flip-flop : une fois enclenchée un état A, seul l'enclenchement d'un autre état B, avec une équation spécifiant que l'enclenchement B annule l'état de A, arrête cet état A.
Un état n'est donc pas un morceau de code exécuté à un instant T, mais la manière d'être de l'objet/agent, durable.
Je vois donc deux type de code définissable dans un agent :
- Soit l'exécution d'une équation d'état
- Soit l'exécution d'un block selon une équation de temps (que ce soit une fois par seconde ou plus souvent/irrégulièrement). Ce block sera donc réexécuté selon l'équation temporelle. Elle définira l'animation de l'agent.
On a les propriétés suivante pour un état
- Un état est paralellisable
- Un état est démarrable ou stopable arbitrairement.
- Un état est démarrable ou stopable par une équation d'état.
On définie ici une équation entre 3 états :
On a ici défini que le sous-marin soit remonte à la surface, plonge, ou se maintient à la même profondeur (ou exclusif).
Définition des 3 états :
- remonte <-
(
// Code a exécuter à intervals réguliers
TIME.each_second { profondeur := profondeur + 5} until {profondeur := 0};
// Equation d'état
(profondeur = 0) => {
maintient_profondeur.on; // on démarre l'état "maintient_profondeur"
remonte.off; // on stop l'état "remonte"
};
);
La logique d'une définition d'état n'étant pas la même, il faut bien voir qu'on a ici deux définition parallèle, et non successives : toutes les secondes la profondeur du sous-marin va diminuer et l'équation sera régulièrement testé.
L'idéal serait que le compilateur réécrive le code de sorte que l'équation d'état soit exécuté à la suite du block
On revient à du code plus classique où l'on définie les slots d'un objet :
On défini 3 objets, sans leur code :
Agents/vaisseau.li
Agents/vaisseauEnnemi.li hérite de vaisseau
Agents/VaisseauAmi.li hérite de vaisseau
Le radar
View/Radar.li
On définie ici un mapping de données directionnelles, avec filtre :
On notera qu'on a ici utilisé une requête OQL afin sélectionner les vaisseaux se trouvant dans un disque de 100 de rayon autour de mysubmarine
View/Carte.li
L'ensemble des primitives que je propose ici sont assez difficile à exécuter pour un interpréteur et encore plus à compiler, en particulier pour les sémantiques de gestion d'état et d'interrogation de données sur traitement effectués dans le passé.
Cela nécessite une nouvelle race de compilateur doté d'une intelligence artificielle, capable de détecter pas mal d'incohérence dans le code et ainsi de prévenir des code qui s'emballent, boucles, et autres comportement ingérable.
Cela nécessitera aussi d'autres méta-machins, pardon méthode de modélisation et gestion de projets informatique.
L'avenir nous dira si tout cela est possible.
Muni de ces axiomes, le code exécuté, même dans des langages fonctionnels, objet et autres, travaille sur les données qu'il possède à l'instant T, manipulant des contenus d'adresses mémoire : il s'agit d'une sémantique opérationnelle.
Les projets informatique grossissent, le nombre de bug et de coût de maintien avec, et c'est guère l'amélioration de sucre syntaxique ou paradigmatique des langages utilisés dans l'industrie, qui permettra de tenir la barre le plus longtemps possible.
Il y a deux ans, votre serviteur avait traduit les interview de Jarold Lanier et Victoria Livschitz , tous deux (à l'époque) chercheurs dans les laboratoires de SUN, critiquant le manque de sémantique des langages.
Jarold Lanier, en particulier, relevait judicieusement que les langages que l'on connait, ne sont que la virtualisation des câbles connectant les blocs logiques d'ordinateur primitifs comme l'ENIAC. Celui-ci préconisait une approche plus basé sur la reconnaissance de forme.
Victoria Livschitz répond à son collègue en déclarant que c'est le manque d'intuivité de la sémantique des langages de programmation modernes qui implique la difficulté de maîtrise des gros projets informatique.
Elle cite aussi l'impossibilité pour une brique logiciel de s'adapter à un nouvel environnement, et partant, de la difficulté de créer des architectures pérennes.
Le problème réside donc dans la sémantique des langages, et moins dans les méthodes de génie logiciel qui sont elles très étudiées.
Il n'y a qu'à voir tout l'engouement des journalistes et architectes pour les technologies SOA ( Source Wikipedia : Service_Oriented_Architecture) et autres méta-trucs.
Le talon d'achille de cette approche est que l'on retombe toujours sur du code classique : ce ne sont que des générateurs de code qu'il faut ensuite faire vivre, maintenir... quand le code généré a la qualité d'être lisible, voire non bogué..
Après quelques réflexions, principalement réalisées dans le cadre du projet Isaac, dont le langage offre des possibilités inédites et réjouissantes, j'en suis venu à vous proposer quelques mécanismes, issu de mon expérience personnelles et surtout professionnelle.
Je me contente de proposer quelques primitives de plus, principalement pensé pour des langages objets, mais probablement adaptable pour d'autres langages. Je ne suis pas à l'abri d'avoir réinventé la poudre, conçue ici et là. Certains mécanismes ne sont que la généralisation de concepts vu ailleurs.
Les utilisateurs de langage orientés aspect reconnaitront peut être des choses faisable avec cette approche, mais un des principes majeur de cette réflexion est d'essayer de rendre la programmation plus intuitive. Cela implique souvent des choix qui peuvent paraitre arbitraire : utiliser une syntaxe de type SQL dans un langage objet est sémantiquement équivalent à d'autres syntaxes plus adapté à la logique objet, mais surement moins intuitives, le SQL se rapprochant du langage naturel.
Il faut donc bien avoir à l'esprit que concevoir des briques pour langages de plus haut niveau est autant de l'informatique théorique que de la psychologie humaine : il faut rapprocher la sémantique d'un langage de l'imagination humaine et de sa psychologie.
Je précise un vocabulaire lisaacien que j'utiliserai dans le texte qui suit :
- Un objet ne doit pas être vu comme un objet d'un langage à classe. Il est cloné et non instancié, il peut changer dynamiquement de parents à l'exécution
- Un slot, est un élément d'un objet. C'est soit une donnée, soit une méthode. En Lisaac, une donnée peut devenir du code, et vice-versa.
- Un block est une fonction, ou peut être encore vu comme une liste d'instructions à évaluation retardée. Un block peut prendre plusieurs valeurs en argument et renvoyer plusieurs autres valeurs. Un bloc s'exécute lorsqu'on appel le message value. Bien évidemment, un block peut être promené un peu partout et exécuté comme on veut. Il peut aussi être transformé en donné (du type de données qu'il est censé renvoyer).
- la syntaxe
- unevariable : TYPE := valeur_d_init [ code;]; représente une définition de la variable de unevariable de type TYPE, qui a l'initialisation vaudra valeur_d_init, et dont on définira un contrat, dont le code est contenu entre les crochet.Ce code sera exécuté à chaque accès (en lecture ou écriture) sur la variable. C'est une sorte de primitive de type aspect. Nous pensons rajouter une primitive dans le compilateur permettant de n'exécuter du code que lorsque l'on écrit ou lit la variable, ce qui permettra d'éviter des traitement inutile. Pour plus d'information, se référer au manuel d'utilisateur de Lisaac
Commençons par le premier que j'ai pompé chez TrollTech. Vous avez sûrement deviné : le système de Slots/Signaux
Ce concept consiste à permettre à des élément d'interface graphique d'être connecté à la méthode d'un objet quelconque, afin de lui envoyer un message lorsqu'un évènement quelconque relatif audit élément survient.
L'intérêt est qu'on a pas à se trimbaler des références d'objet dans les diverses parties du code. On défini une connexion à l'initialisation, et il suffit ensuite d'écrire le code associé.
Ce modèle pose quelques problèmes dans une logique d'objet à classe, mais beaucoup moins dans une logique d'objet à prototype : en effet, un prototype n'est pas une définition formelle d'un objet qui deviendra vivant à l'instanciation, il est d'ors et déjà vivant dès que le programme début son exécution. Il n'y a donc pas de problème, à définir une connexion entre deux objets qui ne s'échangeront jamais leur référence.
Bien utilisé, cela permet une séparation propre du code selon un pattern MVC, en permettant en plus de threader tous les traitements.
Lisaac étant un langage permettant de rendre et prendre en argument des n-uplet typés, on pourrait imaginer des connecter des slot typés.
Par exemple, si l'on connecte un champ éditable d'interface utilisateur, et que l'on connecte l'évènement "modification du texte" à un slot, on pourra utiliser un signal qui renvoi la chaine du champ texte nouvellement modifié. Le slot connecté doit donc prendre une chaîne en argument.
C'est le compilateur qui fera la vérification de cohérence de type à la compilation.
evenement_changement_texte.connect block_a_executer;Le mapping directionnel de données avec filtre
Très souvent dans les logiciels que j'ai eu à écrire, j'ai passé pas mal de temps à écrire de la glue entre éléments d'interfaces, voires objets, et organiser un système de rafraichissement, permettant de synchroniser deux couches d'objet, et autres variables. J'imagine que je suis loin d'être le seul...
Tous ces problèmes reviennent en réalité à définir une équation d'équivalence entre données.
Soit à poser axiomatiquement :
Donnée2 = fonction(Donnée1)
Ce mécanisme consiste donc à définir qu'un objet est toujours égal à un autre, via une relation fonctionnelle.
Cela implique, que toutes modifications de Donnée1 implique un traitement du type Donnée2 = fonction(Donnée1) mettant à jour le contenu de Donnée2, où qu'elle se trouve.
Cela peut être implémenté pratiquement grâce à des langages orientés aspect, comme AspectJ par exemple, où, en Lisaac, grâce à la possibilité de définir un contrat sur un slot, donc une variable.
liststr : LIST[STRING] := NULL [ Self := otherobject.unechaine.split ";"];Ici, on défini que liststr est par définition otherobject.unechaine splité pour le ";". On est obligé (de par la syntaxe actuelle de Lisaac) de définir une valeur "de départ" pour ce slot.
Ce genre de détail à un intérêt majeur dans des interfaces de logiciel gestion, ou il suffira de définir des filtres entre les objets de la vue et du modèle, la vue se mettant à jour toute seule.
Une réflexivité sur les arbres
Lorsqu'on code, on travaille beaucoup sur les arbres, et force est de constater que c'est souvent galère.
Caml nous apporte deux outils fabuleux pour simplifier la difficulté : les types somme et le filtrage de type.
On peut définir un arbre très simplement :
type arbre = Feuille of string | Noeud of arbre;
Un filtrage de type nous aide ensuite à trier
let rec affichearbre = function
Feuille(s) -> print s
|
Noeud (n)-> affichearbre n;;
L'ajout de cette primitive éprouvée dans pas mal de langage aiderait souvent. On pourrait ensuite greffer d'autres mécanismes, existantes dans des logiciels comme CodeWorker.
La gestion du temps
En sémantique opérationnelle, le code exécuté à un instant T travail sur l'état de la mémoire à ce même instant T. Ceci implique que l'on doit en permanence mettre de côté des données, gérer des dépendances diverses et variées afin de simuler une possibilité qui nous semble naturelle pour l'esprit humain : recueillir des informations à partir des évènement de la vie passé, dont on se souvient
Un langage de très haut niveau possède une sémantique permettant de gérer la dimension supplémentaire qu'est le temps. On devrait pouvoir demander au programme de nous donner des informations calculée précédemment, mais sans nécessiter de les mettre de côté.
Prenons un exemple : on doit réaliser un traitement sur des milliers de fichiers XML (une petite manipulation de l'arbre). Ce traitement a la particularité de nécessiter deux passes, car le deuxième traitement, nécessite que le premier soit terminé sur tous les fichiers.
En effet, le document global est éclaté en milliers de fichiers qui s'appellent les uns les autres, on cherche à poser des liens hypertextes dans cette documentation à partir des données contenus dans les documents, puis à vérifier que ces liens sont bijectifs (un lien doit toujours renvoyer vers un document qui permet de revenir au document précédent).
On utilise un objet que l'on va créer pour chaque fichier et que le GC détruira pour nous.
Dans la sémantique du langage, on devrait pouvoir exprimer qu'une information donnée se trouve parmi tous les objets exécutés à tel endroit du code.
Lorsqu'un block de code contenant une boucle a exécuté un traitement sur tous les fichiers, on devrait pouvoir interroger ce block de code sur les liens qui ont été posés.
Pour illustrer certains autres mécanismes, je propose de jouer avec du pseudo-code autour de l'écriture de certaines partie d'un petit jeu de sous-marin : BlueWar est un vieux jeu des années 80 qui consistait à manoeuvrer un sous-marin pour descendre tous les bateaux ennemis qui trainait dans les parages. Ce jeu est sorti sur Amiga, Thomson TO8/TO9, et surement d'autres plateforme !
On disposait de plusieurs vues comme l'écran de commande, avec périscope, carburant disponible, lance torpille ou l'indispensable carte dynamique sur laquelle on voyait se déplacer tous les bateaux.
Vous pourrez avoir une idée de ce qu'était la première version de ce jeu sur Thomson TO8, en regardant quelques screenshot : http://www.logicielsmoto.com/viewsoftware.php?softid=57
Selon un modèle Vue-Agents, ce logiciel comporte
Une vue périscopique avec
- Profondeur
- Charge des batteries, niveau de réservoir
- niveau d'oxygène
- 2 indicateurs de torpille
- Direction
- Voyant avarie
Une vue radar
Une carte marine dynamique
On défini le terrain de jeu :
Agents/sma.li (le sytème multi agent dans son ensemble)
- mysubmarine : SELFSUBMARINE;
- vaisseau_ami : LIST[VAISSEAUAMI];
- vaisseauennemi : LIST[VAISSEAUENNEMI];
Puis le sous-marin que l'on va commander :
Agents/selfsubmarine.li :
Section Private
- time : TIME;
- profondeur : INTEGER := 0;
- charge_batterie : INTEGER;
- niveau_reservoir : INTEGER := 0 [Self := Self.modulo 300];
- moteur_electrique : BOOLEAN;
- vect_direction : VECT2D [(Self.module_zero > 1).if { warning "Attention, le module du vecteur direction n'est pas à 1 !\n"};];
- torpille_prete_à_lancer1, torpille_prete_à_lancer2,avarie : BOOLEAN;
- niveau_oxygene := 0 [Self := Self.modulo 100];
- vitesse : INTEGER;
- position : VECT2D [TIME.each_second {Self := Self+vitesse*vect_direction};]; // On colle une équation liant position, vitesse et direction
- pression_trop_importante : BOOLEAN := FALSE;
- periscope : BOOLEAN; // périscope utilisable en surface;
Equation d'Etat
Il arrive très souvent que l'on ait besoin de programmer une machine à état plus ou moins complexe. Ce qui est particulièrement pénible en procédurale, l'est beaucoup moins avec un langage objet. Mais cet exercice reste encore souvent fastidieux. On aimerait pouvoir définir un ensemble d'équations d'état, qui, du moment qu'elle sont cohérentes entre elle (et c'est là qu'on déplacera la difficulté), permet de définir un agent animé et autonome.
J'utilise mon exemple de jeu BlueWar, pour décrire, par l'exemple, ce que pourrai être une définition d'état.
La syntaxe, inspirée du langage Lisaac est totalement fictive et n'est même pas une proposition, prière de ne donc pas la commenter.
On définit une section dans le code où l'on va "poser" les "équations"
Section StateLa section State ne définie pas des fonctions destinées à être appelées, mais des fonctions d'état fonctionnant à la manière d'un flip-flop : une fois enclenchée un état A, seul l'enclenchement d'un autre état B, avec une équation spécifiant que l'enclenchement B annule l'état de A, arrête cet état A.
Un état n'est donc pas un morceau de code exécuté à un instant T, mais la manière d'être de l'objet/agent, durable.
Je vois donc deux type de code définissable dans un agent :
- Soit l'exécution d'une équation d'état
- Soit l'exécution d'un block selon une équation de temps (que ce soit une fois par seconde ou plus souvent/irrégulièrement). Ce block sera donc réexécuté selon l'équation temporelle. Elle définira l'animation de l'agent.
On a les propriétés suivante pour un état
- Un état est paralellisable
- Un état est démarrable ou stopable arbitrairement.
- Un état est démarrable ou stopable par une équation d'état.
On définie ici une équation entre 3 états :
- gestionprofondeur : STATE_EQUATION := plonger ^ remonter ^ maintenirprofondeur;On a ici défini que le sous-marin soit remonte à la surface, plonge, ou se maintient à la même profondeur (ou exclusif).
- init <- // appelé à la création
(
TIME.each_minute {
moteur_electrique.if {
charge_batterie := charge_batterie - 2;
} else { // La combustion consomme de l'oxygène
niveau_oxygene := niveau_oxygene - 2;
niveau_oxygene := niveau_reservoir - 2;
};
};
);
Définition des 3 états :
- plonge <-
(
TIME.each_second { profondeur := profondeur - 5;};
{profondeur > 300) => {pression_trop_importante := TRUE; plonge.off;}; // le sous-marin implose à cause de la trop grande pression
);
- remonte <-
(
// Code a exécuter à intervals réguliers
TIME.each_second { profondeur := profondeur + 5} until {profondeur := 0};
// Equation d'état
(profondeur = 0) => {
maintient_profondeur.on; // on démarre l'état "maintient_profondeur"
remonte.off; // on stop l'état "remonte"
};
);
La logique d'une définition d'état n'étant pas la même, il faut bien voir qu'on a ici deux définition parallèle, et non successives : toutes les secondes la profondeur du sous-marin va diminuer et l'équation sera régulièrement testé.
L'idéal serait que le compilateur réécrive le code de sorte que l'équation d'état soit exécuté à la suite du block
{profondeur := 0}, car cela évite des tests régulier.
- maintien_profondeur <-
(
// A la surface on recharge les réserves en oxygène.
(profondeur = 0).if {
niveau_oxygene := 100;
périscope := TRUE;
};
);
On revient à du code plus classique où l'on définie les slots d'un objet :
Section Public
- set_direction nb : INTEGER <- // converti orientation horloge -> vecteur 2D
(
vect_direction.x := (nb*pi/6).cos;
vect_direction.y := (nb*pi/6).sin;
);
- lance_une_torpille <-
(
// ...
);
On défini 3 objets, sans leur code :
Agents/vaisseau.li
Agents/vaisseauEnnemi.li hérite de vaisseau
Agents/VaisseauAmi.li hérite de vaisseau
Le radar
View/Radar.li
On définie ici un mapping de données directionnelles, avec filtre :
radar : LIST[VECT2D] [select position from sma.vaisseau_ennemi, sma.vaisseau_ami where (position.module sma.mysubmarine) < 100;];
On notera qu'on a ici utilisé une requête OQL afin sélectionner les vaisseaux se trouvant dans un disque de 100 de rayon autour de mysubmarine
View/Carte.li
- points_vaisseaux : LIST[VECT2D] [Self := select position from sma.vaisseau_ennemi, sma.vaisseau_ami;];
L'ensemble des primitives que je propose ici sont assez difficile à exécuter pour un interpréteur et encore plus à compiler, en particulier pour les sémantiques de gestion d'état et d'interrogation de données sur traitement effectués dans le passé.
Cela nécessite une nouvelle race de compilateur doté d'une intelligence artificielle, capable de détecter pas mal d'incohérence dans le code et ainsi de prévenir des code qui s'emballent, boucles, et autres comportement ingérable.
Cela nécessitera aussi d'autres méta-machins, pardon méthode de modélisation et gestion de projets informatique.
L'avenir nous dira si tout cela est possible.
> Lire le journal (91 commentaires, moyenne: 1,6).
Vous avez demandé le commentaire #893303.



Plusieurs problèmes avec tes primitves
Le problème principal qu'il y a à interfacer un cerveau humain avec un ordinateur est que l'ordinateur est une machine à calculer alors que le cerveau est une machine à reconnaitre (tellement puissante qu'elle peut même par le jeu de description reconnaitre des choses qu'elle n'a jamais rencontré auparavant). Au jour d'aujourd'hui toute la puissance de la machine réside dans sa bétise intégrale : elle n'interprète absolument pas ce qu'on lui donne et fait ce qu'on a demandé même si c'est idiot. C'est surpuissant car 1°) celà garanti un fonctionnement cohérent 2°) celà permet au programmeur (la plupart du temps équipé d'une machine à reconnaitre) de comprendre ce qui se passe exactement en morcelant le déroulement pour trouver les erreurs.
Les gros inconvennients de tes paradigmes est qu'ils imposent une interpretation non triviale de l'intention du programmeur. Aujourd'hui c'est techniquement infaisable et pour le futur je ne suis pas sur que celà soit souhaitable. Il est extrèmement délicat de méler affectation, variables et contrats pour plusieurs raisons.
La première est que l'ordre d'évaluation change tout, par exemple si je commence par valider mes contrats, puis que j'affecte mes états, puis que j'évalue mes variables rien ne me dit qu'à la fin de l'évaluation des variables mes contrats sont encore valide... De plus si j'évalue mes variables puis que je valide mes contrats rien ne me dit non plus qu'une des validation n'a pas modifié une variable qui invalide un contrat précédamment validé etc. Pour finir variable -> etat -> contrat n'a pas forcément le même résultat que contrat -> etat -> variable.
La seconde est que lorsque les variables, contrat et etats s'inter-affectent il est horriblement complexe (en fait impossible si le langage est Turing complet) de valider qu'il n'y a pas de référence circulaire ou de deadlock
Par exemple : [(soleil = visible), (pluie=non), (température > 20°c)] -> {il fait beau} -> {il fait jour et chaud} -> [(soleil = visible), (pluie=non), (température > 20°c)]
Il est nécessaire qu'une personne décide si le contrat affecte l'état, ou si c'est l'état qui valide le contrat ou encore si ce sont les variables qui valide le contrat qui à son tour affecte l'état. La machine ne peut pas prendre cette décision.
Or là on touche le problème de fond, le troisième problème, demandez à un être humain si c'est parcequ'un nombre est négatif qu'on met un signe moins devant ou si c'est parcequ'il y a un signe moins devant qu'on le considère comme négatif et il risque un peu de faire la tête. En fait l'interpretation est décidée en fonction de l'ordre dans lequel arrive les données. -4 est recconu comme valeur négative de quatre et valeur négative de quatre est transcrite comme -4. Pour recalquer ce comportement à un système informatique il faut éclater la notion de variable en trois notions :
- l'unité
- la donnée
- la valeur.
L'unité pourrait être vue comme un méta-ensemble de types, c'est ce qui fixe le cadre de ce dont on est en train de parler. Un bonne exemple d'unité serait "vitesse du sous-marin A exprimée en km/h et évaluée à l'instant t=T+2min et 30 seconde de jeu"
La donnée est le contenu de l'unité, c'est à dire soit une information arbitraire saisie ou décidée à l'initialisation, soit le résultat d'une suite de calcul ou d'évènement sur cette donnée.
La valeur est la justification de la donnée en d'autre terme ce qui permet de faire la différence entre un -4 que l'on interprère en nombre quatre en négatif et un nombre quatre en négatif que l'on note -4. bref tout ce qui permet de décider mathématiquement de quoi la valeur est une conséquence et quelles seront ses causes et donc ultimement de décider de l'ordre d'évaluation juste.
Bon il est 5h du mat, je suis pas sur d'avoir résussi à être clair...
Kha
root est un privilège, pas un droit !
[^]Re: Plusieurs problèmes avec tes primitves
"Bon il est 5h du mat, je suis pas sur d'avoir résussi à être clair..."
Je confirme. J'ai décroché après l'unité. Si l'unité serait les km/h, je n voix pas ce qu'est la donné ou la valeur. Qui est le "5" de "5 km/h" et dans ce cas que représente l'autre concept ?
[^]Re: Plusieurs problèmes avec tes primitves
Je confirme. J'ai décroché après l'unité. Si l'unité serait les km/h, je n voix pas ce qu'est la donné ou la valeur. Qui est le "5" de "5 km/h" et dans ce cas que représente l'autre concept ?
L'unité c'est la déclaration exaustive de ce à quoi se rattache notre variable. Dans le cas d'une vitesse par exemple ca ne serait pas simplement "km/h" mais "km/h rattaché à tel objet calculé à tel moment".
La donnée c'est le 5 dans ton exemple. C'est la partie la plus facile du trio.
La valeur c'est le contexte. Comment a-t-on trouvé la donnée ? A-t-on fait appel à une méthode de type object.getproperty, est-ce une valeur de référence utilisé en comparaison dans un contrat, est-ce un calcul fait à partir d'éléments publics ou privés de l'objet etc. En fait la valeur contient toutes les informations dont le compilateur et/ou programme va avoir besoin pour déterminer dans quel ordre il doit lancer ses évaluations.
Par exemple une voiture qui accèlère sur une autoroute. Dans cette voiture le conducteur controle régulièrement la vitesse pour être sur de ne pas dépasser la limitation, et si c'est le cas il ralenti jusqu'à ce que la vitesse redescende en dessous de la limitation.
Intuitivement le problème est très simple à poser en terme de contrats/etats/variables :
Contrat principal : respecter la limitation de vitesse
- 1ere Condition de réalisation du contrat : la vitesse constatée par le conducteur est inférieure à la vitesse limite
- 2eme condition de réalisation du contrat : le conducteur vérifie la vitesse fréquament.
Etats
- vitesse inférieure à la limite
- vitesse supérieure à la limite
Variables
- limitation de vitesse
- vitesse du véhicule
Le problème est que ce qui est naturel pour l'humain ne l'est pas pour l'ordinateur. Posé comme celà l'ordinateur n'a aucune façon de savoir si c'est parceque la voiture change de vitesse qu'elle respecte la limitation ou si c'est parcequ'elle respecte la limitation qu'elle change la vitesse. L'ordinateur étant idiot on peut supposer qu'il va évaluer le problème à l'envers et tendre vers l'état stable :
Contrat respecté => vitesse inférieure à la limite => accélération et ne plus en bouger.
Si un évènement entrainne un changement de vitesse limite (le conducteur sort de l'autoroute par exemple) l'ordinateur qui voit une variable changer arbitrairement va alors la rechanger arbitrairement ou changer arbitrairement l'autre de façon à garder la première condition valide. Du point de vue de la logique mathématique le problème est parfaitement résolu, par contre le programmeur risque de faire un peu la gueule en voyant le comportement de son programme.
C'est là que la dichotomie valeur/donnée permet de résoudre le problème.
On cherche à comparer deux unités qui sont
"suite de chiffre inscrit sur un panneau concernant le tronçon de route sur lequel roule l'utilisateur" et "vitesse de la voiture telle que constaté par le conducteur quand il regarde à l'instant t"
Les valeurs de ces deux unités sont
"donnée aribitraire fixé par la direction départementale" et "résultat du calcul fonction de la donnée au temps t-1 et de l'accélération du véhicule "
Les données sont (par exemple) 70 et 85
Avec une sémantique adéquate on peut alors faire comprendre au système que la seule action possible sur la vitesse est un changement de l'acceleration du véhicule. De fait l'ordre de résolution est
Etat impacte variable entrainne contrat.
13h40 en espérant avoir été plus clair.
Kha
root est un privilège, pas un droit !
[^]Re: Plusieurs problèmes avec tes primitves
Tu met le doigt sur un problème que je n'avais pas vu, tout à la définition de mon sous-marin muni de sa cohérence intrinsèque. Je suis heureux de ta réponse, car c'est un peu ce que j'esperais en le publiant ici :-)
En fait, tu relèves que mon modèle ne permet pas de définir un code déterministe.
Pourquoi cette erreur ?
- Parce que je mélange une sémantique axiomatique et une sémantique opérationnelle
- Parce que je n'ai pas vu que si j'utilisais certes mes contrats sur variable pour définir des compréhensions d'ensemble, je n'ai pas vu que sémantiquement on peut tout faire, et surtout n'importe quoi.
Donc que faire ?
J'en ai discuté avec Benoit (Sonntag, l'auteur de Lisaac), qui m'a conseiller de regarder la [[Méthode_B]].
J'en suis donc venu à étudier les possibilités de la méthode B évènementielle qui "colle" à peu près ce que je veux faire.
Mise à part que la syntaxe est absolument immonde (on ferait mieux de lui substituer une syntaxe du type OCL), le B évènementiel permet de définir des variables, définir leur ensemble d'appartenance et l'ensemble des états de l'automate. Le compilateur B permet de vérifier de la cohérence logique du modèle pour ensuite générer le code correspondant.
Le problème est qu'il est impossible de mélanger les deux sémantique, à moins qu'on ai la certitude qu'il n'y ait pas d'interférence entre elles (un cas valide pourrait être le fait que le passage à un état implique un appel d'une fonction graphique, qui ne renvoi rien et ne modifie pas l'état interne de l'automate).
Pour ceux, que ça intéresse, un cours assez clair ici : http://www.lri.fr/~paulin/B/poly002.html
Une perspective envisageable serait d'adapter les primitives de B évènementiel, avec éventuellement quelques sucres syntaxiques pour modéliser les états d'agent comme je les ai introduit ici.
En tout cas, encore merci, et je vais réfléchir à tout cela :-)