Programowanie funkcjonalne i przygody tekstowe

14

Jest to głównie teoretyczne pytanie o FP, ale wezmę udział w tekstowych przygodach (takich jak oldschoolowy Zork), aby zilustrować mój punkt widzenia. Chciałbym poznać twoje opinie na temat tego, jak modelowałbyś stanową symulację z FP.

Wydaje się, że przygody tekstowe wymagają OOP. Na przykład wszystkie „pokoje” są instancjami Roomklasy, możesz mieć Itemklasę podstawową i interfejsy, takie jak Item<Pickable>rzeczy, które możesz nosić i tak dalej.

Modelowanie świata w FP działa inaczej, szczególnie jeśli chcesz wymusić niezmienność w świecie, który musi mutować w miarę postępów w grze (obiekty są przemieszczane, wrogowie są pokonani, liczba punktów rośnie, gracz zmienia lokalizację). Wyobrażam sobie jeden duży obiekt, Worldktóry ma to wszystko: jakie pokoje możesz eksplorować, w jaki sposób są połączone, co niesie gracz, jakie dźwignie zostały uruchomione.

Myślę, że czystym podejściem byłoby po prostu przekazanie tego dużego obiektu do dowolnej funkcji i zwrócenie go (ewentualnie zmodyfikowanego). Na przykład mam moveToRoomfunkcję, która pobiera Worldi zwraca ją wraz ze World.player.locationzmianą w nowym pokoju World.rooms[new_room].visited = Truei tak dalej.

Nawet jeśli jest to bardziej „poprawny” sposób, wydaje się, że egzekwuje czystość ze względu na to. W zależności od języka programowania przekazywanie tego potencjalnie bardzo dużego Worldobiektu tam iz powrotem może być kosztowne. Ponadto każda funkcja może wymagać dostępu do dowolnego Worldobiektu. Na przykład, pokój może być dostępny lub nie w zależności od dźwigni uruchomionej w innym pokoju, ponieważ może być zalany, ale jeśli gracz nosi kamizelkę ratunkową, może i tak wejść do niej. Potwór może być agresywny lub nie, w zależności od tego, czy gracz zabił swojego kuzyna w innym pokoju. Oznacza to, że roomCanBeEnteredfunkcja musi mieć dostęp World.player.invetoryi World.rooms, describeMonstermusi mieć dostęp World.monstersi tak dalej (w zasadzie, to musiprzekazać cały ładunek). Wydaje mi się, że to naprawdę wymaga globalnej zmiennej, nawet jeśli jest to dobry styl programowania, szczególnie w FP.

Jak rozwiązałbyś ten problem?

pistacchio
źródło
4
„W zależności od języka programowania przekazywanie tego potencjalnie bardzo dużego obiektu świata może być kosztowne”. Prawdopodobnie zostanie przekazany przez odniesienie. „Ponadto każda funkcja może wymagać dostępu do dowolnego obiektu świata”. Trudno mi uwierzyć, że każda funkcja potrzebuje dostępu do całego stanu gry.
Doval
2
Myślę, że badania Chrisa Martena byłyby interesujące, mają na celu pokazać, jak zrobić fajną interaktywną fikcję w deklaratywnych językach. github.com/chrisamaphone/interactive-lp
Daniel Gratzer
2
Możesz zajrzeć na tego bloga opisującego podejście autora do programowania takiej gry w sposób funkcjonalny. Ten post jest szczególnie trafny.
gallais
3
Muszę się zastanawiać, czy to pytanie wpłynęło na późniejszą decyzję @ EricLippert o napisaniu serii artykułów na temat implementacji (części) maszyny Z w Ocaml ...?
Jules
1
@Jules Link do początku tej serii, dla zainteresowanych: ericlippert.com/2016/02/01/west-of-house
KChaloux

Odpowiedzi:

4

Pamiętaj, że języki funkcjonalne używają struktur danych i oddzielnych funkcji zamiast obiektów. Na przykład zamiast świata miałbyś zestaw pokoi i listę przedmiotów ekwipunku.

Idealnie byłoby również ograniczyć ilość danych, które przekazujesz funkcjom, do tego, ile faktycznie wymagają one tak dużo, jak to możliwe, zamiast przechodzić przez cały świat (powiedz, że wyodrębniasz jeden odpowiedni pokój ze swojego świata; oczywiście całkowicie zależne od siebie światy mogą być trudne oddzielny). Wynik zostałby ponownie włączony do światowej struktury danych, tworząc nowy stan. Nie można modelować stanu bez użycia stanu; jak mówisz, niektóre rzeczy z natury wymagają mutacji.

Większość praktycznych języków funkcjonalnych zapewnia sposób realizacji mutacji albo bezpośrednio (powiedzmy monada ST w Haskell lub transjenty w Clojure), albo wydajnie ją symuluj (często przez ponowne użycie niezmienionych części struktury danych (dobrym przykładem są tutaj domyślne struktury danych Clojure)) ). Tak czy inaczej, utrzymywana jest czystość.

Ponieważ ilość stanu, który należy zmutować, wydaje się ograniczona, nie martwiłbym się zbytnio problemami z wydajnością i stosuję (prawdopodobnie już zoptymalizowane) naiwne podejście funkcjonalne.

Inną opcją, którą widziałem, byłoby zwracanie tylko instrukcji zmiany części świata z twoich indywidualnych funkcji, a następnie aktualizowanie świata zgodnie z nimi. Opis postów na blogu jest dostępny na stronie http://prog21.dadgum.com/23.html .

Obie odpowiedzi dotyczą bardziej sposobu organizowania zmian niż nieprzekazywania całego świata funkcjom, ponieważ z definicji nie można podzielić segmentu całkowicie niezależnego - ale spróbuj zrobić to najlepiej jak potrafisz w twoim przypadku, co nie jest tylko funkcjonalne, ale także dobre praktyki.

hyperfekt
źródło
0

Sam zdecydowanie zastanowiłbym się, czy dany język ma dostęp do jakiejś formy bazy danych. Większość wydarzeń, które jednocześnie zmieniają stan świata, zostaną po prostu zapisane na dysku i nie wpłyną na obecnego gracza w bieżącym pokoju (poza wyjątkowymi okolicznościami, takimi jak wybuchy lub w MMO, przełączniki otwierające drzwi zdalnie, krzyki innych graczy itp.).

Jako taki, aktualny klient naprawdę musi być świadomy Roomobiektu i rzeczy, które mają na niego bezpośredni wpływ. noticableEventsOutsideRoommoże być po prostu podklasą, na którą Roomwpływ miały ostatnie zmiany w bazie danych, a obiekt gry stał się znacznie mniejszy.

Ayelis
źródło
Rozumiem, że to podejście nie ma większego wpływu na wyszukiwanie ścieżek lub wyzwalanie lokalnych wydarzeń (takich jak agro na pobliskich mobach), ale byłem znany z nadużywania baz danych w przeszłości ... Prawdopodobnie po prostu wyślę połączenie update mobs set agro=1 where distance<5i będę skończone z tym. Może nie jest to najlepsza praktyka, ale pasuje do moich celów. Jeśli chodzi o wyszukiwanie ścieżek za pomocą bazy danych, zawsze można było użyć najkrótszego algorytmu ścieżki Dijkstry ...
Ayelis
0

Prawdziwym rozwiązaniem nie jest zebranie wszystkiego do dużego obiektu świata, a następnie przekazanie go. Zamiast tego zaleca się dokładne określenie rodzaju funkcji, z którą mamy do czynienia. Oto kilka przykładów:

BAD:
   f :: (World, Int) -> World

Good:
   f :: (Int,Int,Int,Int) -> World

Zły przykład próbuje zmodyfikować istniejący obiekt, ale dobrym przykładem jest próba stworzenia świata z przestrzeni stanu, w której znajduje się gra. Zasadniczo, aby stworzyć świat, musisz znać wszystkie dane wymagane do wybrania właściwego świata.

tp1
źródło