Czytając słynny SICP, stwierdziłem, że autorzy raczej niechętnie przedstawiają oświadczenie o przypisaniu do Schematu w Rozdziale 3. Czytam tekst i rozumiem, dlaczego tak się czują.
Ponieważ Scheme jest pierwszym funkcjonalnym językiem programowania, o którym coś wiem, jestem trochę zaskoczony, że niektóre funkcjonalne języki programowania (nie oczywiście Scheme) mogą obejść się bez zadań.
Użyjmy przykładu z książki bank account
. Jeśli nie ma instrukcji przypisania, jak można to zrobić? Jak zmienić balance
zmienną? Pytam o to, ponieważ wiem, że istnieje kilka tak zwanych czysto funkcjonalnych języków i zgodnie z pełną teorią Turinga, to też trzeba zrobić.
Nauczyłem się C, Java, Python i często używam zadań w każdym napisanym przeze mnie programie. To naprawdę otwierające oczy doświadczenie. Naprawdę mam nadzieję, że ktoś może krótko wyjaśnić, w jaki sposób unika się zadań w tych funkcjonalnych językach programowania i jaki głęboki wpływ (jeśli w ogóle) ma na te języki.
Powyższy przykład znajduje się tutaj:
(define (make-withdraw balance)
(lambda (amount)
(if (>= balance amount)
(begin (set! balance (- balance amount))
balance)
"Insufficient funds")))
Zmieniło to balance
By set!
. Dla mnie to bardzo przypomina klasową metodę zmiany członka klasy balance
.
Jak powiedziałem, nie znam funkcjonalnych języków programowania, więc jeśli powiedziałem coś złego na ich temat, śmiało to zaznaczam.
set!
lub innych funkcji, które kończą się na!
. Gdy poczujesz się z tym dobrze, przejście na czysty FP powinno być łatwiejsze.Odpowiedzi:
Nie można zmieniać zmiennych bez pewnego operatora przypisania.
Nie do końca. Jeśli język jest kompletny, oznacza to, że może obliczyć wszystko, co może obliczyć każdy inny kompletny język Turinga. Nie oznacza to, że musi mieć wszystkie funkcje, które mają inne języki.
Nie jest sprzecznością, że kompletny język programowania Turinga nie ma możliwości zmiany wartości zmiennej, o ile dla każdego programu, który ma zmienne zmienne, możesz napisać równoważny program, który nie ma zmiennych zmiennych (gdzie „równoważny” oznacza że oblicza to samo). W rzeczywistości każdy program można tak napisać.
Jeśli chodzi o twój przykład: w czysto funkcjonalnym języku po prostu nie byłbyś w stanie napisać funkcji, która zwraca inne saldo konta przy każdym wywołaniu. Ale nadal będziesz mógł przepisać każdy program, który korzysta z takiej funkcji, w inny sposób.
Ponieważ poprosiłeś o przykład, rozważmy imperatywny program, który używa twojej funkcji wycofania (w pseudo-kodzie). Ten program pozwala użytkownikowi na wypłatę z konta, wpłatę na konto lub sprawdzenie kwoty pieniędzy na koncie:
Oto sposób na napisanie tego samego programu bez użycia zmiennych zmiennych (nie zawracam sobie głowy referencyjnie przezroczystym IO, ponieważ nie chodziło o to):
Tę samą funkcję można również napisać bez użycia rekurencji, używając zagięcia nad danymi wejściowymi użytkownika (co byłoby bardziej idiomatyczne niż rekurencja jawna), ale nie wiem, czy znasz już fałdy, więc napisałem to w sposób, który nie wykorzystuje niczego, czego jeszcze nie wiesz.
źródło
newBalance = startingBalance + sum(deposits) - sum(withdrawals)
.Masz rację, że wygląda bardzo podobnie do metody na obiekcie. To dlatego, że w zasadzie to jest to.
lambda
Zadaniem jest zamknięcie, które ciągnie zmiennej zewnętrznejbalance
do jej zastosowania. Posiadanie wielu zamknięć, które zamykają się na te same zewnętrzne zmienne (zmienne) i wiele metod na tym samym obiekcie, to dwie różne abstrakcje do robienia dokładnie tego samego, i jedno z nich można zaimplementować w kategoriach drugiego, jeśli zrozumiesz oba paradygmaty.Sposób, w jaki czysto funkcjonalne języki radzą sobie ze stanem, jest oszukiwaniem. Na przykład w Haskell, jeśli chcesz odczytać dane wejściowe z zewnętrznego źródła (co jest oczywiście niedeterministyczne i niekoniecznie da ten sam wynik dwa razy, jeśli go powtórzysz), używa sztuczki monadowej, aby powiedzieć „mamy dostałem inną udawaną zmienną, która reprezentuje stan całej reszty świata , i nie możemy zbadać jej bezpośrednio, ale odczyt danych wejściowych jest czystą funkcją, która przyjmuje stan świata zewnętrznego i zwraca deterministyczne dane wejściowe, które dokładnie ten stan zawsze będzie renderować plus nowy stan świata zewnętrznego. ” (To oczywiście uproszczone wyjaśnienie. Czytanie, jak to naprawdę działa, poważnie złamie ci mózg.)
Lub w przypadku problemu z kontem bankowym, zamiast przypisywać nową wartość do zmiennej, może zwrócić nową wartość jako wynik funkcji, a następnie osoba dzwoniąca musi sobie z tym poradzić w funkcjonalny sposób, na ogół poprzez odtworzenie dowolnych danych która odwołuje się do tej wartości w nowej wersji zawierającej zaktualizowaną wartość. (To nie jest tak nieporęczna operacja, jak mogłoby się wydawać, jeśli dane są skonfigurowane z odpowiednią strukturą drzewa).
źródło
b = makeWithdraw(42); b(1); b(2); b(3); print(b(4))
możesz po prostu zrobić,b = 42; b1 = withdraw(b1, 1); b2 = withdraw(b1, 2); b3 = withdraw(b2, 3); print(withdraw(b3, 4));
gdziewithdraw
jest po prostu zdefiniowane jakowithdraw(balance, amount) = balance - amount
.„Operatory wielokrotnego przypisania” to jeden przykład funkcji językowej, która ogólnie ma skutki uboczne i jest niezgodna z niektórymi przydatnymi właściwościami języków funkcjonalnych (takimi jak leniwa ocena).
Nie oznacza to jednak, że przypisanie ogólnie jest niezgodne z czystym funkcjonalnym stylem programowania (patrz na przykład niniejsza dyskusja ), ani nie oznacza to, że nie można zbudować składni, która pozwala na działania, które wyglądają jak przypisania w ogóle, są wdrażane bez skutków ubocznych. Tworzenie takiej składni i pisanie w niej wydajnych programów jest jednak czasochłonne i trudne.
W twoim konkretnym przykładzie masz rację - zestaw! operator to zadanie. To nie wolny operator efektem ubocznym, i jest to miejsce, gdzie przerwy Scheme z czysto funkcjonalnego podejścia do programowania.
Ostatecznie, każdy język czysto funkcjonalny będzie musiał zerwać z czysto funkcjonalne podejście kiedyś - zdecydowana większość przydatnych programów zrobić mieć skutki uboczne. Decyzja o tym, gdzie to zrobić, jest zwykle kwestią wygody, a projektanci języków będą starali się dać programistom najwyższą elastyczność w podejmowaniu decyzji, gdzie należy zerwać z czysto funkcjonalnym podejściem, stosownie do ich programu i dziedziny problemów.
źródło
W czysto funkcjonalnym języku programowałby obiekt konta bankowego jako funkcję transformatora strumienia. Obiekt jest uważany za funkcję od nieskończonego strumienia żądań od właścicieli kont (lub kogokolwiek) do potencjalnie nieskończonego strumienia odpowiedzi. Funkcja rozpoczyna się od początkowego salda i przetwarza każde żądanie w strumieniu wejściowym, aby obliczyć nowe saldo, które jest następnie przekazywane do wywołania rekurencyjnego w celu przetworzenia pozostałej części strumienia. (Pamiętam, że SICP omawia paradygmat transformatora strumieniowego w innej części książki).
Bardziej rozbudowana wersja tego paradygmatu nazywa się „funkcjonalnym programowaniem reaktywnym” omawianym tutaj na StackOverflow .
Naiwny sposób robienia transformatorów strumieniowych ma pewne problemy. Możliwe jest (w zasadzie dość łatwe) pisanie błędnych programów, które utrzymują wszystkie stare żądania, marnując miejsce. Co ważniejsze, możliwe jest uzależnienie odpowiedzi na bieżące żądanie od przyszłych wniosków. Obecnie trwają prace nad rozwiązaniem tych problemów. Neel Krishnaswami jest siłą stojącą za nimi.
Zastrzeżenie : Nie należę do kościoła czystego programowania funkcjonalnego. W rzeczywistości nie należę do żadnego kościoła :-)
źródło
Nie można uczynić programu w 100% funkcjonalnym, jeśli ma on zrobić coś pożytecznego. (Jeśli skutki uboczne nie są potrzebne, cała myśl mogłaby zostać zredukowana do stałego czasu kompilacji). Podobnie jak w przykładzie wycofania możesz uczynić większość procedur funkcjonalnymi, ale w końcu będziesz potrzebować procedur, które mają skutki uboczne (wkład użytkownika, wyjście do konsoli). To powiedziawszy, możesz uczynić większość swojego kodu funkcjonalnym, a ta część będzie łatwa do przetestowania, nawet automatycznie. Następnie tworzysz kod niezbędny do wykonania operacji wejścia / wyjścia / bazy danych / ..., który wymagałby debugowania, ale utrzymanie większości kodu w czystości nie będzie zbyt trudne. Wykorzystam twój przykład wypłaty:
Można zrobić to samo w prawie każdym języku i uzyskać te same wyniki (mniej błędów), chociaż może być konieczne ustawienie zmiennych tymczasowych w ramach procedury, a nawet mutowanie elementów, ale to nie ma znaczenia tak długo, jak procedura faktycznie działa funkcjonalnie (same parametry determinują wynik). Wierzę, że staniesz się lepszym programistą w dowolnym języku po tym, jak zaprogramujesz trochę LISP :)
źródło
Przypisanie jest niepoprawną operacją, ponieważ dzieli przestrzeń stanu na dwie części, przed przypisaniem i po przypisaniu. Powoduje to trudności ze śledzeniem, jak zmienne są zmieniane podczas wykonywania programu. Następujące rzeczy w językach funkcjonalnych zastępują zadania:
źródło