Seekor ular di kotak surat dan apa F #

Tentang apa semua ini?


Ini semua tentang ular. Semua orang ingat apa itu ular: ular bergerak di bidang persegi panjang. Menemukan makanan - tumbuh panjang, menemukan dirinya atau ujung lapangan - mati. Dan pengguna hanya dapat mengirim perintah: kiri, kanan, atas, bawah.
Saya memutuskan untuk menambahkan beberapa tindakan di sini dan membuat ular melarikan diri dari pacman. Dan semua ini pada aktor!


Oleh karena itu, hari ini saya akan menggunakan contoh ular untuk berbicara tentang bagaimana membangun model aktor menggunakan MailboxProcessor dari perpustakaan standar, poin apa yang harus dicari, dan perangkap apa yang dapat Anda harapkan.


Kode yang ditulis di sini tidak sempurna, mungkin melanggar beberapa prinsip, dan mungkin lebih baik ditulis. Tetapi jika Anda seorang pemula dan ingin berurusan dengan kotak surat - Saya harap artikel ini membantu Anda.
Jika Anda tahu segalanya tentang kotak surat tanpa saya, Anda mungkin bosan di sini.


Mengapa aktor?


Demi latihan. Saya membaca tentang model aktor, menonton video, saya menyukai segalanya, tetapi saya tidak mencobanya sendiri. Sekarang saya mencobanya.
Terlepas dari kenyataan bahwa sebenarnya saya memilih teknologi demi teknologi, konsep ini sangat berhasil jatuh pada tugas ini.


Mengapa MailboxProcessor, dan bukan, misalnya, Akka.net?


Untuk tugas saya, MailboxProcessor dari stasiun orbital oleh burung pipit, MailboxProcessor jauh lebih sederhana, dan itu adalah bagian dari perpustakaan standar, jadi Anda tidak perlu menghubungkan paket apa pun.


Tentang prosesor kotak surat dan boilerplate terkait


Intinya sederhana. Kotak surat di dalamnya memiliki loop pesan dan beberapa status. Perulangan pesan Anda akan memperbarui status ini sesuai dengan pesan baru yang tiba.


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

Harap dicatat messageLoop rekursif, dan pada akhirnya harus dipanggil lagi, jika tidak hanya satu pesan yang akan diproses, setelah itu aktor ini akan mati. messageLoop juga asinkron, dan setiap iterasi berikutnya dilakukan ketika pesan baru diterima: let! msg = inbox.Receive() let! msg = inbox.Receive() .
Dengan demikian, seluruh muatan logis masuk ke fungsi updateState , yang berarti bahwa untuk membuat kotak surat prosesor, kita bisa membuat fungsi konstruktor yang menerima fungsi pembaruan status dan keadaan nol:


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

Keren! Sekarang kita tidak perlu terus-menerus memonitor agar tidak lupa return! loop newState return! loop newState . Seperti yang Anda tahu, seorang aktor menyimpan negara, tetapi sekarang sama sekali tidak jelas bagaimana cara mendapatkan keadaan ini dari luar. Kotak surat prosesor memiliki metode PostAndReply , yang menggunakan AsyncReplyChannel<'Reply> -> 'Msg fungsi Pesan Pesan sebagai input. Pada awalnya itu membuat saya pingsan - benar-benar tidak jelas dari mana mendapatkan fungsi ini. Tetapi pada kenyataannya, semuanya ternyata lebih sederhana: semua pesan harus dibungkus dengan pembungkus DU, karena kita sekarang mendapatkan 2 operasi pada aktor kita: mengirim pesan itu sendiri dan meminta keadaan saat ini. Begini tampilannya:


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

Fungsi konstruktor kami sekarang terlihat seperti ini:


 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 ) 

Sekarang, untuk bekerja dengan kotak surat, kita perlu membungkus semua pesan kita di Mail.Post ini. Agar tidak menulis ini setiap waktu, lebih baik membungkusnya dalam aplikasi kecil:


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

Saya akan memberi tahu Anda address:string sedikit lebih lambat, tetapi untuk saat ini boilerplate kami sudah siap.


Sebenarnya, ular itu


Dalam ular ada ular, pengguna dengan perintahnya, bidang dan transisi reguler ke bingkai berikutnya.
Ini semua bersama-sama dan perlu diwarnai oleh aktor kita.
Layout awal saya adalah sebagai berikut:


  • Aktor dengan penghitung waktu. Menerima pesan mulai / hentikan / jeda. Setiap n milidetik, mengirim pesan Flush ke aktor Flush . Menyimpan System.Timers.Timer sebagai kondisi
  • Tim aktor Menerima pesan dari pengguna Move Up/Down/Left/Right , AddPerk Speed/Attack (ya, ular saya dapat dengan cepat merangkak dan menyerang penjahat) dan Flush dari timer. Ini menyimpan daftar perintah sebagai keadaan, dan dengan flush daftar ini di-reset.
  • Aktor itu adalah ular. Ini menyimpan keadaan ular - tunjangan, panjang, arah, tikungan dan koordinat.
    Ia menerima daftar pesan dari aktor perintah, pesan Tick (untuk menggerakkan ular 1 sel ke depan), dan pesan GrowUp dari aktor lapangan ketika menemukan makanan.
  • Aktor lapangan. Ini menyimpan peta sel, mengambil keadaan ular dalam pesan dan menggambar koordinat pada gambar yang ada. Ini juga mengirim GrowUp aktor ular dan perintah Stop ke timer jika permainan berakhir.

Seperti yang Anda lihat, bahkan dengan sejumlah kecil entitas, peta pesan sudah non-sepele. Dan sudah pada tahap ini kesulitan muncul: faktanya adalah bahwa secara default F # tidak memungkinkan dependensi siklik. Pada baris kode saat ini, Anda hanya dapat menggunakan kode yang ditulis di atas, dan hal yang sama berlaku untuk file dalam proyek. Ini bukan bug, tapi fitur, dan saya sangat menyukainya, karena membantu menjaga kode tetap bersih, tetapi apa yang harus dilakukan ketika tautan siklik diperlukan oleh desain? Tentu saja, Anda dapat menggunakan rec namespace - dan kemudian di dalam satu file Anda dapat merujuk semua yang ada di file ini, yang saya gunakan.
Kode ini diperkirakan akan berantakan, tetapi sepertinya itu satu-satunya pilihan. Dan itu berhasil.


Masalah dunia luar


Semuanya bekerja selama seluruh sistem aktor diisolasi dari dunia luar, dan saya hanya merusak dan menampilkan garis-garis di konsol. Ketika tiba saatnya untuk mengimplementasikan dependensi dalam bentuk fungsi updateUI , yang seharusnya updateUI ulang untuk setiap centang, saya tidak bisa menyelesaikan masalah ini dalam implementasi saat ini. Tidak jelek atau cantik - tidak mungkin. Dan kemudian saya ingat akku - setelah semua, di sana Anda dapat menghasilkan aktor di sepanjang jalan, dan saya memiliki semua aktor saya dijelaskan pada tahap kompilasi.
Solusinya jelas - gunakan akku! Tidak, tentu saja, Akka masih berlebihan, tetapi saya memutuskan untuk menjilat poin-poin tertentu dari sana - yaitu, untuk menciptakan sistem aktor di mana Anda dapat secara dinamis menambahkan aktor baru dan permintaan aktor yang ada di alamat.
Karena aktor sekarang ditambahkan dan dihapus dalam runtime, tetapi diperoleh dengan alamat daripada tautan langsung, Anda harus memberikan skenario di mana alamat terlihat di mana-mana dan aktor tidak ada di sana. Mengikuti contoh acca yang sama, saya menambahkan sebuah kotak untuk surat mati, dan saya mendesainnya melalui DU favorit saya:


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

Dan sistem itu sendiri terlihat seperti ini:


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

Di sinilah address:string sama address:string , yang saya tulis di atas, berguna. Dan lagi itu berhasil, ketergantungan eksternal sekarang mudah untuk mendapatkan di mana Anda perlu. Fungsi konstruktor aktor sekarang menerima sistem aktor sebagai argumen dan mendapatkan alamat yang diperlukan dari sana:


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

Perlahan


Untuk alasan yang jelas, selama debugging, saya menyetel game ke kecepatan rendah: penundaan antara kutu lebih dari 500 milidetik. Jika Anda mengurangi penundaan menjadi 200, maka pesan mulai datang terlambat, dan tim dari pengguna bekerja dengan penundaan, yang merusak seluruh permainan. Lalat tambahan di salep adalah fakta bahwa timer menerima perintah berhenti jika kehilangan beberapa kali. Untuk pengguna, ini tidak muncul dengan cara apa pun, tetapi bagaimanapun, ada beberapa jenis bug.
Kebenaran yang tidak menyenangkan adalah bahwa para aktor, tentu saja, hebat, tetapi panggilan metode langsung jauh lebih cepat. Oleh karena itu, terlepas dari kenyataan bahwa menyimpan ular itu sendiri di aktor yang terpisah terasa nyaman dari sudut pandang pengorganisasian kode, saya harus meninggalkan ide ini atas nama kecepatan, karena untuk 1 jam permainan pengiriman pesannya terlalu intens:


  1. Pengguna mengirimkan sejumlah perintah langsung ke aktor perintah.
  2. Timer mengirimkan tanda centang ke aktor tim dan, dalam implementasi awal, juga ke aktor ular sehingga ia memindahkan ular ke sel berikutnya
  3. Aktor perintah mengirim daftar perintah untuk ular ketika pesan terkait berasal dari timer.
  4. Aktor ular, setelah memperbarui kondisinya sesuai dengan 2 pesan teratas, mengirim negara ke aktor lapangan.
  5. Aktor lapangan menggambar semuanya. Jika ular menemukan makanan, maka ia mengirim pesan GrowUp ke aktor ular, setelah itu ia mengirim negara baru kembali ke aktor lapangan.

Dan untuk semua ini ada 1 siklus jam, yang tidak cukup, dengan mempertimbangkan sinkronisasi akun di bagian dalam MailboxProcessor . Selain itu, dalam implementasi saat ini, timer mengirim pesan berikutnya setiap n milidetik, terlepas dari apa pun, jadi jika kita tidak masuk ke pengukuran 1 kali, pesan mulai menumpuk, dan situasinya memburuk. Akan jauh lebih baik untuk "meregangkan" ukuran khusus ini, memproses semua yang telah menumpuk, dan melanjutkan.


Versi final


Jelas, skema pesan perlu disederhanakan, sementara sangat diinginkan untuk meninggalkan kode sesederhana dan dapat diakses mungkin - relatif berbicara, saya tidak ingin mendorong semuanya menjadi 1 aktor dewa, dan kemudian tidak ada banyak akal di aktor.
Oleh karena itu, melihat daftar aktor saya, saya menyadari bahwa yang terbaik adalah mengorbankan aktor ular terlebih dahulu. Pengatur waktu diperlukan, buffer perintah pengguna juga diperlukan untuk mengakumulasikannya secara real time, tetapi tuangkan hanya sekali per ketukan, dan tidak ada kebutuhan obyektif untuk menyimpan ular di aktor yang terpisah, ini dilakukan hanya untuk kenyamanan. Selain itu, dengan memegangnya dengan aktor lapangan akan dimungkinkan untuk memproses skrip GrowUp tanpa penundaan. Tick pesan untuk ular juga tidak masuk akal, karena ketika kita mendapatkan pesan dari aktor tim, itu berarti ketukan baru telah terjadi. Menambahkan ini peregangan beat jika terjadi penundaan, kami memiliki perubahan berikut:


  1. Kami menghapus pesan Tick & GrowUp .
  2. Kami memegang aktor ular ke aktor lapangan - dia sekarang akan menyimpan "tapla" dari negara-negara ini.
  3. Kami menghapus System.Timers.Timer dari aktor penghitung waktu. Alih-alih, skema kerjanya adalah sebagai berikut: ketika ia menerima perintah Start , ia mengirimkan Flush aktor tim. Dia mengirim daftar perintah ke bidang + aktor ular, aktor terakhir memproses semua ini dan mengirim pesan Next ke timer, sehingga meminta centang baru darinya. Timer, setelah menerima Next menunggu Thread.Sleep(delay) dan memulai seluruh lingkaran lagi. Semuanya sederhana.

Untuk meringkas.


  • Dalam implementasi sebelumnya, 500 ms adalah penundaan minimum yang diijinkan. Dalam penundaan saat ini, Anda dapat menghapusnya sama sekali - aktor lapangan akan membutuhkan ketukan baru saat siap. Mengumpulkan pesan mentah dari tindakan sebelumnya tidak lagi memungkinkan.
  • Peta olahpesan sangat disederhanakan - alih-alih grafik yang kompleks, kami memiliki loop paling sederhana.
  • Penyederhanaan ini menyelesaikan bug ketika penghitung waktu Stop beberapa kali jika hilang.
  • Daftar pesan telah dikurangi. Lebih sedikit kode - lebih sedikit kejahatan!

Ini terlihat seperti ini:


  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} 

Referensi


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


All Articles