Saya membaca dan mendengar bahwa orang-orang (juga di situs ini) secara rutin memuji paradigma pemrograman fungsional, menekankan betapa baiknya memiliki segala sesuatu yang abadi. Khususnya, orang mengusulkan pendekatan ini bahkan dalam bahasa OO yang secara tradisional sangat penting, seperti C #, Java atau C ++, tidak hanya dalam bahasa yang murni fungsional seperti Haskell yang memaksakan ini pada programmer.
Saya merasa sulit untuk memahaminya, karena saya menemukan sifat berubah-ubah dan efek samping ... nyaman. Namun, mengingat bagaimana orang-orang saat ini mengutuk efek samping dan menganggapnya sebagai praktik yang baik untuk menyingkirkan mereka sedapat mungkin, saya percaya bahwa jika saya ingin menjadi programmer yang kompeten saya harus memulai jorney saya menuju pemahaman yang lebih baik tentang paradigma ... Oleh karena itu Q.
Satu tempat ketika saya menemukan masalah dengan paradigma fungsional adalah ketika suatu objek secara alami dirujuk dari banyak tempat. Biarkan saya menggambarkannya pada dua contoh.
Contoh pertama adalah game C # yang saya coba buat di waktu luang . Ini permainan web berbasis giliran di mana kedua pemain memiliki tim 4 monster dan dapat mengirim monster dari tim mereka ke medan perang, di mana ia akan menghadapi monster yang dikirim oleh pemain lawan. Pemain juga dapat memanggil kembali monster dari medan perang dan menggantinya dengan monster lain dari tim mereka (mirip dengan Pokemon).
Dalam pengaturan ini, monster tunggal dapat dirujuk secara alami dari setidaknya 2 tempat: tim pemain dan medan perang, yang merujuk dua monster "aktif".
Sekarang mari kita pertimbangkan situasi ketika satu monster terkena dan kehilangan 20 poin kesehatan. Dalam kurung paradigma imperatif saya memodifikasi health
bidang monster ini untuk mencerminkan perubahan ini - dan inilah yang saya lakukan sekarang. Namun, ini membuat Monster
kelas bisa berubah dan fungsi terkait (metode) tidak murni, yang saya kira dianggap praktik yang buruk seperti yang sekarang.
Meskipun saya memberi diri saya izin untuk memiliki kode permainan ini dalam keadaan kurang ideal untuk memiliki harapan untuk benar-benar menyelesaikannya di beberapa titik di masa depan, saya ingin tahu dan mengerti bagaimana seharusnya ditulis dengan benar. Karena itu: Jika ini cacat desain, bagaimana cara memperbaikinya?
Dalam gaya fungsional, seperti yang saya mengerti, saya akan membuat salinan dari Monster
objek ini , menjaganya agar tetap sama dengan yang lama kecuali untuk bidang yang satu ini; dan metode suffer_hit
akan mengembalikan objek baru ini alih-alih memodifikasi yang lama di tempat. Lalu aku juga akan menyalin Battlefield
objek, menjaga semua bidangnya tetap sama kecuali monster ini.
Ini hadir dengan setidaknya 2 kesulitan:
- Hirarki dapat dengan mudah menjadi lebih dalam dari contoh sederhana ini
Battlefield
->Monster
. Saya harus melakukan penyalinan semua bidang kecuali satu dan mengembalikan objek baru sepanjang hierarki ini. Ini akan menjadi kode boilerplate yang menurut saya menjengkelkan terutama karena pemrograman fungsional seharusnya mengurangi boilerplate. - Namun, masalah yang jauh lebih parah adalah bahwa hal ini akan menyebabkan data tidak sinkron . Monster aktif di lapangan akan melihat kesehatannya berkurang; Namun, monster yang sama ini, yang dirujuk dari pemain pengendali
Team
, tidak akan melakukannya. Jika saya sebaliknya menggunakan gaya imperatif, setiap modifikasi data akan langsung terlihat dari semua tempat kode lainnya dan dalam kasus seperti ini saya merasa sangat nyaman - tetapi cara saya mendapatkan hal-hal ini persis seperti yang dikatakan orang. salah dengan gaya imperatif!- Sekarang mungkin untuk mengurus masalah ini dengan melakukan perjalanan ke
Team
setelah setiap serangan. Ini pekerjaan ekstra. Namun, bagaimana jika monster tiba-tiba dapat direferensikan dari lebih banyak tempat? Bagaimana jika saya datang dengan kemampuan yang, misalnya, memungkinkan monster fokus pada monster lain yang belum tentu di lapangan (saya sebenarnya mempertimbangkan kemampuan seperti itu)? Apakah saya pasti ingat untuk melakukan perjalanan ke monster yang fokus segera setelah setiap serangan? Ini sepertinya bom waktu yang akan meledak ketika kode semakin kompleks, jadi saya pikir ini bukan solusi.
- Sekarang mungkin untuk mengurus masalah ini dengan melakukan perjalanan ke
Sebuah ide untuk solusi yang lebih baik datang dari contoh kedua saya, ketika saya menemukan masalah yang sama. Di akademisi kami disuruh menulis penerjemah bahasa desain kami sendiri di Haskell. (Ini juga bagaimana saya dipaksa untuk mulai memahami apa itu FP). Masalahnya muncul ketika saya menerapkan penutupan. Sekali lagi cakupan yang sama sekarang dapat direferensikan dari beberapa tempat: Melalui variabel yang menyimpan lingkup ini dan sebagai cakupan induk dari setiap cakupan bersarang! Jelas, jika suatu perubahan dilakukan pada ruang lingkup ini melalui referensi yang menunjuk padanya, perubahan ini juga harus terlihat melalui semua referensi lain.
Solusi yang saya datangi adalah untuk menetapkan setiap lingkup ID dan memegang kamus pusat semua cakupan di State
monad. Sekarang variabel hanya akan menyimpan ID dari lingkup mereka terikat, bukan lingkup itu sendiri, dan cakupan bersarang juga akan memegang ID dari lingkup induknya.
Saya kira pendekatan yang sama bisa dicoba dalam game pertarungan monster saya ... Fields dan tim tidak mereferensikan monster; mereka malah memegang ID monster yang disimpan di kamus monster pusat.
Namun, saya sekali lagi dapat melihat masalah dengan pendekatan ini yang mencegah saya menerimanya tanpa ragu-ragu sebagai solusi untuk masalah ini:
Sekali lagi ini adalah sumber kode boilerplate. Itu membuat one-liners tentu 3-liner: apa yang sebelumnya merupakan modifikasi satu-baris pada satu bidang sekarang membutuhkan (a) Mengambil objek dari kamus pusat (b) Membuat perubahan (c) Menyimpan objek baru ke kamus pusat. Juga, memegang id objek dan kamus pusat alih-alih memiliki referensi meningkatkan kompleksitas. Karena FP diiklankan untuk mengurangi kompleksitas dan kode boilerplate, ini mengisyaratkan saya salah melakukannya.
Saya juga akan menulis tentang masalah kedua yang tampaknya jauh lebih parah: Pendekatan ini memperkenalkan kebocoran memori . Objek yang tidak terjangkau biasanya adalah sampah yang dikumpulkan. Namun, objek yang disimpan dalam kamus pusat tidak boleh berupa sampah yang dikumpulkan, bahkan jika tidak ada objek yang dapat dijangkau merujuk ID khusus ini. Dan sementara pemrograman yang secara teoritis hati-hati dapat menghindari kebocoran memori (kita bisa berhati-hati untuk menghapus secara manual setiap objek dari kamus pusat setelah tidak diperlukan lagi), ini rawan kesalahan dan FP diiklankan untuk meningkatkan kebenaran program sehingga sekali lagi ini mungkin bukan cara yang benar.
Namun, saya menemukan pada waktunya bahwa itu tampaknya menjadi masalah yang terpecahkan. Java menyediakan WeakHashMap
yang dapat digunakan untuk menyelesaikan masalah ini. C # menyediakan fasilitas serupa - ConditionalWeakTable
- meskipun menurut dokumen itu dimaksudkan untuk digunakan oleh kompiler. Dan di Haskell kami memiliki System.Mem.Weak .
Apakah menyimpan kamus seperti itu merupakan solusi fungsional yang tepat untuk masalah ini atau apakah ada yang lebih sederhana yang tidak dapat saya lihat? Saya membayangkan bahwa jumlah kamus seperti itu dapat dengan mudah tumbuh dan buruk; jadi jika kamus ini seharusnya juga tidak berubah ini bisa berarti banyak parameter-passing atau, dalam bahasa yang mendukung itu, perhitungan monadik, karena kamus akan diadakan di monad (tapi sekali lagi saya membacanya dalam fungsi murni bahasa sebagai kode sesedikit mungkin harus monadik, sedangkan solusi kamus ini akan menempatkan hampir semua kode di dalam State
monad; yang sekali lagi membuat saya ragu apakah ini solusi yang tepat.)
Setelah beberapa pertimbangan saya pikir saya akan menambahkan satu pertanyaan lagi: Apa yang kita peroleh dengan membangun kamus seperti itu? Apa yang salah dengan pemrograman imperatif adalah, menurut banyak ahli, bahwa perubahan pada beberapa objek menyebar ke bagian kode lainnya. Untuk mengatasi masalah ini, objek seharusnya tidak dapat diubah - justru karena alasan ini, jika saya mengerti dengan benar, bahwa perubahan yang dilakukan padanya tidak boleh terlihat di tempat lain. Tapi sekarang saya khawatir tentang potongan kode lain yang beroperasi pada data yang sudah ketinggalan zaman sehingga saya membuat kamus pusat sehingga ... sekali lagi perubahan pada beberapa bagian kode menyebar ke bagian kode lainnya! Bukankah kita, karena itu, kembali ke gaya imperatif dengan semua kelemahannya, tetapi dengan kompleksitas tambahan?
sumber
Team
) dapat mengambil hasil dari pertempuran dan dengan demikian menyatakan monster dengan tuple (nomor pertempuran, ID entitas monster).Jawaban:
Bagaimana Pemrograman Fungsional menangani objek yang dirujuk dari beberapa tempat? Ini mengundang Anda untuk mengunjungi kembali model Anda!
Untuk menjelaskan ... mari kita lihat bagaimana permainan jaringan kadang-kadang ditulis - dengan salinan "sumber emas" pusat dari kondisi permainan, dan serangkaian acara klien yang masuk yang memperbarui keadaan itu, dan kemudian disiarkan kembali ke klien lain .
Anda dapat membaca tentang kesenangan yang dimiliki oleh tim Factorio untuk menjadikannya berperilaku baik dalam beberapa situasi; inilah gambaran singkat dari model mereka:
Kuncinya adalah bahwa keadaan setiap objek tidak berubah pada tanda centang khusus di garis waktu . Segala sesuatu dalam keadaan multi-pemain global pada akhirnya harus bertemu dengan realitas deterministik.
Dan - itu mungkin menjadi kunci pertanyaan Anda. Status setiap entitas tidak berubah untuk kutu yang diberikan, dan Anda melacak peristiwa transisi yang menghasilkan instance baru dari waktu ke waktu.
Jika Anda memikirkannya, antrian acara yang masuk dari server harus memiliki akses ke direktori pusat entitas, hanya agar ia dapat menerapkan acara tersebut.
Pada akhirnya, metode mutator satu baris sederhana yang tidak ingin Anda rumit hanya sederhana karena Anda tidak benar-benar memodelkan waktu secara akurat. Lagi pula, jika kesehatan dapat berubah di tengah-tengah loop pemrosesan, maka entitas sebelumnya dalam centang ini akan melihat nilai lama, dan yang kemudian melihat yang berubah. Mengelola ini dengan hati-hati berarti setidaknya membedakan saat ini (kekal) dan berikutnya (dalam konstruksi) negara, yang benar-benar hanya dua kutu dalam waktu-garis besar kutu!
Jadi, sebagai panduan luas, pertimbangkan memecah keadaan monster menjadi sejumlah benda kecil yang berhubungan dengan, katakanlah, lokasi / kecepatan / fisika, kesehatan / kerusakan, aset. Buat peristiwa untuk menggambarkan setiap mutasi yang mungkin terjadi, dan jalankan loop utama Anda sebagai:
Atau semacam itu. Saya menemukan pemikiran "bagaimana saya membuat ini didistribusikan?" adalah latihan mental yang cukup bagus, secara umum, untuk memperbaiki pemahaman saya ketika saya bingung tentang di mana hal-hal hidup dan bagaimana mereka harus berkembang.
Berkat catatan dari @ AaronM.Eshbach, menyoroti bahwa ini adalah domain masalah yang sama dengan Sumber Acara dan pola CQRS , di mana Anda memodelkan perubahan untuk menyatakan dalam sistem terdistribusi sebagai serangkaian acara yang berubah dari waktu ke waktu . Dalam hal ini, kami kemungkinan besar berusaha membersihkan aplikasi database yang kompleks, dengan memisahkan (seperti namanya!) Perintah mutator yang menangani dari sistem query / view. Lebih kompleks tentu saja, tetapi lebih fleksibel.
sumber
Anda masih setengah di kamp imperatif. Alih-alih memikirkan satu objek pada satu waktu pikirkan game Anda dalam hal sejarah permainan atau peristiwa
dll
Anda dapat menghitung status permainan pada titik tertentu dengan merantai tindakan bersama-sama untuk menghasilkan objek keadaan tidak berubah. Setiap permainan adalah fungsi yang mengambil objek keadaan dan mengembalikan objek keadaan baru
sumber