Czytam i słyszę, że ludzie (również na tej stronie) rutynowo chwalą paradygmat programowania funkcjonalnego, podkreślając, jak dobrze jest mieć wszystko niezmienne. W szczególności ludzie proponują to podejście nawet w tradycyjnie imperatywnych językach OO, takich jak C #, Java lub C ++, nie tylko w czysto funkcjonalnych językach, takich jak Haskell, które wymuszają to na programistach.
Trudno mi to zrozumieć, ponieważ uważam, że zmienność i skutki uboczne ... są wygodne. Biorąc jednak pod uwagę to, jak ludzie obecnie potępiają skutki uboczne i uważają je za dobrą praktykę, aby się ich pozbyć wszędzie, gdzie to możliwe, uważam, że jeśli chcę być kompetentnym programistą, muszę zacząć swoją przygodę w celu lepszego zrozumienia paradygmatu ... Stąd moje Q.
Jednym z miejsc, w którym znajduję problemy z paradygmatem funkcjonalnym, jest to, że obiekt jest naturalnie przywoływany z wielu miejsc. Opiszę to na dwóch przykładach.
Pierwszym przykładem będzie moja gra w C #, którą próbuję zrobić w wolnym czasie . To turowa gra internetowa, w której obaj gracze mają zespoły 4 potworów i mogą wysłać potwora ze swojej drużyny na pole bitwy, gdzie zmierzy się z potworem wysłanym przez przeciwnika. Gracze mogą również przywoływać potwory z pola bitwy i zastępować ich innymi potworami ze swojej drużyny (podobnie jak Pokemon).
W tym ustawieniu do jednego potwora można oczywiście przypisać co najmniej z dwóch miejsc: drużyny gracza i pola bitwy, które odnoszą się do dwóch „aktywnych” potworów.
Rozważmy teraz sytuację, w której jeden potwór zostanie trafiony i straci 20 punktów zdrowia. W nawiasie paradygmatu imperatywnego modyfikuję pole tego potwora, health
aby odzwierciedlić tę zmianę - i właśnie to robię teraz. Jednak powoduje to, że Monster
klasa jest zmienna, a powiązane funkcje (metody) nieczyste, co, jak sądzę, jest obecnie uważane za złą praktykę.
Mimo że wyraziłem zgodę na posiadanie kodu tej gry w stanie mniej niż idealnym, aby mieć jakiekolwiek nadzieje na jej ukończenie w pewnym momencie przyszłości, chciałbym wiedzieć i zrozumieć, jak to powinno być napisane poprawnie. Dlatego: Jeśli jest to wada projektowa, jak to naprawić?
W stylu funkcjonalnym, jak rozumiem, zamiast tego zrobiłbym kopię tego Monster
obiektu, zachowując go identycznym ze starym, z wyjątkiem tego jednego pola; a metoda suffer_hit
zwróciłaby ten nowy obiekt zamiast modyfikować stary na miejscu. Następnie podobnie skopiowałbym Battlefield
obiekt, zachowując wszystkie pola bez zmian, z wyjątkiem tego potwora.
Obejmuje to co najmniej 2 trudności:
- Hierarchia może być znacznie głębsza niż ten uproszczony przykład po prostu
Battlefield
->Monster
. Musiałbym wykonać takie kopiowanie wszystkich pól oprócz jednego i zwrócenie nowego obiektu aż do tej hierarchii. Byłby to kod płyty kotłowej, co wydaje mi się irytujące, zwłaszcza, że programowanie funkcjonalne ma zmniejszyć płytę kotła. - Znacznie poważniejszym problemem jest jednak to, że doprowadziłoby to do braku synchronizacji danych . Aktywny potwór na polu zmniejszyłby swoje zdrowie; jednak ten sam potwór, o którym mówi jego kontrolujący gracz
Team
, nie zrobiłby tego. Gdybym zamiast tego przyjął imperatywny styl, każda modyfikacja danych byłaby natychmiast widoczna ze wszystkich innych miejsc kodu, a w takich przypadkach jak ten uważam, że jest to naprawdę wygodne - ale sposób, w jaki otrzymuję rzeczy, jest dokładnie tym , co mówią ludzie źle z imperatywnym stylem!- Teraz można zająć się tym problemem, podróżując do
Team
każdego następnego ataku. To dodatkowa praca. Co jednak, jeśli do potwora można później nagle odwoływać się z jeszcze większej liczby miejsc? Co się stanie, jeśli przyjdę z umiejętnością, która na przykład pozwala potworowi skupić się na innym potworze, który niekoniecznie jest na polu (właściwie rozważam taką umiejętność)? Czy na pewno pamiętam, aby po każdym ataku odbyć podróż do skupionych potworów? Wydaje się, że to bomba zegarowa, która wybuchnie, gdy kod stanie się bardziej złożony, więc myślę, że nie jest to rozwiązanie.
- Teraz można zająć się tym problemem, podróżując do
Pomysł na lepsze rozwiązanie pochodzi z mojego drugiego przykładu, kiedy napotkałem ten sam problem. W środowisku akademickim powiedziano nam, aby w Haskell napisać tłumacza własnego języka. (W ten sposób musiałem zacząć rozumieć, czym jest FP). Problem pojawił się, gdy wdrażałem zamknięcia. Po raz kolejny ten sam zakres może być teraz przywoływany z wielu miejsc: poprzez zmienną, która utrzymuje ten zakres oraz jako zakres nadrzędny dowolnych zagnieżdżonych zakresów! Oczywiście, jeśli wprowadzono zmianę w tym zakresie za pomocą któregokolwiek z odniesień do niego wskazujących, zmiana ta musi być widoczna również we wszystkich innych odniesieniach.
Rozwiązaniem, które przyjąłem, było przypisanie każdemu zakresowi identyfikatora i utrzymanie centralnego słownika wszystkich zakresów w State
monadzie. Teraz zmienne będą miały tylko identyfikator zakresu, do którego były zobowiązane, a nie sam zakres, a zakresy zagnieżdżone również będą miały identyfikator swojego zakresu nadrzędnego.
Wydaje mi się, że w mojej walce z potworami można by zastosować to samo podejście ... Pola i drużyny nie odnoszą się do potworów; zamiast tego przechowują identyfikatory potworów zapisane w centralnym słowniku potworów.
Jednak po raz kolejny widzę problem z tym podejściem, który uniemożliwia mi zaakceptowanie go bez wahania jako rozwiązania problemu:
To po raz kolejny źródło kodu typu „kocioł”. To sprawia, że jednowierszowe są koniecznie 3-liniowe: to, co poprzednio było modyfikacją pojedynczego pola w jednym wierszu, teraz wymaga (a) Pobieranie obiektu ze słownika centralnego (b) Wprowadzanie zmiany (c) Zapisywanie nowego obiektu do centralnego słownika. Ponadto przechowywanie identyfikatorów obiektów i centralnych słowników zamiast odwołań zwiększa złożoność. Ponieważ FP jest reklamowane w celu zmniejszenia złożoności i poprawiania kodu, oznacza to, że robię to źle.
Chciałem też napisać o drugim problemie, który wydaje się znacznie poważniejszy: takie podejście wprowadza wycieki pamięci . Obiekty, które są nieosiągalne, będą zwykle śmieciami. Jednak obiektów przechowywanych w centralnym słowniku nie można zbierać, nawet jeśli żaden osiągalny obiekt nie odwołuje się do tego konkretnego identyfikatora. I chociaż teoretycznie ostrożne programowanie może uniknąć wycieków pamięci (możemy zadbać o ręczne usunięcie każdego obiektu ze słownika centralnego, gdy nie jest już potrzebny), jest to podatne na błędy i FP jest reklamowane w celu zwiększenia poprawności programów, więc po raz kolejny może to nie być właściwą drogą.
Dowiedziałem się jednak z czasem, że wydaje się to raczej rozwiązanym problemem. Java zapewnia, WeakHashMap
że można by rozwiązać ten problem. C # zapewnia podobną funkcję - ConditionalWeakTable
- chociaż zgodnie z dokumentacją ma być używana przez kompilatory. A w Haskell mamy System.Mem.Weak .
Czy przechowywanie takich słowników jest właściwym funkcjonalnym rozwiązaniem tego problemu, czy też jest prostsze, którego nie widzę? Wyobrażam sobie, że liczba takich słowników może łatwo wzrosnąć i źle; więc jeśli te słowniki mają być również niezmienne, może to oznaczać wiele przekazywania parametrów lub, w językach obsługujących to, obliczenia monadyczne, ponieważ słowniki byłyby przechowywane w monadach (ale ponownie czytam to w czysto funkcjonalnym języki jak najmniej kodu powinny być monadyczne, podczas gdy to rozwiązanie słownikowe umieściłoby prawie cały kod w State
monadzie; co ponownie budzi wątpliwości, czy jest to właściwe rozwiązanie.)
Po namyśle myślę, że dodałbym jeszcze jedno pytanie: Co zyskujemy dzięki konstruowaniu takich słowników? To, co jest złe w programowaniu imperatywnym, jest zdaniem wielu ekspertów tym, że zmiany w niektórych obiektach przenoszą się na inne fragmenty kodu. Aby rozwiązać ten problem, obiekty powinny być niezmienne - właśnie z tego powodu, jeśli dobrze rozumiem, wprowadzone w nich zmiany nie powinny być widoczne gdzie indziej. Ale teraz martwię się o inne fragmenty kodu działające na nieaktualnych danych, więc wymyślam centralne słowniki, aby ... po raz kolejny zmiany w niektórych fragmentach kodu rozprzestrzeniały się na inne fragmenty kodu! Czy nie wracamy zatem do imperatywnego stylu ze wszystkimi jego domniemanymi wadami, ale z dodatkową złożonością?
Team
) mogą odzyskać wynik bitwy, a tym samym stany potworów, za pomocą krotki (numer bitwy, identyfikator istoty potwora).