ثعبان في صندوق البريد وماذا يفعل F #

ما هو كل هذا؟


كل شيء عن الثعبان. يتذكر الجميع ما هو الثعبان: ثعبان يتحرك في حقل مستطيل. يجد الطعام - ينمو في الطول ، يجد نفسه أو على حافة الحقل - يموت. ويمكن للمستخدم إرسال أوامر فقط: يسار ، يمين ، أعلى ، أسفل.
قررت أن أضيف بعض الإجراءات هنا وأجعل الأفعى تهرب من بكمن. وكل هذا على الممثلين!


لذلك ، MailboxProcessor اليوم مثال الثعبان للتحدث عن كيفية بناء نموذج ممثل باستخدام MailboxProcessor من المكتبة القياسية ، وما هي النقاط التي يجب البحث عنها ، والمخاطر التي يمكن أن تتوقعها.


الشفرة المكتوبة هنا ليست مثالية ، وقد تنتهك بعض المبادئ ، وقد تكون أفضل في كتابتها. ولكن إذا كنت مبتدئًا وترغب في التعامل مع صناديق البريد - آمل أن تساعدك هذه المقالة.
إذا كنت تعرف كل شيء عن صناديق البريد بدوني ، فقد تشعر بالملل هنا.


لماذا الممثلين؟


من أجل الممارسة. قرأت عن نموذج الممثلين ، شاهدت الفيديو ، أحببت كل شيء ، لكنني لم أجربه بنفسي. الآن جربته.
على الرغم من حقيقة أنني اخترت التكنولوجيا من أجل التكنولوجيا ، إلا أن المفهوم كان ناجحًا جدًا في هذه المهمة.


لماذا MailboxProcessor وليس Akka.net على سبيل المثال؟


بالنسبة MailboxProcessor ، هناك اسم 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 . كما تعلم ، يقوم الممثل بتخزين الدولة ، ولكن الآن ليس من الواضح تمامًا كيفية الحصول على هذه الحالة من الخارج. يحتوي صندوق البريد الخاص بالمعالج على طريقة PostAndReply ، والتي تأخذ وظيفة AsyncReplyChannel<'Reply> -> 'Msg كمساهمة. في البداية قادني إلى ذهول - من غير الواضح تمامًا من أين تحصل على هذه الوظيفة. ولكن في الواقع ، تبين أن كل شيء أبسط: يجب أن يتم تغليف جميع الرسائل في غلاف DU ، لأننا نحصل الآن على عمليتين على ممثلنا: أرسل الرسالة نفسها واطلب الحالة الحالية. إليك ما يبدو عليه:


 //     . // 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 بعد ذلك بقليل ، ولكن الآن لدينا لوحة مرجعية جاهزة.


في الواقع ، الأفعى


في الأفعى يوجد ثعبان ، مستخدم بأوامره ، حقل وانتقال منتظم للإطار التالي.
هنا كل هذا معًا ويلزم تلطيخه من قبل ممثلينا.
كان التخطيط الأولي كما يلي:


  • ممثل مع جهاز توقيت. يقبل رسائل البدء / الإيقاف / الإيقاف المؤقت. كل مللي ثانية ، يرسل رسالة Flush إلى ممثل Flush . يخزن System.Timers.Timer كدولة
  • فرق الممثل. يستقبل رسائل من المستخدم Move Up/Down/Left/Right ، AddPerk Speed/Attack (نعم ، يمكن AddPerk Speed/Attack أن يزحف بسرعة ويهاجم الأوغاد) و Flush من المؤقت. يقوم بتخزين قائمة من الأوامر كدولة ، ويتم إعادة تعيين هذه القائمة مع تدفق.
  • الممثل هو ثعبان. يقوم بتخزين حالة الثعبان - الامتيازات والطول والاتجاه والانحناءات والإحداثيات.
    يقبل قائمة الرسائل من ممثل الأوامر ، ورسالة Tick (لتحريك خلية الأفعى 1 إلى الأمام) ، GrowUp من ممثل الحقل عندما يعثر على الطعام.
  • ممثل الميدان. يقوم بتخزين خريطة الخلايا ، ويأخذ حالة الثعبان في رسالة ويرسم الإحداثيات على صورة موجودة. كما يرسل GrowUp ممثل الأفعى وأمر Stop إلى المؤقت إذا انتهت اللعبة.

كما ترى ، حتى مع وجود هذا العدد الصغير من الكيانات ، فإن خريطة الرسالة غير تافهة بالفعل. وقد نشأت بالفعل صعوبات في هذه المرحلة: الحقيقة هي أن F # بشكل افتراضي لا يسمح بالتبعيات الدورية. في السطر الحالي من التعليمات البرمجية ، يمكنك فقط استخدام الرمز المكتوب أعلاه ، وينطبق الشيء نفسه على الملفات في المشروع. هذا ليس خطأ ، ولكنه ميزة ، وأنا أحب ذلك كثيرًا ، لأنه يساعد في الحفاظ على الشفرة نظيفة ، ولكن ماذا تفعل عندما تكون الروابط الدورية مطلوبة حسب التصميم؟ بالطبع ، يمكنك استخدام rec namespace - ثم داخل ملف واحد يمكنك الرجوع إلى كل شيء موجود في هذا الملف ، والذي استخدمته.
من المتوقع أن يتعطل الرمز ، ولكن يبدو أنه الخيار الوحيد. وعملت.


مشكلة العالم الخارجي


كل شيء يعمل طالما أن نظام الممثلين بأكمله معزول عن العالم الخارجي ، ولم أقم إلا بعرض الخطوط في وحدة التحكم. عندما حان الوقت لتطبيق التبعية في شكل وظيفة updateUI ، والتي كان من المفترض أن تعيد updateUI لكل علامة ، لم أتمكن من حل هذه المشكلة في التنفيذ الحالي. لا قبيحة ولا جميلة - مستحيل. ثم تذكرت akku - بعد كل شيء ، يمكنك إنشاء ممثلين على طول الطريق ، وقد وصفت جميع ممثلي في مرحلة التجميع.
الحل واضح - استخدم akku! لا ، بالطبع ، لا يزال Akka مبالغة في الإفراط ، لكنني قررت أن ألعق بعض النقاط من هناك - أي إنشاء نظام من الممثلين يمكنك فيه إضافة جهات فاعلة جديدة ديناميكيًا والاستعلام عن الممثلين الحاليين على العنوان.
نظرًا لأنه يتم الآن إضافة الممثلين وحذفهم في وقت التشغيل ، ولكن يتم الحصول عليهم عن طريق العنوان بدلاً من الرابط المباشر ، فأنت بحاجة إلى تقديم سيناريو حيث لا يبدو العنوان في أي مكان ولا يكون الفاعل فيه. باتباع مثال جمعية المحاسبين القانونيين المعتمدين نفسها ، أضفت مربعًا للأحرف الميتة ، وقمت بتصميمه من خلال DUs المفضلة:


 // 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. يرسل المستخدم عددًا عشوائيًا من الأوامر مباشرة إلى ممثل الأمر.
  2. يرسل المؤقت علامة إلى ممثل الفريق ، وفي التنفيذ المبكر أيضًا إلى ممثل الثعبان بحيث ينقل الثعبان إلى الخلية التالية
  3. يرسل ممثل الأمر قائمة أوامر للثعبان عندما تأتي الرسالة المقابلة من المؤقت.
  4. يقوم ممثل الأفعى ، بعد تحديث حالته وفقًا للرسالتين العلويتين ، بإرسال الحالة إلى الممثل الميداني.
  5. الممثل الميداني يعيد رسم كل شيء. إذا وجدت الثعبان طعامًا ، فإنه يرسل رسالة GrowUp إلى ممثل الثعبان ، وبعد ذلك يرسل الحالة الجديدة إلى الممثل الميداني.

ولكل هذا هناك دورة ساعة واحدة ، وهي ليست كافية ، مع مراعاة التزامن في أحشاء MailboxProcessor . علاوة على ذلك ، في التطبيق الحالي ، يرسل الموقت الرسالة التالية كل n مللي ثانية ، بغض النظر عن أي شيء ، لذلك إذا لم نصل إلى الإجراء مرة واحدة ، تبدأ الرسائل في التراكم ، ويزداد الوضع سوءًا. سيكون من الأفضل "توسيع" هذا الإجراء الخاص ، ومعالجة كل ما تراكم ، والمضي قدما.


النسخة النهائية


من الواضح أن مخطط الرسائل يحتاج إلى تبسيط ، في حين أنه من المرغوب فيه للغاية ترك الشفرة بسيطة ويمكن الوصول إليها قدر الإمكان - بشكل نسبي ، لا أريد أن أضع كل شيء في ممثل إله واحد ، ومن ثم لا يوجد الكثير من المعنى في الممثلين.
لذلك ، بالنظر إلى قائمة الممثلين ، أدركت أنه من الأفضل التضحية بممثل الأفعى أولاً. هناك حاجة إلى جهاز ضبط الوقت ، كما يلزم وجود مخزن مؤقت لأوامر المستخدم من أجل تجميعها في الوقت الفعلي ، ولكن صبها مرة واحدة فقط لكل نبضة ، ولا توجد حاجة موضوعية لإبقاء الثعبان في ممثل منفصل ، وقد تم ذلك فقط من أجل الراحة. بالإضافة إلى ذلك ، من خلال عقده مع الممثل الميداني ، سيكون من الممكن معالجة البرنامج النصي GrowUp دون تأخير. رسالة Tick للثعبان لا معنى لها أيضًا ، لأنه عندما نتلقى رسالة من ممثل الفريق ، فهذا يعني بالفعل أن هناك فوزًا جديدًا. إضافة إلى ذلك تمدد الضرب في حالة التأخير ، لدينا التغييرات التالية:


  1. نقوم بإزالة رسائل Tick & GrowUp .
  2. نحمل ممثل الثعبان في الممثل الميداني - سيخزن الآن "tapla" هذه الدول.
  3. نقوم بإزالة System.Timers.Timer من ممثل المؤقت. بدلاً من ذلك ، سيكون مخطط العمل كما يلي: عندما يتلقى أمر Start ، يرسل Flush ممثل الفريق. يرسل قائمة بالأوامر إلى الحقل + ممثل الأفعى ، يعالج الممثل الأخير كل هذا ويرسل رسالة Next إلى المؤقت ، وبالتالي يطلب علامة جديدة منه. المؤقت ، بعد استلامه Next ينتظر Thread.Sleep(delay) ويبدأ الدائرة بأكملها من جديد. كل شيء بسيط.

لتلخيص.


  • في التطبيق السابق ، كان 500 مللي ثانية هو الحد الأدنى المسموح به للتأخير. في التأخير الحالي ، يمكنك إزالته تمامًا - سيتطلب الممثل الميداني فوزًا جديدًا عندما يكون جاهزًا. لم يعد من الممكن جمع الرسائل الأولية من الإجراءات السابقة.
  • خريطة المراسلة مبسطة إلى حد كبير - بدلاً من رسم بياني معقد ، لدينا أبسط حلقة.
  • هذا التبسيط حل الخلل عندما 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/ar424861/


All Articles