邮箱中的蛇和F#有什么作用

这是怎么回事?


都是关于蛇的。 每个人都记得蛇是什么:蛇在矩形区域上移动。 找到食物-长大,发现自己或田野边缘-死亡。 用户只能发送命令:左,右,上,下。
我决定在这里添加一些动作,使蛇从吃豆子逃走。 而这一切都在演员身上!


因此,今天,我将以蛇为例,讨论如何使用标准库中的MailboxProcessor构建actor模型,要查找的要点以及可能遇到的陷阱。


此处编写的代码并不完美,可能违反某些原则,并且可能会更好地编写。 但是,如果您是初学者并且想处理邮箱-希望本文对您有所帮助。
如果您在没有我的情况下了解所有有关邮箱的信息,那么您可能会觉得无聊。


为什么是演员?


为了练习。 我读了演员的模特,看了视频,我喜欢一切,但我自己没有尝试。 现在我尝试了。
尽管事实上我为技术而选择技术,但这一概念非常成功地落在了这项任务上。


为什么选择MailboxProcessor,而不选择Akka.net?


对于我的任务, MailboxProcessor是麻雀从轨道站来的, MailboxProcessor更为简单,它是标准库的一部分,因此您无需连接任何程序包。


关于邮箱处理器和相关样板


重点很简单。 里面的邮箱有一个消息循环和某些状态。 您的消息循环将根据收到的新消息来更新此状态。


 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) ) 

请注意messageLoop递归的,最后必须再次调用它,否则仅处理一条消息,此参与者将死亡。 messageLoop也是异步的,并且在收到新消息时执行每个后续迭代: let! msg = inbox.Receive() let! msg = inbox.Receive()
因此,整个逻辑负载updateState函数承担,这意味着要创建处理器的邮箱,我们可以使构造函数接受状态更新函数和零状态:


 //   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 ) 

好酷! 现在我们不需要经常监视,以免忘记return! loop newState return! loop newState 。 如您所知,actor存储一个状态,但是现在完全不清楚如何从外部获取该状态。 处理器的邮箱具有PostAndReply方法,该方法接受AsyncReplyChannel<'Reply> -> 'Msg函数。 最初,它使我陷入僵局-完全不清楚从何处获得此功能。 但实际上,一切都变得更简单:所有消息都必须包装在DU包装器中,因为我们现在对actor进行了2种操作:发送消息本身并询问当前状态。 看起来是这样的:


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

现在,我们的构造函数如下所示:


 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 ) 

现在,要使用邮箱,我们需要将所有消息包装在此Mail.Post 。 为了避免每次都编写此代码,最好将其包装在一个小应用程序中:


 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() 

我将告诉您什么address:string稍晚一些,但是现在我们的样板已经准备好。


其实蛇


在蛇中,有一条蛇,一个带有命令的用户,一个字段和到下一帧的常规过渡。
这一切都在一起,需要我们的演员们弄脏。
我的初始布局如下:


  • 演员与计时器。 接受开始/停止/暂停消息。 每n毫秒,将Flush消息发送给Flush参与者。 将System.Timers.Timer存储为状态
  • 演员团队。 接收来自用户的消息,包括上Move Up/Down/Left/RightAddPerk Speed/Attack (是的,我的蛇可以快速爬网和攻击小人)和计时器Flush 。 它存储命令列表作为状态,并通过刷新清除此列表。
  • 演员是蛇。 它存储蛇的状态-特权,长度,方向,弯曲和坐标。
    当发现食物时,它会接收来自命令执行者的消息列表, Tick消息(向前移动蛇1个单元格)和来自字段执行者的GrowUp消息。
  • 领域的演员。 它存储细胞图,在消息中获取蛇的状态,并在现有图片上绘制坐标。 如果游戏结束,它还会将GrowUp发送GrowUp蛇演员,并将Stop命令发送给计时器。

如您所见,即使只有这么少的实体,消息映射也已经很重要了。 在这个阶段已经出现了困难:事实是默认情况下F#不允许循环依赖。 在当前代码行中,您只能使用上面编写的代码,并且对项目中的文件也是如此。 这不是错误,而是功能,我非常喜欢它,因为它有助于保持代码整洁,但是当设计需要循环链接时该怎么办? 当然,您可以使用rec namespace -然后在一个文件中,您可以引用此文件中使用的所有内容。
预计代码会弄乱,但随后似乎是唯一的选择。 而且有效。


外界的问题


只要演员的整个系统都与外界隔离,一切都可以正常工作,而我只在控制台中进行脱线和显示线条。 当需要以updateUI函数的形式实现依赖关系时(应该为每个刻度重新绘制),我无法在当前实现中解决此问题。 既不丑也不美-没办法。 然后我想起了akku-毕竟,您可以在此过程中生成演员,并且在编译阶段将所有演员介绍给我。
解决方案很明显-使用akku! 不,当然,Akka仍然过高,但是我决定从那里舔一些观点-即创建一个actor系统,您可以在其中动态添加新actor并在该地址查询现有actor。
由于现在在运行时中添加和删除角色,但通过地址而不是直接链接来获取角色,因此我们需要提供一种方案,其中地址看起来不存在而角色不在那里。 按照同一acca的示例,我添加了一个用于存放死信的框,并通过我最喜欢的DU设计了它:


 // 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() 

系统本身看起来像这样:


 //    .     --    . 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() | _ -> () 

这是我上面写过的相同address:string派上用场的地方。 再次奏效,现在外部依存关系很容易到达您需要的位置。 参与者的构造函数现在接受了参与者系统作为参数,并从那里获得了必要的地址:


  //    - (  )   - 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 [] 

慢慢地


出于明显的原因,在调试过程中,我将游戏设置为低速运行:滴答之间的延迟超过500毫秒。 如果将延迟减少到200,则消息开始延迟到达,并且用户团队延迟工作,这破坏了整个游戏。 美中不足的是计时器多次丢失时,计时器会收到停止命令。 对于用户而言,这丝毫没有出现,但是仍然存在某种错误。
令人不愉快的事实是,参与者当然很方便,但是直接方法调用要快得多。 因此,尽管从组织代码的角度来看,将蛇本身存储在单独的角色中很方便,但我还是不得不以速度为名放弃这个想法,因为对于游戏的1个时钟而言,消息传递太激烈了:


  1. 用户直接将任意数量的命令发送到命令执行者。
  2. 计时器将滴答声发送给团队演员,在早期实现中还发送给蛇演员,以便他将蛇移到下一个单元
  3. 当相应的消息来自计时器时,命令执行者发送蛇的命令列表。
  4. 根据2个上层消息更新了状态的蛇演员将状态发送给现场演员。
  5. 现场演员重画了一切。 如果蛇发现了食物,那么它将向蛇演员发送一条GrowUp消息,此后他将新状态发送回田野演员。

考虑到MailboxProcessor肠道中的同步,对于所有这一切来说,只有1个时钟周期是不够的。 而且,在当前的实现中,计时器每隔n毫秒发送一次下一条消息,无论是否发生任何事情,因此,如果我们没有进行1次测量,则消息开始累积,情况变得更糟。 最好“伸展”这一特定的措施,处理所有积累的东西,然后继续前进。


最终版本


显然,应该简化消息方案,同时非常希望使代码尽可能简单且易于访问-相对而言,我不想将所有内容都推给一位上帝演员,因此演员中的意义不大。
因此,通过查看演员名单,我意识到最好先牺牲一个蛇演员。 需要一个计时器,还需要一个缓冲区来实时积累用户命令,但是每拍一次只能将其倒入一次,并且不需要客观地将蛇保留在单独的角色中,这样做只是为了方便。 此外,通过将其与现场演员保持在一起,可以立即处理GrowUp脚本。 蛇的Tick消息也没有多大意义,因为当我们从团队演员那里收到消息时,这已经意味着发生了新的节拍。 此外,如果有延迟,则可以延长节拍,我们进行了以下更改:


  1. 我们删除了“ TickGrowUp消息。
  2. 我们将蛇演员放到现场演员中-他现在将存储这些州的“ tapla”。
  3. 我们从计时器System.Timers.Timer删除System.Timers.Timer 。 而是,工作方案如下:当他收到“ Start命令时,他将“ Flush发送Flush团队演员。 他向现场+蛇演员发送命令列表,最后一个演员处理所有这一切,并将Next一条消息发送给计时器,从而向他请求新的滴答声。 接收到Next的计时器将等待Thread.Sleep(delay)并重新开始整个循环。 一切都很简单。

总结一下。


  • 在先前的实现中,最小允许延迟为500 ms。 在当前延迟中,您可以将其完全删除-现场演员准备就绪时将需要新的节拍。 不再可能从以前的措施中收集原始消息。
  • 消息映射大大简化了-我们拥有最简单的循环,而不是复杂的图。
  • 这种简化解决了计时器丢失时多次Stop的错误。
  • 消息列表已减少。 更少的代码-更少的邪恶!

看起来像这样:


  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} 

参考文献


Source: https://habr.com/ru/post/zh-CN424861/


All Articles