İşlevsel programlama, aynı nesneye birden çok yerden başvurulduğu durumu nasıl ele alır?


10

İnsanların (bu sitede de) fonksiyonel programlama paradigmasını rutin olarak övtüğünü ve her şeyin değişmez olmasının ne kadar iyi olduğunu vurguladığını okuyorum ve duyuyorum. Özellikle, insanlar bu yaklaşımı C #, Java veya C ++ gibi geleneksel olarak zorunlu OO dillerinde bile, sadece programcıya zorlayan Haskell gibi tamamen işlevsel dillerde önermektedir.

Anlamakta zorlanıyorum, çünkü değişebilirliği ve yan etkileri buluyorum ... uygun. Bununla birlikte, insanların şu anda yan etkileri nasıl kınadıkları ve mümkün olan her yerde onlardan kurtulmanın iyi bir uygulama olduğunu düşünürsek, yetkin bir programcı olmak istiyorsam, paradigmayı daha iyi anlamak için jorneyime başlamam gerektiğine inanıyorum ... Dolayısıyla S.

İşlevsel paradigma ile ilgili problemler bulduğum bir yer, bir nesneye doğal olarak birden fazla yerden başvurulmasıdır. İki örnekle açıklayayım.

İlk örnek boş zamanlarımda yapmaya çalıştığım C # oyunum olacak . Her iki oyuncunun da 4 canavardan oluşan takımlara sahip olduğu ve takımlarından savaş alanına rakip bir canavar gönderebileceği bir canavar gönderebildiği sıra tabanlı web oyunu. Oyuncular ayrıca savaş alanından canavarları hatırlayabilir ve onları takımlarından başka bir canavarla değiştirebilir (Pokemon'a benzer şekilde).

Bu ortamda, tek bir canavara doğal olarak en az 2 yerden atıfta bulunulabilir: bir oyuncunun takımı ve iki "aktif" canavara atıfta bulunan savaş alanı.

Şimdi bir canavara vurulduğunda ve 20 sağlık puanı kaybettiğinde durumu ele alalım. Zorunlu paradigmanın köşeli parantezleri içinde bu canavarın healthalanını bu değişikliği yansıtacak şekilde değiştiriyorum - ve şimdi bunu yapıyorum. Ancak, bu Monstersınıf değişken ve ilgili fonksiyonları (yöntemleri) saf olmayan yapar, sanırım şu anda kötü bir uygulama olarak kabul edilir.

Kendime bu oyunun kodunu, geleceğin bir noktasında gerçekten bitirme umuduna sahip olmak için idealden daha az bir duruma sahip olma iznini vermiş olmama rağmen, bunun nasıl olması gerektiğini bilmek ve anlamak istiyorum düzgün yazılmış. Bu nedenle: Bu bir tasarım hatasıysa, nasıl düzeltilir?

İşlevsel tarzda, anladığım kadarıyla, bunun yerine bu Monsternesnenin bir alanı dışında eskisiyle aynı kalmasını sağlıyorum; ve yöntem suffer_hiteskisini değiştirmek yerine bu yeni nesneyi döndürür. Sonra aynı şekilde Battlefield, bu canavar hariç tüm alanlarını aynı tutarak nesneyi kopyalarım .

Bu en az 2 zorluk ile gelir:

  1. Hiyerarşi bu basitleştirilmiş sadece Battlefield-> örneğinden çok daha derin olabilir Monster. Biri hariç tüm alanların bu şekilde kopyalanması ve bu hiyerarşinin sonuna kadar yeni bir nesne döndürülmesi gerekir. Bu, özellikle fonksiyonel programlamanın kazan plakasını azaltması gerektiği için sinir bozucu bulduğum kazan plakası kodu olacaktır.
  2. Bununla birlikte, çok daha ciddi bir sorun, bunun verilerin senkronize olmamasına yol açmasıdır . Alanın aktif canavarı sağlığının azaldığını görecekti; ancak kontrol eden oyuncusundan bahseden aynı canavar Teambunu yapmaz. Bunun yerine zorunlu stili benimsediğimde, verilerin her modifikasyonu diğer tüm kod yerlerinden anında görülebilir ve bu gibi durumlarda gerçekten uygun buluyorum - ama bir şeyleri elde etme yolu tam olarak insanların söylediği şeydir emir kipi ile yanlış!
    • Artık Teamher saldırının ardından bir yolculuğa çıkarak bu konuyla ilgilenmek mümkün olacak . Bu ekstra bir iş. Bununla birlikte, bir canavar daha sonra daha fazla yerden aniden referans verilebilirse ne olur? Örneğin, bir canavarın zorunlu olarak sahada olmayan başka bir canavara odaklanmasına izin veren bir yetenekle gelirsem (aslında böyle bir yeteneği düşünüyorum)? Ben Will mutlaka immediatelly her saldırıdan sonra da odaklanmış canavarlara bir yolculuğa almayı unutmayın? Bu kod daha karmaşık hale geldikçe patlayacak bir saatli bomba gibi görünüyor, bu yüzden bir çözüm olmadığını düşünüyorum.

Aynı soruna çarptığımda daha iyi bir çözüm fikri ikinci örneğimden geliyor. Akademi'de Haskell'de kendi tasarımımızın bir dilinin bir tercümanı yazmamız söylendi. (Ben de FP'nin ne olduğunu anlamaya başlamak zorunda kaldım). Sorun, kapanışları uygularken ortaya çıktı. Bir kez daha aynı kapsama artık birden çok yerden referans verilebilir: Bu kapsamı tutan değişken ve içiçe kapsamların ana kapsamı olarak! Açıkçası, bu kapsamda, kendisine işaret eden referanslardan herhangi biri aracılığıyla bir değişiklik yapılırsa, bu değişikliğin diğer tüm referanslar tarafından da görülebilir olması gerekir.

Birlikte geldiğim çözüm, her bir kapsama bir kimlik atamak ve Statemonad'daki tüm kapsamların merkezi bir sözlüğünü tutmaktı . Artık değişkenler, kapsamın kendisinden ziyade yalnızca bağlı oldukları kapsamın kimliğini tutacak ve iç içe kapsamlar, üst kapsamının kimliğini de tutacaktır.

Sanırım aynı yaklaşım benim canavar savaş oyunumda da denenebilir ... Alanlar ve takımlar canavarlara atıfta bulunmaz; bunun yerine merkezi bir canavar sözlüğüne kaydedilen canavarların kimliklerini tutarlar.

Ancak, bir kez daha bu yaklaşımla ilgili bir sorun görüyorum ki, soruna çözüm olarak tereddüt etmeden kabul etmeme engel oluyorum:

Bir kez daha kaynatma plakası kaynağıdır. Tek satırları zorunlu olarak 3 satır yapar: önceden tek bir alanın tek satır yerinde modifikasyonu artık gerektirir (a) Nesneyi merkezi sözlükten almak (b) Değişikliği yapmak (c) Yeni nesneyi kaydetmek merkezi sözlüğe. Ayrıca, referanslar yerine nesnelerin ve merkezi sözlüklerin kimliklerinin tutulması karmaşıklığı artırır. FP karmaşıklığı ve kaynak kodunu azaltmak için ilan edildiğinden , bu yanlış yapıyorum ipuçları.

Ayrıca çok daha şiddetli görünen asecond problemi hakkında yazacaktım: Bu yaklaşımda bellek sızıntıları ortaya çıkıyor . Ulaşılamayan nesneler normalde çöp toplanır. Bununla birlikte, merkezi bir sözlükte tutulan nesneler, bu özel kimliğe başvurmayan hiçbir ulaşılabilir nesne olmasa bile çöp toplanamaz. Teorik olarak dikkatli programlama bellek sızıntılarını önleyebilirken (artık gerekli olmadığında her nesneyi merkezi sözlükten manuel olarak kaldırmaya dikkat edebiliriz), bu hataya eğilimlidir ve programların doğruluğunu arttırmak için FP reklamı yapılır, bu da bir kez daha doğru yol değil.

Ancak, zaman içinde bunun çözülmüş bir problem gibi göründüğünü öğrendim. Java WeakHashMapbu sorunu çözmek için kullanılabilir sağlar. C # benzer bir olanak sağlar - ConditionalWeakTable- belgelere göre derleyiciler tarafından kullanılması gerekiyorsa da. Ve Haskell'de System.Mem.Weak var .

Bu sözlükleri saklamak bu soruna doğru işlevsel çözüm mü yoksa göremediğim daha basit bir çözüm var mı? Bu tür sözlüklerin sayısının kolayca ve kötü bir şekilde büyüyebileceğini hayal ediyorum; yani bu sözlüklerin değişmez olması gerekiyorsa, bu çok fazla parametre geçişi veya bunu destekleyen dillerde monadik hesaplamalar anlamına gelebilir , çünkü sözlükler monad'larda tutulacaktır (ama bir kez daha bunu tamamen işlevsel olarak okuyorum bu sözlük çözümü neredeyse tüm kodları Statemonad'ın içine yerleştirirken, mümkün olduğunca az kod tek dil olmalıdır ;)

Biraz düşündükten sonra bir soru daha ekleyeceğimi düşünüyorum: Bu sözlükleri oluşturarak ne kazanıyoruz? Zorunlu programlamada yanlış olan şey, birçok uzmana göre, bazı nesnelerdeki değişikliklerin diğer kod parçalarına yayılmasıdır. Bu sorunu çözmek için nesnelerin değişmez olması gerekiyor - tam da bu nedenle, doğru bir şekilde anlarsam, onlarda yapılan değişikliklerin başka bir yerde görünmemesi gerekir. Ama şimdi güncel olmayan veriler üzerinde çalışan diğer kod parçaları hakkında endişeliyim, bu yüzden merkezi sözlükler icat ettim, böylece ... kodun bazı parçalarında bir kez daha değişiklikler diğer kod parçalarına yayılır! Bu nedenle, sözde tüm dezavantajları ile zorunlu bir tarza geri dönmüyoruz, ancak daha fazla karmaşıklık ile mi?


6
Buna biraz perspektif kazandırmak için, işlevsel değişmez programlar çoğunlukla eşzamanlılık içeren veri işleme durumları içindir . Başka bir deyişle, girdi verilerini bir takım denklemler veya çıktı sonucu üreten süreçler aracılığıyla işleyen programlar. Değişmezlik bu senaryoda çeşitli nedenlerle yardımcı olur: birden çok iş parçacığı tarafından okunan değerlerin kullanım ömrü boyunca değişmeyeceği garanti edilir, bu da verileri kilitsiz bir şekilde işleme yeteneğini ve algoritmanın nasıl çalıştığına dair bir nedenden dolayı basitleştirir.
Robert Harvey

8
İşlevsel değişmezlik ve oyun programlama hakkındaki küçük sır, bu iki şeyin birbiriyle uyumsuz olmasıdır. Esasen, statik, taşınmaz bir veri yapısı kullanarak dinamik, sürekli değişen bir sistemi modellemeye çalışıyorsunuz.
Robert Harvey

2
Değişmezliği ve değişmezliği dini bir dogma olarak kabul etmeyin. Her birinin diğerinden daha iyi olduğu durumlar vardır, değişmezlik her zaman daha iyi değildir, örneğin değişmez veri türleriyle bir GUI araç seti yazmak mutlak bir kabus olacaktır.
whatsisname

1
Bu C #-spesifik soru ve cevapları , çoğunlukla mevcut bir değişmez nesnenin hafifçe değiştirilmiş (güncellenmiş) klonları oluşturma ihtiyacından kaynaklanan, ortak levha sorununu kapsar.
rwong

2
Önemli bir fikir, bu oyunda bir canavarın bir varlık olarak kabul edilmesidir. Ayrıca, her savaşın sonucu (savaş sırası numarası, canavarların varlık kimlikleri, savaştan önceki ve sonraki canavarların durumları) belirli bir zaman noktasında (veya zaman adımında) bir durum olarak kabul edilir. Böylece, oyuncular ( Team) savaşın sonucunu ve böylece canavarların durumlarını (savaş numarası, canavar varlık kimliği) demetiyle alabilirler.
rwong

Yanıtlar:


19

Fonksiyonel Programlama birden fazla yerden başvurulan bir nesneyi nasıl işler? Sizi modelinizi tekrar ziyaret etmeye davet ediyor!

Açıklamak için ... ağa bağlı oyunların bazen nasıl yazıldığına bakalım - oyun durumunun merkezi bir "altın kaynak" kopyası ve bu durumu güncelleyen bir dizi gelen müşteri etkinliği ve daha sonra diğer istemcilere yayınlanmak .

Factorio ekibinin bazı durumlarda bunun iyi davranmasını sağlayarak gösterdiği eğlenceyi okuyabilirsiniz ; İşte modellerine kısa bir genel bakış:

Çok oyunculu çalışmamızın temel yolu, tüm istemcilerin oyun durumunu simüle etmeleri ve yalnızca oyuncu girdisini (Giriş Eylemleri adı verilir) alıp göndermesidir. Sunucunun ana sorumluluğu Giriş Eylemlerini proxy yapmak ve tüm istemcilerin aynı işlemleri aynı onay işaretiyle yürütmesini sağlamaktır.

İşlemler yürütüldüğünde sunucunun tahkim yapması gerektiğinden, bir oyuncu eylemi şu şekilde hareket eder: Oyuncu eylemi -> Oyun İstemcisi -> Ağ -> Sunucu -> Ağ-> Oyun istemcisi. Bu, her oyuncu eyleminin yalnızca ağ üzerinden gidiş dönüş yaptığında yürütüldüğü anlamına gelir. Bu, oyunu gerçekten laggy hissettirecek, bu yüzden gecikme gizleme, çok oyunculu oyunun tanıtımından bu yana oyuna eklenen bir mekanizma oldu. Gecikme gizlemesi, diğer oyuncuların eylemlerini ve sunucunun arbitrajını dikkate almadan oyuncu girdisini simüle ederek çalışır.

Factorio'da Oyun Durumu var, bu haritanın, oyuncunun, hakların, her şeyin tam durumu. Sunucudan alınan eylemlere bağlı olarak tüm istemcilerde belirleyici olarak simüle edilir. Bu kutsaldır ve sunucudan veya başka bir istemciden farklıysa, bir desync oluşur.

Oyun Durumunun üstünde Gecikme Durumu var. Bu, ana durumun küçük bir alt kümesini içerir. Gecikme Durumu kutsal değildir ve sadece oyuncunun gerçekleştirdiği Giriş Eylemlerine dayanarak oyun durumunun gelecekte nasıl görüneceğini düşündüğümüzü temsil eder.

Önemli olan, her nesnenin durumunun zaman çizgisindeki belirli bir işarette değişmez olmasıdır . Küresel çok oyunculu durumdaki her şey nihayetinde deterministik bir gerçekliğe yaklaşmalıdır.

Ve - sorunuzun anahtarı bu olabilir. Her varlığın durumu belirli bir onay işareti için değiştirilemez ve zaman içinde yeni örnekler üreten geçiş olaylarını takip edersiniz.

Bunu düşünürseniz, sunucudan gelen olay kuyruğunun, olayları uygulayabilmesi için merkezi bir varlık dizinine erişimi olmalıdır.

Sonunda, karmaşıklaştırmak istemediğiniz basit tek satırlı mutasyon yöntemleri sadece basittir, çünkü zamanı gerçekten doğru bir şekilde modellemezsiniz. Sonuçta, sağlık işleme döngüsünün ortasında sağlık değişebilirse, bu onay işaretindeki önceki varlıklar eski bir değer görür ve daha sonra olanlar değişmiş bir tane görür. Bunu dikkatli bir şekilde yönetmek , büyük zaman çizgisinde gerçekten sadece iki kene olan en az farklılaştırıcı akım (değişmez) ve sonraki (yapım aşamasında) durumlar anlamına gelir !

Bu nedenle, geniş bir rehber olarak, bir canavarın durumunu, örneğin yer / hız / fizik, sağlık / hasar, varlıklarla ilgili bir dizi küçük nesneye bölmeyi düşünün. Olabilecek her mutasyonu tanımlamak için bir olay oluşturun ve ana döngünüzü şu şekilde çalıştırın:

  1. girdileri işleyin ve ilgili olayları oluşturun
  2. dahili olaylar oluşturmak (örneğin, nesne çarpışmaları vb. nedeniyle)
  3. olayları mevcut değişmez canavarlara uygulayın, bir sonraki kene için yeni canavarlar oluşturun - çoğunlukla mümkünse eski değişmemiş durumu kopyalayın, ancak gerektiğinde yeni durum nesneleri oluşturun.
  4. render ve sonraki kene için tekrarlayın.

Ya da böyle bir şey. "Bunu nasıl dağıtabilirim?" Diye düşünürüm. genel olarak, şeylerin nerede yaşadıkları ve nasıl gelişmeleri gerektiği konusunda kafam karıştığında anlayışımı geliştirmek için oldukça iyi bir zihinsel egzersizdir.

@ AaronM.Eshbach'tan gelen bir not sayesinde, bunun Olay Kaynağı ve CQRS modeli ile benzer bir sorun alanı olduğunu vurgulayan, zaman içinde dağıtılmış bir sistemdeki durum değişikliklerini bir dizi değişmez olay olarak modelleniyorsunuz . Bu durumda, büyük olasılıkla, sorgu / görünüm sisteminden mutator komut işlemesini ayırarak (adından da anlaşılacağı gibi!) Karmaşık bir veritabanı uygulamasını temizlemeye çalışıyoruz. Elbette daha karmaşık, ancak daha esnek.


2
Ek başvuru için, bkz. Olay Sağlama ve CQRS . Bu benzer bir sorun alanıdır: Zaman içinde değişmeyen olaylar dizisi olarak dağıtılmış bir sistemdeki durum değişikliklerinin modellenmesi.
Aaron M.Eshbach

@ AaronM.Eshbach bu! Cevabınıza yorumunuzu / alıntılarınızı dahil etmemin sakıncası var mı? Kulağa daha yetkili kılar. Teşekkürler!
SusanW

Tabii ki hayır, lütfen yap.
Aaron M. Eshbach

3

Hala zorunlu olan kampın yarısısınız. Tek seferde tek bir nesneyi düşünmek yerine oyununuzu oyunların veya olayların geçmişi açısından düşünün

p1 - send m1 to battlefield
p2 - send m2 to battlefield
m1 - attacks m2 (2 dam)
m2 - attacks m1 (10 dam)
p1 - retreats m1

vb

Değişmez bir durum nesnesi üretmek için eylemleri bir araya getirerek, oyunun durumunu herhangi bir noktada hesaplayabilirsiniz. Her oynatma, bir durum nesnesi alan ve yeni bir durum nesnesi döndüren bir işlevdir

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.