Jak zbudować system, który ma wszystkie następujące elementy :
- Używanie czystych funkcji z niezmiennymi obiektami.
- Przekaż tylko dane funkcji, których potrzebuje, nie więcej (tj. Nie ma dużego obiektu stanu aplikacji)
- Unikaj posiadania zbyt wielu argumentów do funkcji.
- Unikaj konstruowania nowych obiektów tylko w celu pakowania i rozpakowywania parametrów do funkcji, po prostu unikaj przekazywania zbyt wielu parametrów do funkcji. Jeśli mam spakować wiele elementów do funkcji jako pojedynczy obiekt, chcę, aby ten obiekt był właścicielem tych danych, a nie czymś tymczasowo skonstruowanym
Wydaje mi się, że monada państwowa łamie zasadę nr 2, chociaż nie jest to oczywiste, ponieważ jest wpleciona w monadę.
Mam wrażenie, że muszę jakoś używać Soczewek, ale bardzo mało jest o tym napisane dla języków niefunkcjonalnych.
tło
W ramach ćwiczenia przekształcam jedną z istniejących aplikacji ze stylu obiektowego na styl funkcjonalny. Pierwszą rzeczą, którą próbuję zrobić, jest jak najwięcej wewnętrznego rdzenia aplikacji.
Jedną rzeczą, jaką słyszałem, jest to, jak zarządzać „stanem” w czysto funkcjonalnym języku, a moim zdaniem to, co robią monady państwowe, polega na tym, że logicznie nazywasz funkcję czystą, „przechodząc w stan world as is ”, a kiedy funkcja powróci, zwróci ci stan świata, który się zmienił.
Aby to zilustrować, sposób, w jaki można zrobić „witaj świat” w czysto funkcjonalny sposób, jest taki, że przekazujesz programowi ten stan ekranu i odbierasz stan ekranu z nadrukowanym „witaj światem”. Więc technicznie, wywołujesz funkcję czystą i nie ma żadnych skutków ubocznych.
Na tej podstawie przejrzałem moją aplikację i: 1. Najpierw umieść cały stan mojej aplikacji w jednym globalnym obiekcie (GameState) 2. Po drugie, uczyniłem GameState niezmiennym. Nie możesz tego zmienić. Jeśli potrzebujesz zmiany, musisz zbudować nową. Zrobiłem to, dodając konstruktor kopii, który opcjonalnie przyjmuje jedno lub więcej pól, które uległy zmianie. 3. Do każdej aplikacji przekazuję GameState jako parametr. W ramach funkcji, po zrobieniu tego, co zrobi, tworzy nowe GameState i zwraca je.
Jak mam czysty funkcjonalny rdzeń i zewnętrzną pętlę, która przesyła GameState do głównej pętli przepływu pracy aplikacji.
Moje pytanie:
Mój problem polega na tym, że GameState ma około 15 różnych niezmiennych obiektów. Wiele funkcji na najniższym poziomie działa tylko na kilku z tych obiektów, takich jak utrzymywanie wyniku. Powiedzmy, że mam funkcję, która oblicza wynik. Dzisiaj GameState jest przekazywane do tej funkcji, która modyfikuje wynik, tworząc nowy GameState z nowym wynikiem.
Coś w tym wydaje się nie tak. Funkcja nie potrzebuje całości GameState. Po prostu potrzebuje obiektu Score. Zaktualizowałem go, aby przekazać Wynik i zwrócić tylko Wynik.
Wydawało się to mieć sens, więc poszedłem dalej z innymi funkcjami. Niektóre funkcje wymagałyby ode mnie wprowadzenia 2, 3 lub 4 parametrów z GameState, ale ponieważ użyłem wzorca przez cały zewnętrzny rdzeń aplikacji, przekazuję coraz więcej stanu aplikacji. Na przykład w górnej części pętli przepływu pracy wywołałbym metodę, która wywołałaby metodę, która wywołałaby metodę itp., Aż do miejsca, w którym obliczany jest wynik. Oznacza to, że bieżący wynik jest przekazywany przez wszystkie te warstwy tylko dlatego, że funkcja na samym dole obliczy wynik.
Więc teraz mam funkcje z czasami dziesiątkami parametrów. Mógłbym umieścić te parametry w obiekcie, aby zmniejszyć liczbę parametrów, ale wtedy chciałbym, aby ta klasa była główną lokalizacją stanu aplikacji stanu, a nie obiektem, który jest po prostu budowany w momencie wywołania, aby uniknąć przekazania w wielu parametrach, a następnie rozpakuj je.
Zastanawiam się teraz, czy mam problem z tym, że moje funkcje są zbyt głęboko zagnieżdżone. Jest to wynik chęci posiadania małych funkcji, więc refaktoryzuję, gdy funkcja staje się duża, i dzielę ją na wiele mniejszych funkcji. Ale zrobienie tego powoduje głębszą hierarchię i wszystko, co przekazane do funkcji wewnętrznych, musi zostać przekazane do funkcji zewnętrznej, nawet jeśli funkcja zewnętrzna nie działa bezpośrednio na tych obiektach.
Wydawało się, że po prostu przekazanie GameState po drodze pozwoliło uniknąć tego problemu. Ale wracam do pierwotnego problemu polegającego na przekazywaniu większej ilości informacji do funkcji, niż jej potrzebuje.
źródło
Odpowiedzi:
Nie jestem pewien, czy istnieje dobre rozwiązanie. To może, ale nie musi być odpowiedź, ale komentarz jest o wiele za długi. Robiłem podobne rzeczy i pomogły mi następujące sztuczki:
GameState
hierarchicznie, aby otrzymać 3-5 mniejszych części zamiast 15.Nie wydaje mi się Refaktoryzacja na małe funkcje jest słuszna, ale może lepiej je zgrupujesz. Czasami nie jest to możliwe, czasem potrzebuje tylko drugiego (lub trzeciego) spojrzenia na problem.
Porównaj swój projekt ze zmiennym. Czy są rzeczy, które pogorszyły się po przepisaniu? Jeśli tak, to czy nie możesz poprawić ich w taki sam sposób, jak pierwotnie?
źródło
Nie mogę rozmawiać z C #, ale w Haskell skończyłbyś całym stanem. Możesz to zrobić jawnie lub za pomocą monady państwowej. Jedną z rzeczy, które możesz zrobić, aby rozwiązać problem funkcji otrzymujących więcej informacji, niż są one potrzebne, jest użycie klas Has. (Jeśli nie jesteś zaznajomiony, klasy Haskell są trochę podobne do interfejsów C #). Dla każdego elementu E stanu możesz zdefiniować typ HasE, który wymaga funkcji getE, która zwraca wartość E. Monada stanu może być następnie stworzył przykład wszystkich tych klas. Następnie, w swoich rzeczywistych funkcjach, zamiast jawnego wymagania monady państwowej, potrzebujesz dowolnej monady należącej do klas typów Has dla potrzebnych elementów; co ogranicza możliwości tej funkcji przy użyciu używanej monady. Więcej informacji na temat tego podejścia można znaleźć w artykule Michaela Snoymanaopublikować we wzorcu projektowym ReaderT .
Prawdopodobnie możesz powielić coś takiego w C #, w zależności od tego, jak definiujesz stan, który jest przekazywany. Jeśli masz coś takiego
można zdefiniować interfejsy
IHasMyInt
orazIHasMyString
metodamiGetMyInt
iGetMyString
odpowiednio. Klasa stanu wygląda wtedy następująco:wtedy twoje metody mogą wymagać IHasMyInt, IHasMyString lub całego MyState, stosownie do przypadku.
Następnie można użyć ograniczenia where w definicji funkcji, aby można było przekazać obiekt stanu, ale może on uzyskać tylko ciąg znaków i liczbę całkowitą, a nie podwójną.
źródło
Myślę, że dobrze by było, gdybyś dowiedział się o Redux lub Elm i jak radzą sobie z tym pytaniem.
Zasadniczo masz jedną czystą funkcję, która przejmuje cały stan i akcję, którą wykonał użytkownik i zwraca nowy stan.
Ta funkcja wywołuje następnie inne czyste funkcje, z których każda obsługuje określony fragment stanu. W zależności od akcji wiele z tych funkcji może nic nie robić, ale przywraca pierwotny stan bez zmian.
Aby dowiedzieć się więcej, skorzystaj z Google Elm Architecture lub Redux.js.org.
źródło
Myślę, że starasz się używać języka obiektowego w sposób, w jaki nie powinien być używany, tak jakby był to język czysto funkcjonalny. To nie tak, że języki OO były całym złem. Są zalety tego podejścia, dlatego możemy teraz mieszać styl OO ze stylem funkcjonalnym i mieć możliwość uczynienia niektórych elementów kodu funkcjonalnymi, podczas gdy inne pozostają zorientowane obiektowo, abyśmy mogli skorzystać z całej możliwości ponownego użycia, dziedziczenia lub polimofizmu. Na szczęście nie jesteśmy już zobowiązani do żadnego z tych podejść, więc dlaczego próbujesz ograniczyć się do jednego z nich?
Odpowiadając na twoje pytanie: nie, nie układam żadnych konkretnych stanów za pomocą logiki aplikacji, ale używam tego, co jest odpowiednie dla bieżącego przypadku użycia i stosuję dostępne techniki w najbardziej odpowiedni sposób.
C # nie jest jeszcze (jeszcze) gotowy do użycia tak funkcjonalnie, jak chcesz.
źródło