Po co zniechęcać do korzystania z operatora przypisania lub pętli w programowaniu funkcjonalnym?

9

Jeśli moja funkcja spełnia poniżej dwóch wymagań, uważam, że funkcja Sum zwraca sumę pozycji na liście, na której pozycja ocenia się na prawdę, jeśli dany warunek można uznać za funkcję czystą, prawda?

1) Dla danego zestawu i / p zwracane jest to samo o / p, niezależnie od czasu wywołania funkcji

2) Nie ma żadnych skutków ubocznych

public int Sum(Func<int,bool> predicate, IEnumerable<int> numbers){
    int result = 0;
    foreach(var item in numbers)
        if(predicate(item)) result += item;
    return result;
}

Przykład: Sum(x=>x%2==0, new List<int> {1,2,3,4,5...100});

Powód, dla którego zadaję to pytanie, wynika z tego, że prawie wszędzie widzę ludzi, którzy powinni unikać operatora przypisania i pętli, ponieważ jest to konieczny styl programowania. Co więc może pójść nie tak z powyższym przykładem, który wykorzystuje pętle i operator przypisania w kontekście programowania funkcji?

rahulaga_dev
źródło
1
Nie ma żadnego efektu ubocznego - ma efekt uboczny, gdy itemzmienna zmutowana w pętli.
Fabio
@Fabio ok. Ale czy możesz opracować zakres skutków ubocznych?
rahulaga_dev

Odpowiedzi:

16

Co w programowaniu funkcjonalnym robi różnicę?

Programowanie funkcjonalne jest z zasady deklaratywne . Mówisz, jaki jest twój wynik, a nie jak go obliczyć.

Rzućmy okiem na naprawdę funkcjonalną implementację twojego fragmentu kodu. W Haskell byłoby to:

predsum pred numbers = sum (filter pred numbers)

Czy jasne jest, jaki jest wynik? Jest to suma liczb spełniających orzeczenie. Jak to jest obliczane? Nie obchodzi mnie to, zapytaj kompilatora.

Można powiedzieć, że używanie sumi filterjest podstępem i nie ma znaczenia. Pozwól to wdrożyć bez tych pomocników (chociaż najlepszym sposobem byłoby ich wdrożenie w pierwszej kolejności).

Rozwiązanie „Functional Programming 101”, które nie korzysta sumz rekurencji:

sum pred list = 
    case list of
        [] -> 0
        h:t -> if pred h then h + sum pred t
                         else sum pred t

Nadal jest całkiem jasne, jaki jest wynik pod względem wywołania jednej funkcji. Jest albo 0, albo recursive call + h or 0, w zależności od pred h. Nadal dość proste, nawet jeśli wynik końcowy nie jest od razu oczywisty (choć przy odrobinie praktyki to naprawdę brzmi jak forpętla).

Porównaj to ze swoją wersją:

public int Sum(Func<int,bool> predicate, IEnumerable<int> numbers){
    int result = 0;
    foreach(var item in numbers)
        if (predicate(item)) result += item;
    return result;
}

Jaki jest wynik? O, widzę: pojedynczy returnrachunek, nie zaskakuje tutaj: return result.

Ale co to jest result? int result = 0? To nie wydaje się właściwe. Zrobisz coś później 0. Ok, dodajesz items. I tak dalej.

Oczywiście dla większości programistów jest to dość oczywiste, co dzieje się w takiej prostej funkcji, ale dodaj jakieś dodatkowe returnstwierdzenie i nagle trudniej będzie je wyśledzić. Cały kod dotyczy tego , jak i co pozostało czytelnikowi, aby się zorientować - jest to zdecydowanie bardzo imperatywny styl .

Czy zmienne i pętle są nieprawidłowe?

Nie.

Istnieje wiele rzeczy, które są o wiele łatwiejsze do wyjaśnienia, i wiele algorytmów, które wymagają zmiennego stanu, aby były szybkie. Ale zmienne są z natury bezwzględnie konieczne, wyjaśniając, jak zamiast tego , i dając niewielkie przewidywanie, jaka może być ich wartość, kilka linii później lub po kilku iteracjach pętli. Pętle generalnie wymagają, aby państwo miało sens, dlatego też są z natury bezwzględnie konieczne.

Zmienne i pętle nie są po prostu programowaniem funkcjonalnym.

Podsumowanie

Współczesne programowanie funkcyjne to nieco więcej stylu i użyteczny sposób myślenia niż paradygmat. W tym sposobie myślenia zdecydowanie preferowane są czyste funkcje, ale tak naprawdę to tylko niewielka część.

Większość rozpowszechnionych języków pozwala korzystać z niektórych funkcjonalnych konstrukcji. Na przykład w Pythonie możesz wybierać między:

result = 0
for num in numbers:
    if pred(result):
        result += num
return result

lub

return sum(filter(pred, numbers))

lub

return sum(n for n in numbers if pred(n))

Te wyrażenia funkcjonalne dobrze pasują do tego rodzaju problemów i po prostu skracają kod (a krótszy jest dobry ). Nie powinieneś bezmyślnie zastępować ich imperatywnym kodem, ale gdy pasują, prawie zawsze są lepszym wyborem.

Frax
źródło
dziękuję za miłe wyjaśnienie !!
rahulaga_dev
1
@RahulAgarwal Ta odpowiedź może być dla Ciebie interesująca, ładnie oddaje styczną koncepcję ustalania prawd kontra opisywanie kroków. Podoba mi się także wyrażenie „języki deklaratywne zawierają skutki uboczne, podczas gdy języki imperatywne nie” - zwykle programy funkcjonalne mają czyste i bardzo widoczne cięcia między stanowym kodem zajmującym się światem zewnętrznym (lub wykonującym pewien zoptymalizowany algorytm) a kodem czysto funkcjonalnym.
Frax,
1
@Frax: dzięki !! Zapoznam się z tym. Niedawno natknąłem się również na wykład Richa Hickeya na temat wartości . Myślę, że zasada jednego kciuka - „pracuj z wartościami i wyrażeniami, zamiast pracować z czymś, co ma wartość i może się zmienić”
rahulaga_dev
1
@Frax: Można też powiedzieć, że FP jest abstrakcją nad imperatywnym programowaniem - ponieważ ostatecznie ktoś musi pouczyć maszynę o tym, jak to zrobić, prawda? Jeśli tak, to czy programowanie imperatywne nie ma większej kontroli na niskim poziomie w porównaniu do FP?
rahulaga_dev
1
@Frax: Zgadzam się z Rahulem, że imperatyw ma niższy poziom w tym sensie, że jest bliżej podstawowej maszyny. Gdyby sprzęt mógł wykonywać kopie danych bez żadnych kosztów, nie potrzebowalibyśmy destrukcyjnych aktualizacji w celu poprawy wydajności. W tym sensie paradygmat imperatywny jest bliższy metalowi.
Giorgio
9

W programowaniu funkcjonalnym odradza się stosowanie stanu zmiennego. Pętle są w związku z tym zniechęcane, ponieważ pętle są użyteczne tylko w połączeniu ze stanem zmiennym.

Funkcja jako całość jest czysta, co jest świetne, ale paradygmat programowania funkcjonalnego ma zastosowanie nie tylko na poziomie całych funkcji. Chcemy także unikać stanu zmiennego również na poziomie lokalnym, wewnątrz funkcji. Rozumowanie jest w zasadzie takie samo: unikanie stanu zmiennego ułatwia zrozumienie kodu i zapobiega niektórym błędom.

W twoim przypadku możesz napisać, numbers.Where(predicate).Sum()co jest znacznie prostsze. A prostsze oznacza mniej błędów.

JacquesB
źródło
dzięki !! Wydaje mi się, że brakowało mi linii uderzającej - ale paradygmat programowania funkcjonalnego dotyczy nie tylko całego poziomu funkcji, ale teraz zastanawiam się również, jak wizualizować tę granicę. Zasadniczo z punktu widzenia konsumenta jest to czysta funkcja, ale programista, który napisał tę funkcję, nie przestrzegał wytycznych dotyczących czystej funkcji? zmieszany :(
rahulaga_dev
@RahulAgarwal: Jaka granica?
JacquesB
Jestem w pewnym sensie zdezorientowany, czy paradygmat programowania kwalifikuje się jako FP z perspektywy konsumenta funkcji? Bcoz, jeśli spojrzę na implementację implementacji Wherein numbers.Where(predicate).Sum()- korzysta z foreachpętli.
rahulaga_dev
3
@RahulAgarwal: Jako konsument funkcji, tak naprawdę nie obchodzi cię, czy funkcja lub moduł wewnętrznie używa stanu zmiennego, o ile jest on z zewnątrz czysty.
JacquesB
7

Chociaż masz rację, że z punktu widzenia zewnętrznego obserwatora twoja Sumfunkcja jest czysta, wewnętrzna implementacja wyraźnie nie jest czysta - masz zapisany stan, w resultktórym wielokrotnie mutujesz. Jednym z powodów unikania stanu zmiennego jest to, że powoduje ono większe obciążenie poznawcze programisty, co z kolei prowadzi do większej liczby błędów [potrzebne cytowanie] .

Podczas gdy w takim prostym przykładzie, ilość przechowywanego stanu zmiennego jest prawdopodobnie wystarczająco mała, aby nie powodować żadnych poważnych problemów, nadal obowiązuje ogólna zasada. Przykład zabawkowy jak Sumprawdopodobnie nie jest najlepszym sposobem zilustrowania przewagi programowania funkcjonalnego nad imperatywnym - spróbuj zrobić coś z wieloma zmiennymi stanami, a zalety mogą stać się wyraźniejsze.

Philip Kendall
źródło