Czytałem artykuł o programowaniu funkcjonalnym, w którym pisze autor
(take 25 (squares-of (integers)))
Zauważ, że nie ma żadnych zmiennych. Rzeczywiście, ma tylko trzy funkcje i jedną stałą. Spróbuj napisać kwadraty liczb całkowitych w Javie bez użycia zmiennej. Och, prawdopodobnie jest na to sposób, ale z pewnością nie jest to naturalne i nie przeczytałoby tak ładnie, jak mój program powyżej.
Czy można to osiągnąć w Javie? Zakładając, że musisz wydrukować kwadraty pierwszych 15 liczb całkowitych, czy możesz napisać pętlę for lub while bez używania zmiennych?
Powiadomienie o modzie
To pytanie nie jest konkursem golfowym. Szukamy odpowiedzi, które wyjaśniają związane z nimi pojęcia (najlepiej bez powtarzania wcześniejszych odpowiedzi), a nie tylko dla kolejnego fragmentu kodu.
java
functional-programming
variables
minusSeven
źródło
źródło
squaresOf(integers()).take(25)
(pisanie tych funkcji pozostawia się jako ćwiczenie dla czytelnika; trudność leży w nieskończonym zestawieintegers()
, ale jest to problem dla Javy ze względu na jego chętną ocenę, nie ma nic wspólnego z zmienne)Odpowiedzi:
Czy można zaimplementować taki przykład w Javie bez użycia destrukcyjnych aktualizacji? Tak. Jednak, jak wspomniano @Titon i sam artykuł, będzie brzydki (w zależności od gustu). Jednym ze sposobów jest użycie rekurencji; oto przykład Haskella, który robi coś podobnego:
Uwaga 1) brak mutacji, 2) zastosowanie rekurencji oraz 3) brak pętli. Ostatnia kwestia jest bardzo ważna - języki funkcjonalne nie wymagają wbudowanych w język konstrukcji zapętlonych, ponieważ rekurencja może być używana w większości (wszystkich?) Przypadków, w których pętle są używane w Javie. Oto dobrze znana seria artykułów pokazujących, jak niezwykle ekspresyjne mogą być wywołania funkcji.
Ten artykuł był dla mnie niezadowalający i chciałbym przedstawić kilka dodatkowych uwag:
Ten artykuł jest bardzo kiepskim i mylącym wyjaśnieniem programowania funkcjonalnego i jego zalet. Zdecydowanie poleciłbym inne źródła do nauki o programowaniu funkcjonalnym.
Najbardziej mylącą częścią tego artykułu jest to, że nie wspomina, że istnieją dwa zastosowania instrukcji przypisania w Javie (i większości innych głównych języków):
powiązanie wartości z nazwą:
final int MAX_SIZE = 100;
aktualizacja destrukcyjna:
int a = 3; a += 1; a++;
Programowanie funkcjonalne omija drugie, ale obejmuje pierwsze (przykłady:
let
-wyrażenia, parametry funkcji, itiony najwyższego poziomudefine
) . Jest to bardzo ważny punkt do uchwycenia, ponieważ w przeciwnym razie po prostu artykuł wydaje się głupie, a może zostawić cię zastanawiać, jakie sątake
,squares-of
iintegers
jeśli nie zmienne?Ponadto przykład nie ma znaczenia. To nie pokazuje implementacje
take
,squares-of
lubintegers
. Z tego, co wiemy, są one implementowane przy użyciu zmiennych zmiennych. Jak powiedział @Martin, możesz w prosty sposób napisać ten przykład w Javie.Jeszcze raz polecam unikanie tego artykułu i innych podobnych, jeśli naprawdę chcesz dowiedzieć się o programowaniu funkcjonalnym. Wydaje się, że jest napisane bardziej w celu szokowania i obrażania niż nauczania pojęć i podstaw. Zamiast tego sprawdź jeden z moich ulubionych artykułów Johna Hughesa. Hughes próbuje rozwiązać niektóre z tych samych problemów, które omówił artykuł (chociaż Hughes nie mówi o współbieżności / równoległości); oto zwiastun:
źródło
take 25 (map (^2) [1..])
jako przykładu?[1..]
? To fajna funkcja wbudowana w Haskell, ale nie ilustruje koncepcji generowania takiej listy. Jestem pewien, że instancjeEnum
klasy (której wymaga ta składnia) są również pomocne, ale były zbyt leniwe, aby je znaleźć. Tak więcunfoldr
. :)Nie zrobiłbyś tego. Zmienne są podstawą programowania imperatywnego, a jeśli spróbujesz programować imperatywnie bez użycia zmiennych, po prostu sprawisz każdemu ból w dupie. W różnych paradygmatach programowania style są różne, a różne koncepcje stanowią podstawę. Zmienna w Javie, gdy jest dobrze używana z małym zakresem, nie jest złem. Pytanie o program Java bez zmiennych jest jak proszenie o program Haskell bez funkcji, więc nie pytaj o to i nie daj się oszukać, że programowanie imperatywne jest gorsze, ponieważ używa zmiennych.
Tak więc sposobem Java byłoby:
i nie daj się zwieść napisaniu go w bardziej złożony sposób z powodu nienawiści do zmiennych.
źródło
Najprostsze, co mogę zrobić z rekurencją, to funkcja z jednym parametrem. Nie jest bardzo Java-esque, ale działa:
źródło
W twoim przykładzie funkcjonalnym nie widzimy, jak implementowane są funkcje
squares-of
itake
. Nie jestem ekspertem od Java, ale jestem pewien, że moglibyśmy napisać te funkcje, aby włączyć takie zdanie ...co nie jest tak bardzo różne.
źródło
squares-of
nie jest prawidłową nazwą w Javie (squares_of
choć jest). Ale poza tym dobra uwaga, która pokazuje, że przykład tego artykułu jest słaby.integer
leniwie generuje liczby całkowite, atake
funkcja wybiera 25squared-of
liczb zinteger
. Krótko mówiąc, powinieneś miećinteger
funkcję leniwego tworzenia liczb całkowitych do nieskończoności.(integer)
funkcja jest trochę szaleństwem - funkcja wciąż jest czymś, co mapuje argument na wartość. Okazuje się, że(integer)
nie jest to funkcja, a jedynie wartość. Można nawet posunąć się tak daleko, aby powiedzieć, żeinteger
jest to zmienna związana z nieskończoną liczbą liczb.W Javie możesz to zrobić (zwłaszcza nieskończoną część listy) za pomocą iteratorów. W poniższym przykładzie kodu liczba dostarczona do
Take
konstruktora może być dowolnie wysoka.Lub metodami fabrycznymi z możliwością łączenia:
Gdzie
SquaresOf
,Take
iIntegers
przedłużyćNumbers
źródło
while (test.hasNext()) System.out.println(test.next())
byłyby nie do przyjęcia w FP; iteratory są z natury zmienne.Krótka wersja:
Aby styl pojedynczego przydziału działał niezawodnie w Javie, potrzebujesz (1) pewnego rodzaju niezmiennej infrastruktury i (2) wsparcia na poziomie kompilatora lub środowiska wykonawczego w celu eliminacji wywołania ogonowego.
Możemy napisać dużą część infrastruktury i możemy zorganizować rzeczy, aby uniknąć zapełniania stosu. Ale tak długo, jak każde wywołanie zajmuje ramkę stosu, będzie istniało ograniczenie liczby możliwych rekurencji. Staraj się, aby iterery były małe i / lub leniwe, i nie powinieneś mieć większych problemów. Przynajmniej większość problemów, na które napotkasz, nie wymaga natychmiastowego powrotu miliona wyników. :)
Pamiętaj również, że ponieważ program musi faktycznie wprowadzać widoczne zmiany, aby być wartym uruchomienia, nie można sprawić, by wszystko było niezmienne. Możesz jednak zachować ogromną większość własnych rzeczy na niezmiennym poziomie, używając niewielkiego podzbioru niezbędnych zmiennych (na przykład strumieni) tylko w niektórych kluczowych punktach, w których alternatywy byłyby zbyt uciążliwe.
Długa wersja:
Mówiąc najprościej, program Java nie może całkowicie ominąć zmiennych, jeśli chce zrobić coś, co warto zrobić. Można zawierać je, a tym samym ograniczyć zmienność na ogromnym stopniu, ale bardzo zaprojektować języka i API - wraz z potrzebą, aby w końcu zmienić system podstawowy - dokonać całkowitej niezmienności niewykonalne.
Java została zaprojektowana od początku jako imperatywny , zorientowany obiektowo język.
while (true)
ifor (;;)
! - są całkowicie zależne od zmiennej, która zmienia się gdzieś z iteracji na iterację.Efektem końcowym tych decyzji projektowych jest to, że bez zmiennych zmiennych Java nie ma możliwości zmiany stanu czegokolwiek - nawet czegoś tak prostego jak wydruk „Witaj świecie!” do ekranu wiąże się strumień wyjściowy, który polega na umieszczaniu bajtów w buforze , który można modyfikować .
Tak więc dla wszystkich praktycznych celów ograniczamy się do usuwania zmiennych z własnego kodu. OK, możemy to zrobić. Prawie. Zasadniczo potrzebowalibyśmy zastąpić prawie całą iterację rekurencją, a wszystkie mutacje rekurencyjnymi wywołaniami zwracającymi zmienioną wartość. tak jak ...
Zasadniczo budujemy listę połączoną, w której każdy węzeł jest listą samą w sobie. Każda lista ma „nagłówek” (bieżąca wartość) i „ogon” (pozostała lista podrzędna). Większość języków funkcjonalnych robi coś podobnego, ponieważ jest bardzo podatna na niezmienność. „Następna” operacja po prostu zwraca ogon, który zwykle jest przekazywany do następnego poziomu w stosie wywołań rekurencyjnych.
To jest bardzo uproszczona wersja tych rzeczy. Ale to wystarczy, aby zademonstrować poważny problem z tym podejściem w Javie. Rozważ ten kod:
Chociaż do wyniku potrzebujemy tylko 25 liczb wewnętrznych,
squares_of
nie wie o tym. Zwróci kwadrat każdej liczby wintegers
. Rekurencja na głębokości 20 milionów poziomów powoduje całkiem duże problemy w Javie.Widzisz, funkcjonalne języki, w których zwykle robisz takie dziwactwa, mają funkcję zwaną „eliminacją ogona”. Oznacza to, że gdy kompilator widzi, że ostatnim działaniem kodu jest wywołanie samego siebie (i zwrócenie wyniku, jeśli funkcja nie jest nieważna), używa ramki stosu bieżącego wywołania zamiast konfigurowania nowego i wykonuje zamiast tego „skok” „wywołania” (więc używane miejsce na stosie pozostaje stałe). Krótko mówiąc, idzie to w około 90% w kierunku przekształcenia rekurencji ogona w iterację. Może poradzić sobie z tymi miliardami ints bez przepełnienia stosu. (W końcu wciąż zabraknie pamięci, ale zestawienie miliarda ints i tak zepsuje pamięć w systemie 32-bitowym).
W większości przypadków Java tego nie robi. (Zależy to od kompilatora i środowiska wykonawczego, ale implementacja Oracle tego nie robi.) Każde wywołanie funkcji rekurencyjnej zużywa pamięć ramki stosu. Zużyj za dużo, a otrzymasz przepełnienie stosu. Przepełnienie stosu prawie gwarantuje śmierć programu. Musimy więc upewnić się, że tego nie zrobimy.
Jedno pół obejście ... leniwa ocena. Nadal mamy ograniczenia dotyczące stosów, ale można je powiązać z czynnikami, nad którymi mamy większą kontrolę. Nie musimy obliczać miliona ints tylko po to, aby zwrócić 25. :)
Zbudujmy więc trochę leniwej infrastruktury do oceny. (Ten kod został przetestowany jakiś czas temu, ale od tego czasu go trochę zmodyfikowałem; przeczytaj pomysł, a nie błędy składniowe. :))
(Należy pamiętać, że jeśli byłoby to faktycznie wykonalne w Javie, kod przynajmniej nieco podobny do powyższego byłby już częścią API).
Teraz, gdy istnieje infrastruktura, pisanie kodu, który nie wymaga zmiennych i jest co najmniej stabilny przy mniejszych ilościach danych, jest dość trywialny.
To głównie działa, ale wciąż jest podatne na przepełnienia stosu. Wypróbuj
take
2 miliardy intów i wykonaj na nich jakąś akcję. : P W końcu wyrzuci wyjątek, przynajmniej dopóki 64+ GB pamięci RAM nie stanie się standardem. Problem polega na tym, że ilość pamięci programu zarezerwowana dla stosu nie jest tak duża. Zazwyczaj wynosi od 1 do 8 MiB. (Można poprosić o większe, ale to nie ma znaczenia aż tak dużo, ile prosisz - dzwonisztake(1000000000, someInfiniteSequence)
, to będzie uzyskać wyjątek.) Kontrolę szczęście z oceny leniwy, słabym punktem jest w obszarze możemy lepiej . Musimy tylko uważać, ile mytake()
.Nadal będzie miał wiele problemów ze skalowaniem, ponieważ nasze zużycie stosu rośnie liniowo. Każde połączenie obsługuje jeden element, a resztę przekazuje do innego połączenia. Teraz, gdy o tym myślę, jest jedna sztuczka, którą możemy wyciągnąć, która może dać nam nieco więcej rezerwy: zmienić łańcuch połączeń w drzewo połączeń. Zastanów się nad czymś takim:
workWith
w zasadzie dzieli pracę na dwie połowy i przypisuje sobie każdą połowę do innego wezwania. Ponieważ każde wywołanie zmniejsza rozmiar listy roboczej o połowę, a nie o jedną, powinno to być skalowane logarytmicznie, a nie liniowo.Problem polega na tym, że ta funkcja potrzebuje danych wejściowych - a przy połączonej liście uzyskanie długości wymaga przejścia całej listy. Łatwo to jednak rozwiązać; po prostu nie obchodzi mnie, ile jest wpisów. :) Powyższy kod działałby z czymś takim
Integer.MAX_VALUE
jak liczenie, ponieważ null i tak zatrzymuje przetwarzanie. Liczba jest głównie tam, więc mamy solidną podstawę. Jeśli spodziewasz się, żeInteger.MAX_VALUE
na liście będzie więcej niż kilka pozycji, możesz sprawdzićworkWith
wartość zwracaną - na końcu powinna ona być pusta. W przeciwnym razie powtórz.Pamiętaj, że dotyka to tyle elementów, ile chcesz. To nie jest leniwe; robi to natychmiast. Chcesz to zrobić tylko dla akcji - to znaczy dla rzeczy , których jedynym celem jest zastosowanie się do każdego elementu na liście. W tej chwili myślę, że sekwencje byłyby o wiele mniej skomplikowane, gdyby były liniowe; nie powinno stanowić problemu, ponieważ sekwencje i tak nie nazywają się same - po prostu tworzą obiekty, które wywołują je ponownie.
źródło
Wcześniej próbowałem utworzyć interpreter dla języka podobnego do seplenienia w Javie (kilka lat temu i cały kod został utracony, tak jak w CVS w sourceforge), a iteratory Java są nieco gadatliwe dla programowania funkcjonalnego na listach.
Oto coś oparte na interfejsie sekwencji, który ma tylko dwie operacje potrzebne do uzyskania bieżącej wartości i uzyskania sekwencji zaczynającej się od następnego elementu. Są one nazywane głową i ogonem po funkcjach w schemacie.
Ważne jest, aby używać mniej więcej tak
Seq
lubIterator
interfejsów jak oznacza to, że lista jest tworzona leniwie.Iterator
Interfejs nie może być niezmienny przedmiot, więc jest mniej nadaje się do programowania funkcyjnego - jeśli nie można stwierdzić, czy wartość przekazać do funkcji został zmieniony przez nią, traci jedną z kluczowych zalet programowania funkcjonalnego.Oczywiście
integers
powinna być lista wszystkich liczb całkowitych, więc zacząłem od zera i na przemian zwracałem wartości dodatnie i ujemne.Istnieją dwie wersje kwadratów - jedna tworzy niestandardową sekwencję, druga używa
map
„funkcji” - Java 7 nie ma lambda, więc użyłem interfejsu - i stosuje ją kolejno do każdego elementu w sekwencji.Celem tej
square ( int x )
funkcji jest jedynie usunięcie potrzebyhead()
dwukrotnego wywoływania - normalnie zrobiłbym to, umieszczając wartość w zmiennej końcowej, ale dodanie tej funkcji oznacza, że w programie nie ma żadnych zmiennych, tylko parametry funkcji.Gadatliwość Java dla tego rodzaju programowania skłoniła mnie do napisania drugiej wersji mojego interpretera w C99.
źródło
Teoretycznie możesz zaimplementować prawie wszystko w Javie, używając tylko rekurencji i żadnych zmiennych zmiennych.
W praktyce:
Język Java nie jest do tego przeznaczony. Wiele konstruktów zaprojektowano pod kątem mutacji i bez nich trudno je stosować. (Na przykład nie można zainicjować tablicy Java o zmiennej długości bez mutacji).
To samo dotyczy bibliotek. A jeśli ograniczysz się do klas bibliotek, które nie wykorzystują mutacji pod przykryciem, jest to jeszcze trudniejsze. (Nie możesz nawet użyć String ... zobacz, jak
hashcode
jest implementowany.)Implementacje Java głównego nurtu nie obsługują optymalizacji wywołania ogona. Oznacza to, że rekurencyjne wersje algorytmów są „głodne” przestrzeni stosu. A ponieważ stosy wątków Java nie rosną, musisz wstępnie przydzielić duże stosy ... lub zaryzykować
StackOverflowError
.Połącz te trzy rzeczy, a Java nie jest tak naprawdę realną opcją do pisania użytecznych (tzn. Nietrywialnych) programów bez zmiennych.
(Ale hej, to w porządku. Istnieją inne języki programowania dla JVM, z których niektóre obsługują programowanie funkcjonalne.)
źródło
Gdy szukamy przykładu pojęć, powiedziałbym, odłóżmy na bok Javę i poszukajmy innego, ale znanego ustawienia, w którym można znaleźć znaną wersję pojęć. Potoki UNIX są raczej podobne do łączenia leniwych funkcji.
W Linuksie oznacza to, daj mi bajty, z których każdy składa się z fałszywych, a nie prawdziwych bitów, dopóki nie stracę apetytu; zmień każdy z tych bajtów na znak nowej linii; numer każdej tak utworzonej linii; i wyprodukuj kwadrat tej liczby. Ponadto mam apetyt na 25 linii i nie więcej.
Twierdzę, że programista nie powinien odradzać pisania potoku Linux w ten sposób. To stosunkowo normalne skrypty powłoki Linux.
Twierdzę, że programista nie powinien próbować pisać tego samego podobnie w Javie. Powodem jest utrzymanie oprogramowania jako główny czynnik w całym cyklu życia projektów oprogramowania. Nie chcemy zamykać następnego programisty, prezentując pozornie program Java, ale tak naprawdę jest on napisany w niestandardowym, jednorazowym języku poprzez skomplikowane kopiowanie funkcjonalności, która już istnieje na platformie Java.
Z drugiej strony twierdzę, że następny programista mógłby być bardziej akceptowalny, gdyby niektóre nasze pakiety „Java” były faktycznie pakietami wirtualnej maszyny Java napisanymi w jednym z języków funkcjonalnych lub obiektowych / funkcjonalnych, takich jak Clojure i Scala. Są one zaprojektowane do kodowania przez łączenie funkcji razem i do wywoływania z Javy w normalny sposób wywołań metod Java.
Z drugiej strony programista Java może nadal być inspiracją do programowania funkcjonalnego.
Ostatnio moją ulubioną techniką było użycie niezmiennej, niezainicjowanej zmiennej powrotu i pojedynczego wyjścia, aby, podobnie jak niektóre kompilatory języka funkcjonalnego, Java sprawdzała, czy bez względu na to, co dzieje się w treści funkcji, zawsze zapewniam jedną i tylko jedną zwracana wartość. Przykład:źródło
int result = -n; if (n < 1) { result = 0 } return result;
jeśli kodujesz, gdy kompiluje się dobrze, a kompilator nie ma pojęcia, czy zamierzasz uczynić go równoważnym funkcji z mojego przykładu. Może ten przykład jest zbyt prosty, aby technika wyglądała na pomocną, ale w funkcji z wieloma gałęziami fajnie jest wyjaśnić, że wynik jest przypisywany dokładnie jeden raz, niezależnie od tego, którą ścieżką podążamy.if (n < 1) return 0; else return -n;
, że skończysz bez problemu ... a poza tym jest to prostsze. :) Wydaje mi się, że w tym przypadku, „jeden zwrot” reguła rzeczywiście pomaga przyczyną emisji nie wiedząc, gdy wartość zwrócona została ustalona; w przeciwnym razie możesz po prostu ją zwrócić, a Java byłaby w stanie określić, kiedy inne ścieżki mogą nie zwracać wartości, ponieważ nie będziesz już oddzielać obliczania wartości od faktycznego jej zwrotu.if (n < 0) return -n; else if (n == 0) return 0; else return n - 1;
.Najłatwiejszym sposobem, aby się tego dowiedzieć, byłoby przesłanie następujących informacji do kompilatora Frege i sprawdzenie wygenerowanego kodu Java:
źródło