Un serpent dans la boîte aux lettres et qu'est-ce que F #

De quoi s'agit-il?


Tout tourne autour du serpent. Tout le monde se souvient de ce qu'est un serpent: un serpent se déplace sur un champ rectangulaire. Trouve de la nourriture - grandit en longueur, se retrouve lui-même ou au bord du champ - meurt. Et l'utilisateur ne peut envoyer que des commandes: gauche, droite, haut, bas.
J'ai décidé d'ajouter une action ici et de faire fuir le serpent du pacman. Et tout cela sur les acteurs!


Par conséquent, aujourd'hui, j'utiliserai l'exemple d'un serpent pour parler de la façon de construire un modèle d'acteur à l'aide de MailboxProcessor partir de la bibliothèque standard, des points à rechercher et des pièges auxquels vous pouvez vous attendre.


Le code écrit ici n'est pas parfait, peut violer certains principes et peut être mieux écrit. Mais si vous êtes débutant et que vous souhaitez gérer les boîtes aux lettres, j'espère que cet article vous aidera.
Si vous savez tout sur les boîtes aux lettres sans moi, vous pourriez vous ennuyer ici.


Pourquoi des acteurs?


Par souci de pratique. J'ai lu sur le modèle des acteurs, regardé la vidéo, j'ai tout aimé, mais je ne l'ai pas essayé moi-même. Maintenant je l'ai essayé.
Malgré le fait que j'ai choisi la technologie pour la technologie, le concept est tombé avec succès sur cette tâche.


Pourquoi MailboxProcessor, et non, par exemple, Akka.net?


Pour ma tâche, le MailboxProcessor est de la station orbitale par moineaux, MailboxProcessor beaucoup plus simple, et il fait partie de la bibliothèque standard, vous n'avez donc pas besoin de connecter de packages.


À propos des processeurs de boîtes aux lettres et des passe-partout associés


Le point est simple. La boîte aux lettres à l'intérieur a une boucle de message et un état. Votre boucle de message mettra à jour cet état en fonction de l'arrivée du nouveau message.


 let actor = MailboxProcessor.Start(fun inbox -> // ,    //   . inbox --    MailboxProcessor let rec messageLoop oldState = async { //   let! msg = inbox.Receive() //    let newState = updateState oldState msg //      return! messageLoop newState } //       .    --     messageLoop (0,0) ) 

Veuillez noter que messageLoop récursif, et à la fin il doit être rappelé, sinon un seul message sera traité, après quoi cet acteur mourra. messageLoop également asynchrone et chaque itération suivante est effectuée lorsqu'un nouveau message est reçu: let! msg = inbox.Receive() let! msg = inbox.Receive() .
Ainsi, la charge logique entière va à la fonction updateState , ce qui signifie que pour créer la boîte aux lettres du processeur, nous pouvons créer une fonction constructeur qui accepte une fonction de mise à jour d'état et un état zéro:


 //   applyMessage       //      (fun inbox -> ...) let buildActor applyMessage zeroState = MailboxProcessor.Start(fun inbox -> let rec loop state = async{ let! msg = inbox.Receive() let newState = applyMessage state msg return! loop newState } loop zeroState ) 

Cool! Maintenant, nous n'avons pas besoin de surveiller en permanence pour ne pas oublier le return! loop newState return! loop newState . Comme vous le savez, un acteur stocke un état, mais maintenant il n'est pas du tout clair comment obtenir cet état de l'extérieur. La boîte aux lettres du processeur a une méthode PostAndReply , qui prend la fonction AsyncReplyChannel<'Reply> -> 'Msg comme entrée. Au début, cela m'a conduit dans une stupeur - on ne sait pas vraiment d'où obtenir cette fonction. Mais en réalité, tout s'est avéré plus simple: tous les messages doivent être emballés dans un wrapper DU, car nous obtenons maintenant 2 opérations sur notre acteur: envoyer le message lui-même et demander l'état actuel. Voici à quoi ça ressemble:


 //     . // Mail<_,_>   ,  Post & Get --  . // F#       , //   compare & equals . //         --   . //   [<Struct>] .       type Mail<'msg, 'state> = | Post of 'msg | Get of AsyncReplyChannel<'state> 

Notre fonction constructeur ressemble maintenant à ceci:


 let buildActor applyMessage zeroState = MailboxProcessor.Start(fun inbox -> let rec loop state = async{ let! msg = inbox.Receive() //    ,     // .    -- ,     //     . //     --      //    .      ! match msg with | Post msg -> let newState = applyMessage state msg return! loop newState | Get channel -> channel.Reply state return! loop state } loop zeroState ) 

Maintenant, pour travailler avec la boîte aux lettres, nous devons Mail.Post tous nos messages dans ce Mail.Post . Afin de ne pas écrire cela à chaque fois, il est préférable de l'envelopper dans une petite application:


 module Mailbox = let buildAgent applyMessage zeroState = MailboxProcessor.Start(fun inbox -> let rec loop state = async{ let! msg = inbox.Receive() match msg with | Post msg -> let newState = applyMessage state msg return! loop newState | Get channel -> channel.Reply state return! loop state } loop zeroState ) let post (agent: MailboxProcessor<_>) msg = Post msg |> agent.Post let getState (agent: MailboxProcessor<_>) = agent.PostAndReply Get let getStateAsync (agent: MailboxProcessor<_>) = agent.PostAndAsyncReply Get //   Single Case Discriminated Union. // MailboxProcessor   API.      , //  -  ,  ,      //   .       ,     //        . type MailAgent<'msg, 'state> = MailAgent of address:string * mailbox:MailboxProcessor<Mail<'msg, 'state>> //     API with member this.Post msg = //            let (MailAgent (address,this)) = this Mailbox.post this msg member this.GetState() = let (MailAgent (address,this)) = this Mailbox.getState this member this.GetStateAsync() = let (MailAgent (address,this)) = this Mailbox.getStateAsync this member this.Address = let (MailAgent (address, _)) = this address member this.Dispose() = let (MailAgent (_, this)) = this (this:>IDisposable).Dispose() interface IDisposable with member this.Dispose() = this.Dispose() 

Je vais vous dire quelle address:string peu plus tard, mais pour l'instant notre passe-partout est prêt.


En fait, le serpent


Dans le serpent, il y a un serpent, un utilisateur avec ses commandes, un champ et une transition régulière vers l'image suivante.
Voici tout cela ensemble et doit être taché par nos acteurs.
Ma disposition initiale était la suivante:


  • Acteur avec une minuterie. Accepte les messages de démarrage / arrêt / pause. Toutes les n millisecondes, envoie un message de Flush à l'acteur de Flush . Stocke System.Timers.Timer tant qu'état
  • Équipes d'acteurs. Reçoit les messages de l'utilisateur Move Up/Down/Left/Right , AddPerk Speed/Attack (oui, mon serpent peut rapidement ramper et attaquer les méchants) et Flush depuis la minuterie. Il stocke une liste de commandes en tant qu'état, et avec un vidage, cette liste se réinitialise.
  • L'acteur est un serpent. Il stocke l'état du serpent - avantages, longueur, direction, virages et coordonnées.
    Il accepte une liste de messages de l'acteur des commandes, un message Tick (pour déplacer la cellule du serpent 1 vers l'avant) et un message GrowUp de l'acteur du champ lorsqu'il trouve de la nourriture.
  • Acteur de terrain. Il stocke une carte de cellules, prend l'état d'un serpent dans un message et dessine des coordonnées sur une image existante. Il envoie également GrowUp acteur GrowUp serpent et la commande Stop au chronomètre si le jeu est terminé.

Comme vous pouvez le voir, même avec un si petit nombre d'entités, la mappe de messages est déjà non triviale. Et déjà à ce stade des difficultés sont apparues: le fait est que par défaut F # ne permet pas les dépendances cycliques. Dans la ligne de code actuelle, vous ne pouvez utiliser que le code écrit ci-dessus, et il en va de même pour les fichiers du projet. Ce n'est pas un bug, mais une fonctionnalité, et je l'aime beaucoup, car cela aide à garder le code propre, mais que faire lorsque des liens cycliques sont nécessaires par conception? Bien sûr, vous pouvez utiliser l' rec namespace - puis à l'intérieur d'un fichier, vous pouvez vous référer à tout ce qui se trouve dans ce fichier, que j'ai utilisé.
Le code devrait gâcher, mais il semblait être la seule option. Et ça a marché.


Le problème du monde extérieur


Tout fonctionnait aussi longtemps que tout le système d'acteurs était isolé du monde extérieur, et je ne faisais qu'effondrer et afficher des lignes dans la console. Quand est venu le temps d'implémenter la dépendance sous la forme de la fonction updateUI , qui était censée être redessinée pour chaque tick, je n'ai pas pu résoudre ce problème dans l'implémentation actuelle. Ni laid ni beau - pas question. Et puis je me suis souvenu de akku - après tout, vous pouvez générer des acteurs en cours de route, et tous mes acteurs sont décrits au stade de la compilation.
La solution est évidente - utilisez akku! Non, bien sûr, Akka est toujours exagéré, mais j'ai décidé de lécher certains points à partir de là - à savoir, pour créer un système d'acteurs dans lequel vous pouvez ajouter dynamiquement de nouveaux acteurs et interroger les acteurs existants à l'adresse.
Étant donné que les acteurs sont désormais ajoutés et supprimés lors de l'exécution, mais obtenus par l'adresse plutôt que par le lien direct, vous devez fournir un scénario où l'adresse ne semble nulle part et l'acteur n'est pas là. En suivant l'exemple du même acca, j'ai ajouté une boîte pour les lettres mortes, et je l'ai conçue à travers mes DU préférés:


 // Agent<_,_> --  ,   ,     //     ,      . //      ,    --    Box (mailagent), // ,       ,   ,   , //       Deadbox.      MailAgent,  . //           . //    --    . type Agent<'message,'state> = | Box of MailAgent<'message,'state> | DeadBox of string * MailAgent<string * obj, Map<string,obj list>> with member this.Post msg = match this with | Box box -> box.Post msg | DeadBox (address, deadbox) -> (address, box msg) |> deadbox.Post interface IDisposable with member this.Dispose() = match this with | Box agent -> agent.Dispose() | DeadBox (_,agent) -> agent.Dispose() 

Et le système lui-même ressemble à ceci:


 //    .     --    . type MailboxNetwork() as this = //      .   ! [<DefaultValue>] val mutable agentRegister: ConcurrentDictionary<string, obj> //       do this.agentRegister <- ConcurrentDictionary<string, obj>() //      , //    Map --     let deadLettersFn deadLetters (address:string, msg:obj) = printfn "Deadletter: %s-%A" address msg match Map.tryFind address deadLetters with //   | None -> Map.add address [msg] deadLetters //   --   | Some letters -> //  --      Map.remove address deadLetters |> Map.add address (msg::letters) let deadLettersAgent() = ("deadLetters", Map.empty |> Mailbox.buildAgent deadLettersFn) |> MailAgent member this.DeadLetters = deadLettersAgent() // -     ,      member this.Box<'message,'state>(address) = match this.agentRegister.TryGetValue address with | (true, agent) when (agent :? MailAgent<'message,'state>) -> // ,       ,   let agent = agent :?> MailAgent<'message, 'state> Box agent | _ -> DeadBox (address, this.DeadLetters) //  --    member this.KillBox address = this.agentRegister.TryRemove(address) |> ignore member this.RespawnBox (agent: MailAgent<'a,'b>) = this.KillBox agent.Address this.agentRegister.TryAdd (agent.Address, agent) |> ignore interface IDisposable with member this.Dispose() = for agent in this.agentRegister.Values do match agent with | :? IDisposable as agent -> agent.Dispose() | _ -> () 

C'est là que la même address:string , à propos de laquelle j'ai écrit ci-dessus, a été utile. Et encore une fois, cela a fonctionné, la dépendance externe était maintenant facile à obtenir là où vous en avez besoin. Les fonctions constructeurs des acteurs acceptent désormais le système d'acteurs comme arguments et en obtiennent les adresses nécessaires:


  //    - (  )   - let gameAgent (mailboxNetwork: MailboxNetwork) = mailboxNetwork.Box<Command list, GameState>(gameAddress) //    message loop           let commandAgentFn (mailboxNetwork: MailboxNetwork) commands msg = let gameAgent = gameAgent mailboxNetwork match msg with | Cmd cmd -> cmd::commands | Flush -> commands |> gameAgent.Post [] 

Lentement


Pour des raisons évidentes, lors du débogage, j'ai réglé le jeu à faible vitesse: le délai entre les ticks était de plus de 500 millisecondes. Si vous réduisez le retard à 200, les messages ont commencé à arriver en retard et les équipes de l'utilisateur ont travaillé avec un retard, ce qui a gâché tout le jeu. Une mouche supplémentaire dans la pommade était le fait que la minuterie recevait la commande d'arrêt en cas de perte à plusieurs reprises. Pour l'utilisateur, cela n'apparaissait en aucune façon, mais néanmoins, il y avait une sorte de bogue.
La vérité désagréable était que les acteurs sont, bien sûr, très bien, mais les appels de méthode directs sont beaucoup plus rapides. Par conséquent, malgré le fait que le stockage du serpent lui-même dans un acteur séparé était pratique du point de vue de l'organisation du code, j'ai dû abandonner cette idée au nom de la vitesse, car pour 1 heure du jeu, la messagerie était trop intense:


  1. L'utilisateur envoie un nombre arbitraire de commandes directement à l'acteur de commande.
  2. La minuterie envoie une tique à l'acteur de l'équipe et, dans une mise en œuvre précoce, également à l'acteur de serpent afin qu'il déplace le serpent vers la cellule suivante
  3. L'acteur de commande envoie une liste de commandes pour le serpent lorsque le message correspondant provient du temporisateur.
  4. L'acteur serpent, ayant mis à jour son état selon les 2 messages supérieurs, envoie l'état à l'acteur de terrain.
  5. L'acteur de terrain redessine tout. Si le serpent a trouvé de la nourriture, il envoie un message GrowUp à l'acteur de serpent, après quoi il renvoie le nouvel état à l'acteur de terrain.

Et pour tout cela, il y a 1 cycle d'horloge, ce qui n'est pas suffisant, compte tenu de la synchronisation dans les entrailles du MailboxProcessor . De plus, dans l'implémentation actuelle, le temporisateur envoie le message suivant toutes les n millisecondes, indépendamment de quoi que ce soit, donc si nous ne sommes pas entrés dans la mesure 1 fois, les messages commencent à s'accumuler et la situation empire. Il vaudrait beaucoup mieux «étirer» cette mesure particulière, traiter tout ce qui s'est accumulé et passer à autre chose.


Version finale


Évidemment, le schéma des messages doit être simplifié, alors qu'il est très souhaitable de laisser le code aussi simple et accessible que possible - relativement parlant, je ne veux pas tout mettre dans 1 acteur divin, et puis il n'y a pas beaucoup de sens dans les acteurs.
Par conséquent, en regardant ma liste d'acteurs, j'ai réalisé qu'il valait mieux sacrifier un acteur serpent en premier. Une minuterie est nécessaire, un tampon de commandes utilisateur est également nécessaire pour les accumuler en temps réel, mais ne le versez qu'une fois par battement, et il n'y a pas de besoin objectif de garder le serpent dans un acteur séparé, cela a été fait juste pour plus de commodité. De plus, en le tenant avec l'acteur de terrain, il sera possible de traiter le script GrowUp sans délai. Tick message de Tick pour le serpent n'a pas non plus beaucoup de sens, car lorsque nous recevons un message de l'acteur de l'équipe, cela signifie déjà qu'un nouveau rythme s'est produit. Ajoutant à cela l'étirement du rythme en cas de retard, nous avons les modifications suivantes:


  1. Nous GrowUp messages Tick & GrowUp .
  2. Nous tenons l'acteur serpent dans l'acteur de terrain - il va maintenant stocker la «tapla» de ces états.
  3. Nous System.Timers.Timer de l'acteur timer. Au lieu de cela, le schéma de travail sera le suivant: après avoir reçu la commande Start , il envoie Flush acteur Flush commande. Il envoie une liste de commandes au champ + acteur serpent, le dernier acteur traite tout cela et envoie un message Next au timer, lui demandant ainsi un nouveau tick. Le temporisateur, après avoir reçu Next attend Thread.Sleep(delay) et recommence tout le cercle. Tout est simple.

Pour résumer.


  • Dans l'implémentation précédente, 500 ms était le délai minimum autorisé. Dans le délai actuel, vous pouvez le supprimer complètement - l'acteur de terrain aura besoin d'un nouveau rythme lorsqu'il sera prêt. La collecte des messages bruts des mesures précédentes n'est plus possible.
  • La carte de messagerie est grandement simplifiée - au lieu d'un graphique complexe, nous avons la boucle la plus simple.
  • Cette simplification a résolu le bogue lorsque le chronomètre était Stop plusieurs fois en cas de perte.
  • La liste des messages a été réduite. Moins de code - moins de mal!

Cela ressemble à ceci:


  let [<Literal>] commandAddress = "command" let [<Literal>] timerAddress = "timer" let [<Literal>] gameAddress = "game" // -     let commandAgent (mailboxNetwork: MailboxNetwork) = mailboxNetwork.Box<CommandMessage, Command list>(commandAddress) let timerAgent (mailboxNetwork: MailboxNetwork) = mailboxNetwork.Box<TimerCommand, TimerState>(timerAddress) let gameAgent (mailboxNetwork: MailboxNetwork) = mailboxNetwork.Box<Command list, GameState>(gameAddress) // message loop   let gameAgentFn (mailboxNetwork: MailboxNetwork) updateUi gameState cmd = let timerAgent = timerAgent mailboxNetwork //    match gameState.gameFrame with //     | Frame field -> //       let gameState = Game.updateGameState gameState cmd timerAgent.Post Next //    updateUi gameState //     gameState // ! | End (Win _) -> timerAgent.Post PauseOrResume Game.updateGameState gameState cmd //        | _ -> timerAgent.Post Stop //     gameState // message loop   let commandAgentFn (mailboxNetwork: MailboxNetwork) commands msg = let gameAgent = gameAgent mailboxNetwork match msg with | Cmd cmd -> cmd::commands //       | Flush -> commands |> gameAgent.Post //     [] // message loop   let timerAgentFn (mailboxNetwork: MailboxNetwork) (state: TimerState) cmd = let commandAgent = commandAgent mailboxNetwork match cmd with | Start -> commandAgent.Post Flush; {state with active = true} | Next -> if state.active then //    ,     Threading.Thread.Sleep(state.delay) commandAgent.Post Flush; state | Stop -> printfn "Stop received"; { state with active = false } | PauseOrResume -> if not state.active then //     --   commandAgent.Post Flush { state with active = not state.active } | SetDelay delay -> Threading.Thread.Sleep(delay) if state.active then commandAgent.Post Flush {state with delay = delay} 

Les références


Source: https://habr.com/ru/post/fr424861/


All Articles