Czy zamknięcia z efektami ubocznymi są uważane za „funkcjonalny styl”?

9

Wiele współczesnych języków programowania obsługuje pewne pojęcia zamknięcia , np. Fragmentu kodu (bloku lub funkcji)

  1. Może być traktowany jako wartość, a zatem przechowywany w zmiennej, przekazywany do różnych części kodu, definiowany w jednej części programu i wywoływany w zupełnie innej części tego samego programu.
  2. Może przechwytywać zmienne z kontekstu, w którym są zdefiniowane, i uzyskiwać do nich dostęp, gdy zostaną później wywołane (być może w zupełnie innym kontekście).

Oto przykład zamknięcia napisanego w Scali:

def filterList(xs: List[Int], lowerBound: Int): List[Int] =
  xs.filter(x => x >= lowerBound)

Literał funkcji x => x >= lowerBoundzawiera wolną zmienną lowerBound, która jest zamknięta (powiązana) argumentem funkcji filterListo tej samej nazwie. Zamknięcie jest przekazywane do metody bibliotecznej filter, która może wywoływać ją wielokrotnie jako normalną funkcję.

Czytałem wiele pytań i odpowiedzi na tej stronie i, o ile rozumiem, termin zamknięcie jest często automatycznie kojarzony z programowaniem funkcjonalnym i stylem programowania funkcjonalnego.

Definicja programowania funkcji na wikipedii brzmi:

W informatyce programowanie funkcjonalne jest paradygmatem programowania, który traktuje obliczenia jako ocenę funkcji matematycznych i unika stanu i zmiennych danych. Podkreśla zastosowanie funkcji, w przeciwieństwie do imperatywnego stylu programowania, który podkreśla zmiany stanu.

i dalej

[...] w kodzie funkcjonalnym wartość wyjściowa funkcji zależy tylko od argumentów wprowadzonych do funkcji [...]. Wyeliminowanie efektów ubocznych może znacznie ułatwić zrozumienie i przewidywanie zachowania programu, co jest jedną z kluczowych motywacji rozwoju programowania funkcjonalnego.

Z drugiej strony wiele konstrukcji zamknięcia dostarczonych przez języki programowania pozwala zamknięciu na przechwytywanie zmiennych nielokalnych i zmianę ich po wywołaniu zamknięcia, powodując w ten sposób efekt uboczny dla środowiska, w którym zostały zdefiniowane.

W tym przypadku zamknięcia wprowadzają pierwszą ideę programowania funkcjonalnego (funkcje są pierwszorzędnymi jednostkami, które można przenosić jak inne wartości), ale pomijają drugą ideę (unikanie skutków ubocznych).

Czy takie użycie zamknięć ze skutkami ubocznymi jest uważane za funkcjonalny styl, czy też zamknięcia są uważane za bardziej ogólną konstrukcję, którą można stosować zarówno w funkcjonalnym, jak i niefunkcjonalnym stylu programowania? Czy jest jakaś literatura na ten temat?

WAŻNA UWAGA

Nie kwestionuję przydatności efektów ubocznych ani zamykania się z efektami ubocznymi. Nie jestem również zainteresowany dyskusją na temat zalet / wad zamknięć z efektami ubocznymi lub bez nich.

Interesuje mnie tylko to, czy stosowanie takich zamknięć jest nadal uważane za styl funkcjonalny przez zwolennika programowania funkcjonalnego, czy też przeciwnie, ich stosowanie jest zniechęcane podczas używania stylu funkcjonalnego.

Giorgio
źródło
3
W czysto funkcjonalnym stylu / języku efekty uboczne są niemożliwe ... więc sądzę, że byłoby to kwestią: na jakim poziomie czystości uważa się kod za funkcjonalny?
Steven Evers,
@SnOrfus: na 100%?
Giorgio
1
Efekty uboczne są niemożliwe w czysto funkcjonalnym języku, więc powinien odpowiedzieć na twoje pytanie.
Steven Evers,
@SnOrfus: W wielu językach zamknięcia są uważane za funkcjonalny konstrukt programistyczny, więc byłem trochę zdezorientowany. Wydaje mi się, że (1) ich użycie jest bardziej ogólne niż tylko wykonywanie FP, i (2) używanie zamknięć nie gwarantuje automatycznie, że używasz funkcjonalnego stylu.
Giorgio
Czy downvoter może zostawić notatkę, jak poprawić to pytanie?
Giorgio

Odpowiedzi:

7

Nie; definicja paradygmatu funkcjonalnego dotyczy braku stanu i domyślnie braku skutków ubocznych. Nie chodzi o funkcje wyższego rzędu, zamknięcia, manipulowanie listami obsługiwanymi językami ani inne funkcje językowe ...

Nazwa programowania funkcjonalnego pochodzi od matematycznego pojęcia funkcji - powtarzane wywołania na tym samym wejściu zawsze dają to samo wyjście - funkcja nullipotentna. Można to osiągnąć tylko wtedy, gdy dane są niezmienne . W celu ułatwienia rozwoju funkcje stały się zmienne (funkcje się zmieniają, dane są nadal niezmienne), a zatem pojęcie funkcji wyższego rzędu (funkcjonałów w matematyce, na przykład pochodnych) - funkcja, która przyjmuje na wejściu inną funkcję. Aby umożliwić przenoszenie funkcji i przekazywanie ich jako argumentów, przyjęto funkcje pierwszej klasy ; następnie, w celu dalszego zwiększenia wydajności, pojawiły się zamknięcia .

Jest to oczywiście bardzo uproszczony widok.

m3th0dman
źródło
3
Funkcje wyższego rzędu nie mają absolutnie nic wspólnego ze zmiennością, podobnie jak funkcje pierwszej klasy. Potrafię z łatwością przekazywać funkcje i konstruować z nich inne funkcje w Haskell, bez żadnych stanów zmiennych ani skutków ubocznych.
tdammers
@tdammers Prawdopodobnie nie wyraziłem bardzo wyraźnie; kiedy powiedziałem, że funkcje stały się zmienne, nie mówiłem o zmienności danych, ale o tym, że zachowanie funkcji można zmienić (funkcją wyższego rzędu).
m3th0dman
Funkcja wyższego rzędu nie musi zmieniać istniejącej funkcji; większość przykładów podręczników łączy istniejące funkcje w nowe funkcje bez ich modyfikacji - weźmy mapna przykład, która bierze funkcję, stosuje ją do listy i zwraca listę wyników. mapnie modyfikuje żadnego z argumentów, nie zmienia zachowania funkcji, którą przyjmuje za argument, ale jest to zdecydowanie funkcja wyższego rzędu - jeśli zastosujesz ją częściowo, używając tylko parametru funkcji, zbudowałeś nowa funkcja, która działa na liście, ale nadal nie nastąpiła mutacja.
tdammers
@tdammers: Dokładnie: zmienność jest używana tylko w językach rozkazujących lub w paradygmacie. Chociaż mogą mieć pojęcie funkcji wyższego rzędu (lub metody) i zamknięcia, porzucają niezmienność. Może to być przydatne (nie mówię, że nie należy tego robić), ale moje pytanie brzmi, czy nadal można nazwać tę funkcję. Co oznaczałoby, że zamknięcie nie jest pojęciem ściśle funkcjonalnym.
Giorgio
@Giorgio: większość klasycznych językach FP zrobić mieć zmienność; Haskell jest jedynym, który mogę wymyślić z czubka głowy, który w ogóle nie pozwala na zmienność. Mimo to unikanie stanu zmiennego jest ważną wartością w FP.
tdammers
9

Nie. „Styl funkcjonalny” oznacza programowanie bez skutków ubocznych.

Aby zobaczyć, dlaczego, spójrz na wpis Erica Lipperta na temat ForEach<T>metody rozszerzenia i dlaczego Microsoft nie umieścił metody sekwencji takiej jak w Linq :

Jestem filozoficznie przeciwny oferowaniu takiej metody z dwóch powodów.

Pierwszym powodem jest to, że takie postępowanie narusza zasady programowania funkcjonalnego, na których oparte są wszystkie pozostałe operatory sekwencji. Oczywiście jedynym celem wywołania tej metody jest wywołanie skutków ubocznych. Celem wyrażenia jest obliczenie wartości, a nie wywołanie efektu ubocznego. Celem oświadczenia jest wywołanie efektu ubocznego. Witryna wywoływania tej rzeczy wyglądałaby okropnie podobnie do wyrażenia (chociaż, co prawda, ponieważ metoda ta zwraca wartość pustki, wyrażenie to może być użyte tylko w kontekście wyrażenia wyrażenia). Nie pasuje mi to, że stworzyć jedyny operator sekwencji, który będzie użyteczny tylko ze względu na efekty uboczne.

Drugi powód jest taki, że dodaje to zerowej mocy reprezentacyjnej do języka. W ten sposób możesz przepisać ten idealnie czysty kod:

foreach(Foo foo in foos){ statement involving foo; }

do tego kodu:

foos.ForEach((Foo foo)=>{ statement involving foo; });

który używa prawie dokładnie tych samych znaków w nieco innej kolejności. A jednak druga wersja jest trudniejsza do zrozumienia, trudniejsza do debugowania i wprowadza semantykę zamknięcia, potencjalnie zmieniając w ten sposób czas życia obiektu w subtelny sposób.

Robert Harvey
źródło
1
Chociaż zgadzam się z nim ... jego argument nieco słabnie, gdy się zastanawia ParallelQuery<T>.ForAll(...). Realizacja takiego IEnumerable<T>.ForEach(...)jest niezwykle przydatna do debugowania ForAlloświadczenia (wymienić ForAllz ForEachi usunąć AsParallel()i można znacznie łatwiej prześledzić / debug IT)
Steven Evers
Najwyraźniej Joe Duffy ma różne pomysły. : D
Robert Harvey
Scala również nie wymusza czystości: możesz przekazać nieczyste zamknięcie funkcji wyższego rzędu. Mam wrażenie, że idea zamknięcia nie jest specyficzna dla programowania funkcjonalnego, ale jest bardziej ogólna.
Giorgio
5
@Giorgio: Zamknięcia nie muszą być czyste, aby nadal być uważane za zamknięcia. Muszą być jednak czyste, aby można je było uznać za „styl funkcjonalny”.
Robert Harvey,
3
@Giorgio: Naprawdę przydatne jest zamknięcie stanu zmiennego i efektów ubocznych i przekazanie go innej funkcji. Jest to po prostu sprzeczne z celami programowania funkcjonalnego. Myślę, że wiele zamieszania wiąże się z eleganckim wsparciem języka dla lambd, które są powszechne w językach funkcjonalnych.
GlenPeterson
0

Programowanie funkcjonalne z pewnością przenosi funkcje pierwszej klasy na kolejny poziom pojęciowy, ale deklarowanie funkcji anonimowych lub przekazywanie funkcji innym funkcjom niekoniecznie jest funkcją programowania funkcjonalnego. W C wszystko było liczbą całkowitą. Liczba, wskaźnik do danych, wskaźnik do funkcji ... wszystko tylko ints. Możesz przekazywać wskaźniki funkcji do innych funkcji, tworzyć listy wskaźników funkcji ... Heck, jeśli pracujesz w języku asemblera, funkcje są tak naprawdę tylko adresami w pamięci, w których przechowywane są bloki instrukcji maszyny. Nadanie funkcji nazwy jest dodatkowym obciążeniem dla osób, które potrzebują kompilatora do pisania kodu. W związku z tym funkcje były „pierwszej klasy” w całkowicie niefunkcjonalnym języku.

Jeśli jedyne, co robisz, to obliczanie wzorów matematycznych w REPL, możesz być funkcjonalnie czysty ze swoim językiem. Ale większość programów biznesowych ma skutki uboczne. Utrata pieniędzy w oczekiwaniu na zakończenie długotrwałego programu to efekt uboczny. Podejmowanie jakichkolwiek działań zewnętrznych: zapisywanie do pliku, aktualizowanie bazy danych, rejestrowanie zdarzeń w kolejności itp. Wymaga zmiany stanu. Możemy dyskutować o tym, czy stan jest naprawdę zmieniony, jeśli umieścisz te działania w niezmiennych opakowaniach, które odpychają skutki uboczne, aby Twój kod nie musiał się o nie martwić. Ale to tak, jakby dyskutować, czy drzewo wydaje hałas, jeśli pada w lesie, a nikt go nie słyszy. Faktem jest, że drzewo zaczęło się wyprostować i skończyło na ziemi. Stan zmienia się, gdy rzeczy się skończą, nawet jeśli tylko po to, aby zgłosić, że coś się skończyło.

Pozostaje nam więc skala funkcjonalnej czystości, nie czerni i bieli, ale odcienie szarości. W tej skali im mniej skutków ubocznych, tym mniejsza zmienność, tym lepiej (bardziej funkcjonalnie).

Jeśli absolutnie potrzebujesz efektu ubocznego lub stanu mutable w swoim skądinąd funkcjonalnym kodzie, dążysz do enkapsulacji lub z reszty twojego programu najlepiej jak potrafisz. Używanie zamknięcia (lub czegokolwiek innego) w celu wstrzyknięcia efektów ubocznych lub stanu zmiennego w poza tym czyste funkcje jest przeciwieństwem programowania funkcjonalnego. Jedynym wyjątkiem może być to, że zamknięcie było najskuteczniejszym sposobem na zamknięcie skutków ubocznych kodu, do którego jest przekazywany. Nadal nie jest to „programowanie funkcjonalne”, ale może być szafą, którą można uzyskać w określonych sytuacjach.

GlenPeterson
źródło