JeuWeb - Crée ton jeu par navigateur
Mon apprentissage de Erlang - Version imprimable

+- JeuWeb - Crée ton jeu par navigateur (https://jeuweb.org)
+-- Forum : Discussions, Aide, Ressources... (https://jeuweb.org/forumdisplay.php?fid=38)
+--- Forum : Programmation, infrastructure (https://jeuweb.org/forumdisplay.php?fid=51)
+--- Sujet : Mon apprentissage de Erlang (/showthread.php?tid=5664)

Pages : 1 2 3 4


RE: Mon apprentissage de Erlang - Sephi-Chan - 27-08-2011

En fait, c'est plus proche de la version proposée par Niahoo, avec le case.
Mais au moins, ça a une bonne tronche et ce n'est pas inutilement compliqué, malgré les quelques parasites.


RE: Mon apprentissage de Erlang - srm - 27-08-2011

Les parasites sont du au fait que le langage est typé Smile


RE: Mon apprentissage de Erlang - Sephi-Chan - 26-04-2012

Bon, je me remets un peu sur Erlang ces jours-ci.

J'en profite pour parler du système de processus.
Le processus a l'air d'être l'unité de base de Erlang : un programme est composé de multiples processus qui communiquent entre eux par des messages.

Contrairement à la programmation objet, on n'invoque pas de méthode sur un processus : on lui envoie un message. La différence, c'est qu'un n'implique pas forcément un retour. Si je veux savoir si le processus en face a entendu mon message, il faut qu'il me réponde (en m'envoyant à son tour un message).

Le parallèle avec la vie réelle est assez simple : chaque personne est un processus. Quand on s'adresse à quelqu'un (un processus), on lui envoie un message auquel il répond ou non (parce qu'il n'a pas entendu ou qu'il est mort entre-temps, par exemple). Quand il répond, il envoie à son tour un message.

On peut créer un processus grâce à la fonction spawn.


Character = spawn(character, character, []).

J'utilise ici cette fonction spawn avec 3 arguments, le premier est un atom qui indique le nom du module, le second est un atom qui indique le nom de la fonction, et le troisième est une liste d'arguments à passer à la fonction.

Je crée donc un processus à partir de la fonction character du module character. Je ne lui donne aucun argument. La fonction me retourne un l'identifiant du processus créé (qu'on appelle Pid, pour process identifier) que je garde de côté dans une variable Character, qu'on distingue d'un atom par la majuscule) pour pouvoir lui envoyer des messages.

Voyons une implémentation possible de ce module et de cette fonction.


- module(character).
- export([ character/0 ]).

character() ->
receive
introduce ->
io:format("Received introduce message.~n"),
character();
Any ->
io:format("Received other kind of message. Stop listening.~n"),
void
end.

Ici, character est une fonction qui dispose d'une clause receive, qui va lui permettre de recevoir des messages et qui utilise le pattern matching pour distinguer les messages qu'elle reçoit et les traiter.

En l'état, elle est capable de recevoir deux types de messages : un message contenant seulement l'atom introduce, et n'importe quel autre message (on stock ce message — quel qu'il soit — dans la variable Any, qui aurait pu s'appeler n'importe comment, par ailleurs).

Comme on peut le voir, le premier cas de message affiche à l'écran une chaîne puis appelle la fonction character : elle est donc récursive. Si on ne fait pas ça, le bloc receive se termine et le processus n'est plus capable de recevoir de message.

À l'inverse, c'est exactement le comportement qu'on demande à l'autre forme de message reçu : l'arrêt de la réception.

Je teste maintenant le code dans une console :


3> c(character).
character.erl:20: Warning: variable 'Any' is unused
{ok,character}
4> Character = spawn(character, character, []).
<0.41.0>
5> Character ! introduce.
Received introduce message.
introduce
6> Character ! introduce.
Received introduce message.
introduce
7> Character ! endive.
Received other kind of message. Stop listening.
endive
8> Character ! introduce.
introduce

Comme on peut le voir, quand j'envoie un message contenant l'atom endive, les messages introduce suivant ne sont pas traités. Si l'on n'avait pas créé de règle pour les messages d'une autre forme, le message endive n'aurait pas été traité, mais la réception des messages suivants n'aurait pas été interrompue pour autant (je ne suis pas encore capable d'en expliquer la raison).

Notons que l'utilisation de la fonction io:format sert seulement pour voir facilement ce qui se passe.

Dans le prochain poste, j'expliquerais comment permettre à notre personnage de répondre au message.


RE: Mon apprentissage de Erlang - niahoo - 26-04-2012

(26-04-2012, 02:05 PM)Sephi-Chan a écrit : Comme on peut le voir, quand j'envoie un message contenant l'atom endive, les messages introduce suivant ne sont pas traités. Si l'on n'avait pas créé de règle pour les messages d'une autre forme, le message endive n'aurait pas été traité, mais la réception des messages suivants n'aurait pas été interrompue pour autant (je ne suis pas encore capable d'en expliquer la raison).

Si tu n'avais pas utilisé un catch-all (je te suggère d'ailleurs d'utiliser _ au lieu de Any), le process serait resté bloqué sur receive, en attente d'autres messages, et aurait pu les traiter au fur et à mesure qu'ils arrivent, les messages non reconnus de type endive seraient simplement ignorés jusqu'au prochain appel de receive.

On utilise ça qand on attend des messages avec une priorité. dans le code que je donne, on veut vérifier si la mailbox ne contient aucun message de type important, et s'il n'y en a pas, on consulte les autres messages, un truc du genre (non testé):

Code :
listen(prio) ->
    receive
        {important, Message} ->
            do_something(Message),
            listen(prio)
        after 0 ->
            listen()
    end.
        


listen() ->
    receive
        {_, Message} ->
            do_something(Message),
            listen(prio)
    end.

edit : la clause 'after' ne servait à rien dans la seconde fonction. ainsi c'est plus simple.


RE: Mon apprentissage de Erlang - Sephi-Chan - 26-04-2012

Pour en revenir aux messages entre processus. On va modifier le programme pour que le processus envoie un message en retour après avoir reçu un message d'introduction.


- module(character).
- export([ character/0 ]).

character() ->
receive
{ Sender, introduce } ->
io:format("Received introduce message.~n"),
Sender ! reply,
character();

_Any ->
io:format("Received other kind of message. Stop listening.~n"),
void
end.

Ici, on change le pattern du message : avant on attendait seulement l'atom introduce, maintenant on accepte un tuple composé de quelque chose et de introduce. Ce quelque chose, ça va être le Pid du processus qui a envoyé le message : quand on attend une réponse de quelqu'un, on s'assure de lui donner notre adresse.

Ici, quand l'envoyeur va envoyer un message, il va envoyer un tuple contenant son propre Pid et l'atom introduce. Ainsi, l'autre lui répondra l'atom reply !

D'ailleurs, on peut voir qu'en recevant le message introduce, il va se heurter à la seconde règle du receive et donc arrêter de recevoir des messages ! Smile

Pour régler ça, il faudrait ajouter une nouvelle règle au receive pour gérer la réception d'un message contenant l'atom reply.

Comme ceci :


- module(character).
- export([ character/0 ]).

character() ->
receive
{ Sender, introduce } ->
io:format("~p received introduce message.~n", [self()]),
Sender ! reply,
character();

reply ->
io:format("~p received reply message.~n", [self()]),
character();

_ ->
io:format("~p received other kind of message. Stop listening.~n", [self()]),
void
end.

Ici, j'ai utilisé la fonction self qui retourne le Pid du processus courant.

Pour essayer ce code :


96> Character1 = spawn(character, character, []).
<0.198.0>
97> Character2 = spawn(character, character, []).
<0.200.0>
98> Character1 ! { Character2, introduce }.
<0.198.0> received introduce message from <0.200.0>.
{<0.200.0>,introduce}
<0.200.0> received reply message.
99> Character1 ! { Character2, introduce }.
<0.198.0> received introduce message from <0.200.0>.
{<0.200.0>,introduce}
<0.200.0> received reply message.
100> Character2 ! { Character1, introduce }.
<0.200.0> received introduce message from <0.198.0>.
{<0.198.0>,introduce}
<0.198.0> received reply message.

Et voilà ! Deux personnages sont capables de discuter ensemble ! Smile


RE: Mon apprentissage de Erlang - niahoo - 26-04-2012

héhé mais là vous êtes trois à parler, tu sers d'entremetteur !

Voici un petit exemple de code pour dialoguer avec un character
Code :
- module(character).
- export([ start/1, talk_to/2,character/1,test/0 ]).



start(Name) ->spawn(character, character, [Name]).

talk_to(CharacterPID, Msg) ->
  CharacterPID ! {self(), Msg},
  receive
    {reply, Reply} -> lists:flatten(Reply)
    after 5000 -> noreply
  end.



character(Name) ->
  receive
    { Sender, introduce } ->
      Reply = io_lib:format("Hello ~p, I'm ~s.", [Sender,Name]),
      Sender ! {reply, Reply},
      character(Name);

    { Sender, stop } ->
      Reply = io_lib:format("Bye.",[]),
      Sender ! {reply, Reply};

    { Sender, Msg } ->
      Reply = io_lib:format("You said ~s.", [Msg]),
      Sender ! {reply, Reply},
      character(Name)

  end.

test() ->
  Pid = spawn(character, character, ["BBOy"]),
  talk_to(Pid, introduce).
Code :
54> c(character).                
{ok,character}
55> P = character:start("Sephi").
<0.174.0>
56> character:talk_to(P, introduce).    
"Hello <0.166.0>, I'm Sephi."
57> character:talk_to(P, "bla bla bla").
"You said bla bla bla."
58> character:talk_to(P, stop).        
"Bye."
59> character:talk_to(P, stop).
noreply

la fonction test ne compte pas, c'est juste pour s'éviter de taper dans le shell quand on debug.

On peut voir un truc courant en erlang, la partie client et la partie serveur son codées dans le même module
On encapsule donc tout ce qui concerne le processus, premièrement le 'spawn', ensuite l'envoi de messages, et enfin la réception des messages.

ça permet d'avoir un comportement asynchrone mais en gardant une API qui ressemble à du code classique, synchrone.
Enfin, ça nique un peu la lisibilité du code, mais io_lib:format/2 renvoie des listes imbriquées, la fonction lists:flatten/1 prend une liste et l'applatit comme son nom l'indique

Code :
62> lists:flatten([1,2,[[4,5],6,7,[8,[9]]]]).
[1,2,4,5,6,7,8,9]



RE: Mon apprentissage de Erlang - srm - 27-04-2012

Pour les curieux le code équivalent en Scala est disponible http://www.jeuweb.org/showthread.php?tid=6739&pid=107533#pid107533 Smile


RE: Mon apprentissage de Erlang - Sephi-Chan - 20-10-2012

J'ai fait mes premiers pas avec OTP. Le but est de créer le socle d'une application OTP qu'on peut lancer et qui lance un superviseur principal et tous les processus enfants.

Pour le moment l'application ne fait rien d'autre que démarrer. Le code est sur GitHub.

Derrière le terme d'application OTP se cache en fait 3 composants : un fichier qui décrit l'application, un module qui implémente le comportement application et un autre qui implémente le comportement supervisor.

Le tout est organisé en 2 répertoire : un premier nommé src qui contient le code, et l'autre nommé ebin qui contient le code compilé (et le fichier de description de l'application).

Ce fichier de description contient s'appelle battleship.app, où battleship est le nom de notre application. Il contient un tuple de 3 éléments.


{
application,
battlefleet,
[
{ description, "A game." },
{ vsn, "0.1.0" },
{ modules, [ battlefleet_app, battlefleet_sup ] },
{ registered, [] },
{ applications, [ kernel, stdlib ] },
{ mod, { battlefleet_app, [] } }
]
}.
  • Le premier atome application est fixe.
  • Le second atome reprend le nom de l'application.
  • La liste représente un ensemble de propriétés.
    • Les propriétés description et vsn sont explicites.
    • modules indique les modules utilisés par l'application (pour le moment je n'ai que mon module d'application et son superviseur).
    • registered liste les processus nommés (les autres sont anonymes et ne peuvent pas être appelés directement).
    • applications liste les autres applications nécessaires au bon fonctionnement de la mienne, ici kernel et stdlib (indiquées par commodités, ces deux là sont toujours chargées). Dans un futur proche, je rajouterai probablement le serveur HTTP cowboy qui me permettra de faire du push.
    • Enfin, mod indique le module d'application et les arguments à lui transmettre. La fonction start de ce module sera appelée (en lui transmettant ces arguments, ici une liste vide).


Le fichier suivant est mon module d'application, src/battlefleet_app.erl :


-module(battlefleet_app).
-behaviour(application).
-export([ start/2, stop/1 ]).


start(normal, Args) ->
battlefleet_supConfusedtart_link(Args).


stop(_State) ->
ok.

Il définit donc le module battlefleet_app, qui implémente le comportement application. Il exporte deux fonctions start et stop (En Erlang, les fonctions qui ne sont pas exportées ne peuvent être appelées qu'au sein du module dans lequel elles sont définies).

Comme toujours avec OTP, on fournit essentiellement des modules contenant des fonctions qu'on n'utilise pas directement : c'est OTP qui les invoquera et il faut donc respecter ses exigences en terme d'arguments acceptés et de valeur retournées (cf. celles du comportement application)


La fonction start sert à appeler la fonction start_link de notre superviseur principal (le module battlefleet_sup) avec des arguments donnés (la fameuse liste vide qu'on a renseigné dans notre fichier battleship.app).

N'oubliez pas qu'en Erlang, les mots comme normal ne sont pas des variables (qui commencent par une majuscule) mais des atomes, des valeurs qui servent juste à donner du sens au code grâce au pattern matching. Ici, si on appelle la fonction start de ce module (ce que fera OTP) avec autre chose que l'atome normal en premier argument, la fonction ne sera même pas appelée. Cela nous permet d'écrire différentes implémentation de la fonction selon ce qu'elle reçoit. En quelque sorte, ça évite d'avoir à faire une condition à l'intérieur de la fonction.

En revanche, le second argument est bien transmis à la fonction dans la variable Args.


La fonction stop a pour seul effet de retourner l'atome ok. Notez le underscore qui précède le nom de la variable. Il sert à indiquer au développeur (qui relit le code) que cette variable ne sera pas utilisée. C'est une convention purement sémantique. Le compilateur n'émet pas d'avertissement indiquant que la variable n'etst pas utilisée si elle est préfixée de ce underscore.


Le fichier suivant est mon module d'application, src/battlefleet_sup.erl :


-module(battlefleet_sup).
-behaviour(supervisor).
-export([ start_link/1, init/1 ]).


start_link(Args) ->
supervisorConfusedtart_link({ local, ?MODULE }, ?MODULE, Args).


init(_Args) ->
RestartStrategy = { one_for_one, 1, 60 },
ChildrenSpecification = [],

{ ok, { RestartStrategy, ChildrenSpecification } }.

Le superviseur a pour rôle de… superviser. C'est un processus qui surveille un autre processus et de réagir à son cycle de vie (extinction propre, extinction brutale, etc.). C'est la base d'Erlang en matière de tolérance à l'erreur. L'un des mantra d'Erlang est "Let it crash". En effet, Erlang incite à ne pas coder de manière défensive (ce qui complique considérable le code) et de laisser un processus planter s'il rencontre une erreur (un message innatendu, par exemple) afin qu'il soit relancé dans un état sain par un superviseur.

Ici, le fichier définit le module battlefleet_sup (qu'on a utilisé dans battlefleet_app !), qui implémente le comportement supervisor et exporte les fonctions start_link et init.

En Erlang, les epxression comme ?MODULE sont des macros. En l'occurrence, ?MODULE est une macro native qui représente le nom du module courant. Les macros sont remplacées par leur valeur au moment de la compilation. Ici, la macro ?MODULE sera remplacée par l'atome battlefleet_sup.

La fonction start_link appelle à son tour la fonction start_link du module supervisor (fourni par Erlang) en lui transmettant 3 arguments : un tuple, un nom de module et la liste d'arguments qu'elle a elle-même reçu.

Le premier tuple indique qu'on va nommer localement le processus (à l'échelle d'un nœud Erlang, et pas de tout le cluster éventuel dans le cas d'une application distribuée sur plusieurs machines). On choisit d'utiliser le nom du module comme nom de processus.

Le second argument indique un nom de module. OTP se chargera d'appeler la fonction init de ce module en lui transmettant les arguments Args.


Et il se trouve que — comme par hasard — on définit également cette fonction init ! Celle-ci a pour but de décrire les processus qui seront supervisés par le processus superviseur. Pour cela, elle doit renvoyer un tuple de 2 éléments : l'atome ok et un autre tuple de 2 éléments pour décrire la stratégie de relancement des processus qui plantent et la liste de ces processus.

Ce second tuple va d'abord indiquer les critères de relancement en cas de crash des processus supervisés. On choisit la stratégie de relancement lol (qui consiste à simplement relancer le processus qui plante) avec une On impose au superviseur des limites : le superviseur ne relancera qu'une fois un processus planté toutes les 60 secondes. Le but est bien sûr d'éviter de consommer trop de ressources à relancer un processus qui persiste à planter.

Le second élément du tuple est une liste qui décrit les processus qui seront supervisés. Pour le moment il n'en supervise aucun.



Voilà ! Comme vous pouvez le voir, il y a beaucoup de choses à dire sur ces 16 malheureuses lignes de code !


Ma prochaine étape est de créer un module qui va me permettre de placer des vaisseaux (chacun représenté par un processus Erlang) sur mon plateau de jeu. Je devrais également prévoir un superviseur pour tous les vaisseaux. L'étape d'après sera de pouvoir faire bouger ces vaisseaux puis de faire persister leur état à l'extinction et au redémarrage du serveur.


RE: Mon apprentissage de Erlang - archANJS - 20-10-2012

Très intéressant Smile


RE: Mon apprentissage de Erlang - Sephi-Chan - 21-10-2012

Et bien ! Je ne pensais pas à avoir autant de difficulté pour simplement démarrer un nouveau superviseur (qui supervisera les vaisseaux) en tant qu'enfant du superviseur principal.

https://github.com/Sephi-Chan/battlefleet/tree/c01d6ad284c57b234bd5b42e13e27342e4538af5

J'ai donc modifié le superviseur principal pour qu'il lance à son tour un superviseur de type ship_sup. L'idée étant de créer un arbre de supervision.


-module(battlefleet_sup).
-behaviour(supervisor).
-export([ start_link/1, init/1 ]).

%% API.
start_link(Args) ->
supervisorConfusedtart_link({ local, ?MODULE }, ?MODULE, Args).


%% Callbacks.
init(_Args) ->
RestartStrategy = { one_for_one, 10, 60 },
ShipSup = { ship_sup, { ship_sup, start_link, [] }, permanent, infinity, supervisor, [ ship_sup ] },
{ ok, { RestartStrategy, [ ShipSup ] } }.

Ici, on peut voir que j'ajoute une définition à la liste des processus enfants du superviseur.

Détaillons un peu de quoi est constitué une définition. Déjà, on peut voir que c'est un tuple de 6 éléments.
  • Le premier élément est un identifiant interne du superviseur. Cet identifiant doit être unique et permet ainsi d'éviter qu'un autre enfant similaire soit lancé (a priori, on ne voudrait pas avoir 2 superviseurs identiques) ;
  • Le second est un tuple de 3 éléments :
    • Le nom du module du superviseur ;
    • Le nom de la fonction à exécuter pour lancer le processus ;
    • La liste des arguments d'initialisation du processus. Ici, aucun argument (je le précise car ça m'a bloqué pendant au moins une heure : je pensais que ça transmettait une liste vide) ;
  • Le suivant indique le mode de vie du processus. Celui-ci est un processus permanent qui devra toujours être relancé s'il s'arrête ;
  • Vient ensuite le délai de lancement. Comme c'est un superviseur, on lui laisse tout le temps dont il a besoin ;
  • Enfin vient la liste des modules dépendants. C'est utilisé pour le hot code swapping (la modification du code sans arrêter l'application) ;


Il faut ensuite implémenter le module ship_sup.


-module(ship_sup).
-behaviour(supervisor).
-export([ start_link/0 ]).
-export([ init/1 ]).


start_link() ->
supervisorConfusedtart_link({ local, ?MODULE }, ?MODULE, []).


init(_Args) ->
{ ok, { { one_for_one, 10, 60 }, [] } }.

Il s'agit donc d'un superviseur tout simple qui ne supervise rien pour le moment.

Pendant 1 heure, j'ai donc eu un plantage au démarrage de l'application car je pensais que start_link était appelé avec 1 argument (une liste vide). Alors que OTP l'appelait en fait sans argument.

Plus qu'à créer un module pour créer des vaisseaux, les faire persister et les faire bouger.