Czy podczas programowania w stylu funkcjonalnym masz stan pojedynczej aplikacji, który splatasz z logiką aplikacji?

12

Jak zbudować system, który ma wszystkie następujące elementy :

  1. Używanie czystych funkcji z niezmiennymi obiektami.
  2. Przekaż tylko dane funkcji, których potrzebuje, nie więcej (tj. Nie ma dużego obiektu stanu aplikacji)
  3. Unikaj posiadania zbyt wielu argumentów do funkcji.
  4. 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.

Daisha Lynn
źródło
1
Nie jestem ekspertem od projektowania i szczególnie funkcjonalnym, ale skoro twoja gra z natury ma stan ewoluujący, czy jesteś pewien, że programowanie funkcjonalne jest paradygmatem pasującym do wszystkich warstw twojej aplikacji?
Walfrat
Walfrat, myślę, że jeśli porozmawiasz z ekspertami w zakresie programowania funkcjonalnego, prawdopodobnie przekonasz się, że powiedzieliby, że paradygmat programowania funkcjonalnego zawiera rozwiązania do zarządzania ewoluującym stanem.
Daisha Lynn,
Twoje pytanie wydawało mi się szersze, że dotyczy tylko. Jeśli chodzi tylko o zarządzanie stanami, tutaj jest początek: zobacz odpowiedź i link w stackoverflow.com/questions/1020653/...
Walfrat
2
@DaishaLynn Nie sądzę, że powinieneś usunąć pytanie. Zostało to ocenione pozytywnie i nikt nie próbuje go zamknąć, więc nie sądzę, że jest poza zasięgiem tej witryny. Dotychczasowy brak odpowiedzi może wynikać tylko z tego, że wymaga specjalistycznej wiedzy niszowej. Ale to nie znaczy, że w końcu nie zostanie znalezione i nie będzie odpowiedzi.
Ben Aaronson
2
Zarządzanie stanem zmiennym w złożonym czysto funkcjonalnym programie bez znaczącej pomocy językowej jest ogromnym bólem. W Haskell jest to możliwe z powodu monad, zwięzłej składni, bardzo dobrego wnioskowania o typie, ale wciąż może być bardzo denerwujące. W C # myślę, że miałbyś znacznie więcej kłopotów.
Przywróć Monikę

Odpowiedzi:

2

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:

  • Podziel GameStatehierarchicznie, aby otrzymać 3-5 mniejszych części zamiast 15.
  • Niech implementuje interfejsy, aby twoje metody widziały tylko potrzebne części. Nigdy nie rzucaj ich z powrotem, ponieważ skłamałbyś sobie na temat prawdziwego typu.
  • Pozwól również częściom wdrożyć interfejsy, abyś miał pełną kontrolę nad tym, co przekazujesz.
  • Używaj obiektów parametrów, ale rób to oszczędnie i spróbuj zamienić je w rzeczywiste obiekty o własnym zachowaniu.
  • Czasami przekazanie nieco więcej niż potrzeba jest lepsze niż długa lista parametrów.

Zastanawiam się teraz, czy mam problem z tym, że moje funkcje są zbyt głęboko zagnieżdżone.

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?

maaartinus
źródło
Ktoś kazał mi zmienić projekt, aby każda funkcja wymagała tylko jednego parametru, aby móc używać curry. Próbowałem tej jednej funkcji, więc zamiast wywoływać DeleteEntity (a, b, c), teraz wywołuję DeleteEntity (a) (b) (c). To urocze i ma sprawić, że będzie to bardziej złożone, ale po prostu jeszcze tego nie rozumiem.
Daisha Lynn
@DaishaLynn Używam Javy i nie ma słodkiego cukru syntaktycznego do curry, więc (dla mnie) nie warto próbować. Jestem raczej sceptycznie nastawiony do możliwego użycia funkcji wyższego rzędu w naszym przypadku, ale daj mi znać, czy to zadziałało.
maaartinus
2

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

public class MyState
{
    public int MyInt {get; set; }
    public string MyString {get; set; }
}

można zdefiniować interfejsy IHasMyIntoraz IHasMyStringmetodami GetMyInti GetMyStringodpowiednio. Klasa stanu wygląda wtedy następująco:

public class MyState : IHasMyInt, IHasMyString
{
    public int MyInt {get; set; }
    public string MyString {get; set; }
    public double MyDouble {get; set; }

    public int GetMyInt () 
    {
        return MyInt;
    }

    public string GetMyString ()
    {
        return MyString;
    }

    public double GetMyDouble ()
    {
        return MyDouble;
    }
}

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ą.

public static T DoSomething<T>(T state) where T : IHasMyString, IHasMyInt
{
    var s = state.GetMyString();
    var i = state.GetMyInt();
    return state;
}
DylanSp
źródło
To interesujące. Tak więc obecnie, gdzie przechodzę, wywołuję funkcję i przekazuję 10 parametrów według wartości, przekazuję „gameSt'ate” 10 razy, ale do 10 różnych typów parametrów, takich jak „IHasGameScore”, „IHasGameBoard” itp. Chciałbym tam był sposobem na przekazanie jednego parametru, który może wskazywać funkcja musi implementować wszystkie interfejsy w tym jednym typie. Zastanawiam się, czy mogę to zrobić z „ogólnym ograniczeniem”. Pozwól mi spróbować.
Daisha Lynn
1
Zadziałało. Tutaj działa: dotnetfiddle.net/cfmDbs .
Daisha Lynn
1

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.

Daniel T.
źródło
Nie znam Elma, ale uważam, że jest podobny do Redux. Czy w Redux nie są wywoływane wszystkie reduktory przy każdej zmianie stanu? Brzmi wyjątkowo nieefektywnie.
Daisha Lynn
Jeśli chodzi o optymalizacje niskiego poziomu, nie zakładaj, mierz. W praktyce jest wystarczająco szybko.
Daniel T.
Dzięki Daniel, ale to nie zadziała dla mnie. Zrobiłem już wystarczająco dużo prac, aby wiedzieć, że nie powoduje powiadamiania każdego komponentu w interfejsie użytkownika przy każdej zmianie danych, niezależnie od tego, czy kontrola dba o kontrolę.
Daisha Lynn,
-2

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.

t3chb0t
źródło
3
Nie jestem zachwycony tą odpowiedzią ani tonem. Nic nie nadużywam. Przekraczam granice C #, aby użyć go jako bardziej funkcjonalnego języka. Nie jest to rzadkie. Wydaje się, że jesteś temu filozoficznie przeciwny, co jest w porządku, ale w takim przypadku nie patrz na to pytanie. Twój komentarz nikomu się nie przyda. Pójść dalej.
Daisha Lynn
@DaishaLynn się mylisz, nie jestem w żaden sposób temu przeciwny i faktycznie używam go dużo ... ale tam, gdzie jest to naturalne i możliwe, i nie próbuję zmienić języka OO w funkcjonalny, ponieważ jest on modny aby to zrobić. Nie musisz zgadzać się z moją odpowiedzią, ale to nie zmienia faktu, że nie używasz właściwie swoich narzędzi.
t3chb0t
Nie robię tego, ponieważ jest to bardzo modne. Sam C # zmierza w kierunku użycia funkcjonalnego stylu. Sam Anders Hejlsberg wskazał jako taki. Rozumiem, że interesuje Cię tylko używanie języka w głównym strumieniu i rozumiem, dlaczego i kiedy jest to właściwe. Po prostu nie wiem, dlaczego ktoś taki jak ty jest w tym temacie. Jak naprawdę pomagasz?
Daisha Lynn
@ DaishaLynn, jeśli nie możesz poradzić sobie z odpowiedziami krytykującymi twoje pytanie lub podejście, prawdopodobnie nie powinieneś zadawać tutaj pytań lub następnym razem powinieneś po prostu dodać oświadczenie, że jesteś zainteresowany odpowiedziami wspierającymi Twój pomysł w 100%, ponieważ nie chcę usłyszeć prawdę, ale raczej uzyskać wsparcie.
t3chb0t
Bądźcie wobec siebie bardziej serdeczni. Krytyka jest możliwa bez dyskredytującego języka. Próba zaprogramowania C # w funkcjonalnym stylu z pewnością nie jest „nadużyciem” ani przypadkiem. Jest to powszechna technika używana przez wielu programistów C # do nauki z innych języków.
zumalifeguard