Muszę przyznać, że niewiele wiem o programowaniu funkcjonalnym. Przeczytałem o tym tu i tam, i wtedy dowiedziałem się, że w programowaniu funkcjonalnym funkcja zwraca to samo wyjście, dla tego samego wejścia, bez względu na to, ile razy funkcja jest wywoływana. To jest dokładnie jak funkcja matematyczna, która ocenia na tym samym wyjściu dla tej samej wartości parametrów wejściowych, które dotyczą wyrażenia funkcji.
Rozważmy na przykład:
f(x,y) = x*x + y; // It is a mathematical function
Bez względu na to, ile razy użyjesz f(10,4)
, zawsze będzie miała wartość 104
. Jako taki, gdziekolwiek napisałeś f(10,4)
, możesz go zastąpić 104
bez zmiany wartości całego wyrażenia. Ta właściwość jest nazywana referencyjną przezroczystością wyrażenia.
Jak mówi Wikipedia ( link ),
I odwrotnie, w kodzie funkcjonalnym wartość wyjściowa funkcji zależy tylko od argumentów wprowadzonych do funkcji, więc wywołanie funkcji f dwa razy z tą samą wartością dla argumentu x da ten sam wynik f (x) za każdym razem.
Czy funkcja programowania czasu (która zwraca aktualny czas) może istnieć w programowaniu funkcjonalnym?
Jeśli tak, to jak może istnieć? Czy nie narusza to zasady programowania funkcjonalnego? W szczególności narusza przejrzystość referencyjną, która jest jedną z właściwości programowania funkcjonalnego (jeśli dobrze to rozumiem).
A jeśli nie, to jak poznać aktualny czas w programowaniu funkcjonalnym?
Odpowiedzi:
Innym sposobem na wyjaśnienie tego jest: żadna funkcja nie może uzyskać bieżącego czasu (ponieważ ciągle się zmienia), ale akcja może uzyskać bieżący czas. Powiedzmy, że
getClockTime
jest to stała (lub funkcja zerowa, jeśli chcesz), która reprezentuje akcję uzyskiwania bieżącego czasu. Ta akcja jest taka sama za każdym razem, bez względu na to, kiedy jest używana, więc jest to prawdziwa stała.Podobnie, powiedzmy,
print
jest funkcją, która zajmuje trochę czasu i drukuje ją na konsoli. Ponieważ wywołania funkcji nie mogą wywoływać efektów ubocznych w czystym języku funkcjonalnym, zamiast tego wyobrażamy sobie, że jest to funkcja, która pobiera znacznik czasu i zwraca akcję drukowania go na konsoli. Ponownie, jest to prawdziwa funkcja, ponieważ jeśli nadasz jej ten sam znacznik czasu, zwróci tę samą czynność drukowania za każdym razem.Jak teraz wydrukować bieżącą godzinę na konsoli? Cóż, musisz połączyć te dwie akcje. Jak więc możemy to zrobić? Nie możemy po prostu przejść
getClockTime
sięprint
, ponieważ druk spodziewa się znacznik czasu, a nie działania. Ale możemy sobie wyobrazić, że istnieje operator,>>=
który łączy dwie akcje, jedną, która pobiera znacznik czasu, a drugą, która bierze jedną za argument i drukuje. Po zastosowaniu tego do wcześniej wymienionych działań, wynikiem jest ... tadaaa ... nowa akcja, która pobiera bieżący czas i drukuje go. A tak przy okazji, dokładnie tak się dzieje w Haskell.Tak więc koncepcyjnie można to zobaczyć w następujący sposób: czysty program funkcjonalny nie wykonuje żadnych operacji we / wy, definiuje akcję , którą następnie wykonuje system wykonawczy. Działania jest taka sama za każdym razem, ale efektem jego wykonania zależy od okoliczności, gdy jest on wykonywany.
Nie wiem, czy było to bardziej zrozumiałe niż inne wyjaśnienia, ale czasami pomaga mi to myśleć w ten sposób.
źródło
getClockTime
akcję zamiast funkcji. Cóż, jeśli tak nazwiesz, to wywołaj każdą akcję funkcji , wtedy nawet programowanie imperatywne stałoby się programowaniem funkcjonalnym. A może chcesz to nazwać programowaniem funkcyjnym .main
akcji. Umożliwia to oddzielenie czystego kodu funkcjonalnego od kodu imperatywnego, a separacja ta jest wymuszona przez system typów. Traktowanie działań jako obiektów pierwszej klasy pozwala także przekazywać je i budować własne „struktury kontrolne”.->
- w taki sposób standard definiuje ten termin i to naprawdę jedyna sensowna definicja w kontekście Haskell. Więc czymś, którego typ jestIO Whatever
to nie funkcja.putStrLn
to nie jest akcja - to funkcja, która zwraca akcję.getLine
jest zmienną zawierającą akcję. Akcje to wartości, zmienne i funkcje to „pojemniki” / „etykiety”, które dajemy tym akcjom.Tak i nie.
Różne funkcjonalne języki programowania rozwiązują je inaczej.
W Haskell (bardzo czystym) wszystko to musi się wydarzyć w tak zwanej Monadzie I / O - patrz tutaj .
Możesz myśleć o tym jako o wprowadzeniu kolejnego wejścia (i wyjścia) do swojej funkcji (stan świata) lub łatwiejszym o miejsce, w którym dzieje się „nieczystość”, taka jak zmiana czasu.
Inne języki, takie jak F #, mają wbudowaną pewną nieczystość, więc możesz mieć funkcję, która zwraca różne wartości dla tego samego wejścia - tak jak normalne języki imperatywne.
Jak wspomniał Jeffrey Burka w swoim komentarzu: Oto miłe wprowadzenie do Monady I / O prosto z wiki Haskell.
źródło
W Haskell używa się konstrukcji zwanej monadą do radzenia sobie z efektami ubocznymi. Monada w zasadzie oznacza, że kapsułkujesz wartości w kontenerze i masz pewne funkcje do łączenia funkcji od wartości do wartości w kontenerze. Jeśli nasz kontener ma typ:
możemy bezpiecznie realizować działania IO. Ten typ oznacza: Działanie typu
IO
jest funkcją, która pobiera token typuRealWorld
i zwraca nowy token wraz z wynikiem.Chodzi o to, że każda akcja IO mutuje stan zewnętrzny, reprezentowany przez magiczny token
RealWorld
. Korzystając z monad, można połączyć wiele funkcji, które mutują prawdziwy świat razem. Najważniejszą funkcją monady jest>>=
wymawiane bind :>>=
wykonuje jedną akcję i funkcję, która bierze wynik tej akcji i tworzy z niej nową akcję. Typ zwracany jest nową akcją. Załóżmy na przykład, że istnieje funkcjanow :: IO String
, która zwraca ciąg znaków reprezentujący bieżący czas. Możemy połączyć go z funkcjąputStrLn
do wydrukowania:Lub napisane w
do
-Notation, który jest bardziej znany programistom:Wszystko to jest czyste, gdy mapujemy mutację i informacje o świecie na zewnątrz do
RealWorld
tokena. Za każdym razem, gdy uruchamiasz tę akcję, otrzymujesz oczywiście inny wynik, ale dane wejściowe nie są takie same:RealWorld
token jest inny.źródło
RealWorld
zasłony dymnej. Najważniejsze jest jednak to, jak ten rzekomy obiekt jest przekazywany w łańcuchu. Brakujący element zaczyna się tam, gdzie jest źródło lub połączenie ze światem rzeczywistym - zaczyna się od głównej funkcji, która działa w monadzie IO.RealWorld
obiekcie globalnym, który jest przekazywany do programu podczas jego uruchamiania.main
funkcja przyjmujeRealWorld
argument. Zostaje przekazany dopiero po egzekucji.RealWorld
i zapewniają tylko mizerne funkcje, aby to zmienićputStrLn
, jest to, że jakiś programista Haskell nie zmienia sięRealWorld
w jednym ze swoich programów, tak że adres Haskell Curry i data urodzenia są takie, że stają się sąsiadami dorastanie (może to uszkodzić kontinuum czasoprzestrzenne w taki sposób, że zrani język programowania Haskell.)RealWorld -> (a, RealWorld)
nie rozkłada się na metaforę nawet pod współbieżnością, o ile pamiętasz, że świat rzeczywisty może być zmieniany przez inne części wszechświata poza twoją funkcją (lub twoim obecnym procesem) przez cały czas. Tak (a) methaphor nie załamać, oraz (b) za każdym razem, to wartość, która maRealWorld
jako jej typ jest przekazywany do funkcji, funkcja musi być ponownie ocenione, ponieważ prawdziwy świat będzie się zmieniło w międzyczasie ( który jest modelowany jak wyjaśniono @fuz, zwracając inną „wartość tokena” za każdym razem, gdy wchodzimy w interakcję ze światem rzeczywistym).Większość funkcjonalnych języków programowania nie jest czysta, tzn. Pozwala funkcjom nie tylko zależeć od ich wartości. W tych językach jest całkowicie możliwe, aby funkcja zwracała bieżący czas. Z języków, w których otagowano to pytanie, dotyczy to Scali i F # (jak również większości innych wariantów ML ).
W językach takich jak Haskell i Clean , które są czyste, sytuacja jest inna. W Haskell aktualny czas nie byłby dostępny przez funkcję, ale przez tak zwaną akcję IO, która jest sposobem Haskella na kapsułkowanie efektów ubocznych.
W Clean byłaby to funkcja, ale funkcja przyjmowałaby wartość świata jako argument i zwracałaby nową wartość świata (oprócz bieżącego czasu) jako wynik. System typów zapewniłby, że każdej wartości światowej można użyć tylko raz (a każda funkcja, która zużywa wartość światową, tworzy nową). W ten sposób funkcja czasu musiałaby być wywoływana za każdym razem z innym argumentem, a zatem za każdym razem można by było zwrócić inny czas.
źródło
„Aktualny czas” nie jest funkcją. To jest parametr. Jeśli kod zależy od aktualnego czasu, oznacza to, że kod jest parametryzowany według czasu.
źródło
Można to absolutnie zrobić w czysto funkcjonalny sposób. Można to zrobić na kilka sposobów, ale najprostszym jest zwrócenie funkcji czasu nie tylko czasu, ale także funkcji, którą należy wywołać, aby uzyskać następny pomiar czasu .
W języku C # można zaimplementować go w następujący sposób:
(Należy pamiętać, że jest to przykład, który ma być prosty, a nie praktyczny. W szczególności węzłów listy nie można wyrzucać, ponieważ są one zrootowane przez ProgramStartTime).
Ta klasa „ClockStamp” działa jak niezmienna połączona lista, ale tak naprawdę węzły są generowane na żądanie, aby mogły zawierać „bieżący” czas. Każda funkcja, która chce mierzyć czas, powinna mieć parametr „clockStamp” i musi również zwrócić wynik swojego ostatniego pomiaru (aby osoba dzwoniąca nie widziała starych pomiarów), jak poniżej:
Oczywiście trochę niewygodne jest przechodzenie ostatniego pomiaru do środka i do środka, do środka i na zewnątrz, do środka i na zewnątrz. Istnieje wiele sposobów na ukrycie płyty głównej, szczególnie na poziomie projektowania języka. Myślę, że Haskell używa tego rodzaju sztuczki, a następnie ukrywa brzydkie części za pomocą monad.
źródło
i++
w pętli for nie jest referencyjnie przezroczyste;)struct TimeKleisli<Arg, Res> { private delegate Res(TimeStampedValue<Arg>); }
. Ale kod z tym nadal nie wyglądałby tak ładnie jak Haskell zedo
składnią.SelectMany
, która umożliwia składnię zrozumienia zapytania. Nadal nie możesz programować polimorficznie w stosunku do monad, więc to wszystko jest ciężka walka przeciwko słabemu systemowi typów :(Dziwi mnie, że żadna z odpowiedzi ani komentarzy nie wspomina o węgielnicach ani koindukcji. Zazwyczaj koindukcję wspomina się podczas wnioskowania o nieskończonych strukturach danych, ale ma ona również zastosowanie do niekończącego się strumienia obserwacji, takiego jak rejestr czasu w CPU. Stan ukrytego modelu węgla; oraz modele koindukcyjne obserwujące ten stan. ( Stan budowy normalnych modeli indukcyjnych ).
To gorący temat w Reaktywnym Programowaniu Funkcjonalnym. Jeśli interesują Cię tego rodzaju rzeczy, przeczytaj to: http://digitalcommons.ohsu.edu/csetech/91/ (28 s.)
źródło
Tak, funkcja czysta może zwrócić czas, jeśli jest podany jako parametr. Inny argument czasu, inny wynik czasu. Następnie utwórz inne funkcje czasu i połącz je z prostym słownikiem funkcji (-of-time) -transforming (wyższego rzędu). Ponieważ podejście jest bezstanowe, czas tutaj może być ciągły (niezależny od rozdzielczości), a nie dyskretny, znacznie zwiększając modułowość . Ta intuicja jest podstawą Functional Reactive Programming (FRP).
źródło
Tak! Masz rację! Now () lub CurrentTime () lub jakakolwiek sygnatura metody takiego smaku nie wykazuje przejrzystości referencyjnej w jeden sposób. Ale zgodnie z instrukcją dla kompilatora jest on parametryzowany przez wejście zegara systemowego.
Według danych wyjściowych Now () może wyglądać tak, jakby nie zachowywała przezroczystości referencyjnej. Ale rzeczywiste zachowanie zegara systemowego i funkcji na nim jest zgodne z przejrzystością referencyjną.
źródło
Tak, funkcja pobierania czasu może istnieć w programowaniu funkcjonalnym przy użyciu nieco zmodyfikowanej wersji programowania funkcjonalnego znanego jako nieczyste programowanie funkcjonalne (domyślnym lub głównym jest programowanie funkcjonalne).
W przypadku uzyskania czasu (lub odczytu pliku lub wystrzelenia pocisku) kod musi wchodzić w interakcje ze światem zewnętrznym, aby wykonać zadanie, a ten świat zewnętrzny nie jest oparty na czystych podstawach programowania funkcjonalnego. Aby umożliwić czysto funkcjonalnemu światowi programowania interakcję z tym nieczystym światem zewnętrznym, ludzie wprowadzili nieczyste programowanie funkcjonalne. W końcu oprogramowanie, które nie wchodzi w interakcje ze światem zewnętrznym, nie jest użyteczne poza wykonywaniem obliczeń matematycznych.
Niewiele funkcjonalnych języków programowania ma wbudowaną funkcję nieczystości, dzięki czemu nie jest łatwo oddzielić, który kod jest nieczysty i który jest czysty (jak F # itp.), A niektóre funkcjonalne języki programowania zapewniają, że robiąc pewne nieczyste rzeczy kod ten wyraźnie się wyróżnia w porównaniu do czystego kodu, takiego jak Haskell.
Innym ciekawym sposobem na zobaczenie tego jest to, że funkcja „get time” w programowaniu funkcjonalnym pobierałaby obiekt „world”, który ma aktualny stan świata, taki jak czas, liczba ludzi żyjących na świecie itp. Następnie otrzymujemy czas, z którego świata obiekt byłby zawsze czysty, tzn. przechodząc w tym samym stanie świata, zawsze otrzymujesz ten sam czas.
źródło
Twoje pytanie łączy dwie powiązane miary języka komputerowego: funkcjonalny / imperatywny i czysty / nieczysty.
Język funkcjonalny określa relacje między wejściami i wyjściami funkcji, a język imperatywny opisuje określone operacje w określonej kolejności do wykonania.
Czysty język nie stwarza efektów ubocznych ani nie zależy od nich, a język nieczysty używa ich przez cały czas.
W stu procentach czyste programy są w zasadzie bezużyteczne. Mogą wykonywać ciekawe obliczenia, ale ponieważ nie mogą wywoływać efektów ubocznych, nie mają danych wejściowych ani wyjściowych, więc nigdy nie dowiesz się, co obliczyli.
Aby być w ogóle użytecznym, program musi być co najmniej nieczysty. Jednym ze sposobów uczynienia czystego programu użytecznym jest umieszczenie go w cienkim nieczystym opakowaniu. Tak jak w przypadku tego nieprzetestowanego programu Haskell:
źródło
IO
wartości i wyniki za czyste.Poruszasz bardzo ważny temat w programowaniu funkcjonalnym, czyli wykonywaniu I / O. Wiele czystych języków korzysta z wbudowanych języków specyficznych dla domeny, np. Podjęzyka, którego zadaniem jest kodowanie działań , które mogą dawać wyniki.
Na przykład środowisko uruchomieniowe Haskell oczekuje, że zdefiniuję wywołaną akcję,
main
która składa się ze wszystkich akcji tworzących mój program. Środowisko wykonawcze wykonuje następnie tę akcję. W większości przypadków wykonuje czysty kod. Od czasu do czasu środowisko wykonawcze wykorzystuje dane obliczeniowe do wykonywania operacji we / wy i przekazuje dane z powrotem do czystego kodu.Możesz narzekać, że brzmi to jak oszustwo i w pewnym sensie: definiując działania i oczekując, że środowisko wykonawcze je wykona, programista może zrobić wszystko, co może zrobić normalny program. Ale silny system typu Haskell tworzy silną barierę między czystymi i „nieczystymi” częściami programu: nie można po prostu dodać, powiedzmy, dwóch sekund do bieżącego czasu procesora i wydrukować go, trzeba zdefiniować akcję, która spowoduje bieżące Czas procesora i przekaż wynik innej akcji, która doda dwie sekundy i wydrukuje wynik. Pisanie zbyt dużej ilości programu jest jednak uważane za zły styl, ponieważ utrudnia wnioskowanie, które efekty są wywoływane, w porównaniu do typów Haskell, które mówią nam wszystko , co możemy wiedzieć o wartości.
Przykład:
clock_t c = time(NULL); printf("%d\n", c + 2);
w C, w porównaniumain = getCPUTime >>= \c -> print (c + 2*1000*1000*1000*1000)
z Haskell. Operator>>=
służy do komponowania akcji, przekazując wynik pierwszej do funkcji, która powoduje drugą akcję. Ten wyglądający dość tajemniczo, kompilatory Haskell obsługują cukier syntaktyczny, który pozwala nam napisać ten ostatni kod w następujący sposób:To ostatnie wydaje się konieczne, prawda?
źródło
Nie istnieje w sensie czysto funkcjonalnym.
Najpierw może się przydać wiedza o tym, jak czas jest pobierany na komputerze. Zasadniczo na pokładzie znajdują się obwody, które śledzą czas (dlatego komputer zwykle potrzebuje małej baterii). Następnie może istnieć jakiś proces wewnętrzny, który ustawia wartość czasu w określonym rejestrze pamięci. Co zasadniczo sprowadza się do wartości, którą CPU może odzyskać.
W przypadku Haskell istnieje koncepcja „działania we / wy”, które reprezentuje typ, który można wykonać w celu przeprowadzenia pewnego procesu we / wy. Zamiast odwoływać się do
time
wartości, odwołujemy się doIO Time
wartości. Wszystko to byłoby czysto funkcjonalne. Nie odwołujemy się,time
ale coś w stylu „odczytaj wartość rejestru czasu” .Gdy faktycznie wykonamy program Haskell, akcja IO faktycznie miałaby miejsce.
źródło