Jak może istnieć funkcja czasu w programowaniu funkcjonalnym?

646

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ć 104bez 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?

Nawaz
źródło
15
Myślę, że większość (lub wszystkie) języki funkcjonalne nie są tak ścisłe i łączą funkcjonalne i imperatywne programowanie. Przynajmniej takie jest moje wrażenie z F #.
Alex F
13
@Adam: Skąd dzwoniący miałby znać aktualny czas?
Nawaz
29
@Adam: W rzeczywistości jest to nielegalne (jak w: niemożliwe) w czysto funkcjonalnych językach.
sepp2k
47
@Adam: Prawie. Język ogólnego przeznaczenia, który jest czysty, zazwyczaj oferuje pewne ułatwienia w uzyskaniu „stanu świata” (tj. Rzeczy takie jak aktualny czas, pliki w katalogu itp.) Bez naruszania referencyjnej przejrzystości. W Haskell to monada IO, a w Clean to świat. Tak więc w tych językach funkcja, która potrzebuje aktualnego czasu, przyjmie ją jako argument lub będzie musiała zwrócić akcję IO zamiast jej rzeczywistego wyniku (Haskell) lub przyjmie stan świata jako swój argument (Clean).
sepp2k
12
Myśląc o FP, łatwo jest zapomnieć: komputer to duża część zmiennych stanów. FP tego nie zmienia, jedynie to ukrywa.
Daniel

Odpowiedzi:

176

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 getClockTimejest 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, printjest 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ść getClockTimesię 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.

Prelude> System.Time.getClockTime >>= print
Fri Sep  2 01:13:23 東京 (標準時) 2011

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.

dainichi
źródło
33
To mnie nie przekonuje. Dogodnie wywołałeś getClockTimeakcję 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 .
Nawaz
92
@Nawaz: Kluczową rzeczą, na którą należy tutaj zwrócić uwagę, jest to, że nie można wykonać akcji z poziomu funkcji. Możesz łączyć akcje i funkcje tylko razem, aby tworzyć nowe akcje. Jedynym sposobem wykonania akcji jest ułożenie jej w mainakcji. 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”.
hammar
36
Nie wszystko w Haskell jest funkcją - to kompletna bzdura. Funkcja to coś, czego typ zawiera ->- w taki sposób standard definiuje ten termin i to naprawdę jedyna sensowna definicja w kontekście Haskell. Więc czymś, którego typ jest IO Whateverto nie funkcja.
sepp2k
9
@ sepp2k Więc moja lista :: [a -> b] jest funkcją? ;)
fuz
8
@ThomasEding Jestem naprawdę spóźniony na imprezę, ale chcę to wyjaśnić: putStrLnto nie jest akcja - to funkcja, która zwraca akcję. getLinejest zmienną zawierającą akcję. Akcje to wartości, zmienne i funkcje to „pojemniki” / „etykiety”, które dajemy tym akcjom.
kqr
356

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.

Carsten
źródło
223
Kluczową rzeczą, o której należy pamiętać w przypadku monady IO w Haskell, jest to, że obejście tego problemu to nie tylko hack; monady są ogólnym rozwiązaniem problemu definiowania sekwencji działań w pewnym kontekście. Jednym z możliwych kontekstów jest prawdziwy świat, dla którego mamy monadę IO. Kolejny kontekst dotyczy transakcji atomowej, dla której mamy monadę STM. Jeszcze innym kontekstem jest implementacja algorytmu proceduralnego (np. Tasowanie Knutha) jako czystej funkcji, dla której mamy monadę ST. Możesz także zdefiniować własne monady. Monady są rodzajem przeciążalnego średnika.
Paul Johnson,
2
Uważam za użyteczne, aby nie nazywać takich rzeczy jak „bieżące funkcje”, ale coś w rodzaju „procedur” (choć możliwe, że rozwiązanie Haskell jest wyjątkiem od tego).
singpolyma
z perspektywy Haskella klasyczne „procedury” (takie, które mają typy takie jak „... -> ()”) są nieco trywialne, ponieważ czysta funkcja z ... -> () nie jest w stanie nic zrobić.
Carsten
3
Typowy termin Haskell to „akcja”.
Sebastian Redl
6
„Monady są rodzajem przeciążalnego średnika”. +1
użytkownik2805751
147

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:

data IO a = IO (RealWorld -> (a,RealWorld))

możemy bezpiecznie realizować działania IO. Ten typ oznacza: Działanie typu IOjest funkcją, która pobiera token typu RealWorldi 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 :

(>>=) :: IO a -> (a -> IO b) -> IO b

>>=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 funkcja now :: IO String, która zwraca ciąg znaków reprezentujący bieżący czas. Możemy połączyć go z funkcją putStrLndo wydrukowania:

now >>= putStrLn

Lub napisane w do-Notation, który jest bardziej znany programistom:

do currTime <- now
   putStrLn currTime

Wszystko to jest czyste, gdy mapujemy mutację i informacje o świecie na zewnątrz do RealWorldtokena. Za każdym razem, gdy uruchamiasz tę akcję, otrzymujesz oczywiście inny wynik, ale dane wejściowe nie są takie same: RealWorldtoken jest inny.

fuz
źródło
3
-1: Jestem niezadowolony z RealWorldzasł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.
u0b34a0f6ae
2
@ kaizer.se Możesz pomyśleć o RealWorldobiekcie globalnym, który jest przekazywany do programu podczas jego uruchamiania.
fuz
6
Zasadniczo twoja mainfunkcja przyjmuje RealWorldargument. Zostaje przekazany dopiero po egzekucji.
Louis Wasserman,
13
Widzisz, powodem, dla którego ukrywają RealWorldi zapewniają tylko mizerne funkcje, aby to zmienić putStrLn, jest to, że jakiś programista Haskell nie zmienia się RealWorldw 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.)
PyRulez
2
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 ma RealWorldjako 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).
Qqwy,
73

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.

sepp2k
źródło
2
To sprawia, że ​​brzmi to tak, jakby Haskell i Clean robili różne rzeczy. Z tego, co rozumiem, robią to samo, tyle że Haskell oferuje ładniejszą składnię (?), Aby to osiągnąć.
Konrad Rudolph
27
@Konrad: Robią to samo w tym sensie, że obaj używają funkcji systemu typów do abstrakcyjnych efektów ubocznych, ale o to chodzi. Zauważ, że bardzo dobrze jest wytłumaczyć monadę IO w kategoriach typu świata, ale standard Haskell tak naprawdę nie definiuje typu świata i nie jest możliwe uzyskanie wartości typu Świat w Haskell (chociaż jest to bardzo możliwe i rzeczywiście konieczne w czystości). Co więcej, Haskell nie ma wpisywania unikatowości jako funkcji systemu typów, więc jeśli dał ci dostęp do Świata, nie byłby w stanie zapewnić, że używasz go w czysty sposób, jak to czyści Clean.
sepp2k
51

„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.

Vlad Patryshev
źródło
22

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:

// Exposes mutable time as immutable time (poorly, to illustrate by example)
// Although the insides are mutable, the exposed surface is immutable.
public class ClockStamp {
    public static readonly ClockStamp ProgramStartTime = new ClockStamp();
    public readonly DateTime Time;
    private ClockStamp _next;

    private ClockStamp() {
        this.Time = DateTime.Now;
    }
    public ClockStamp NextMeasurement() {
        if (this._next == null) this._next = new ClockStamp();
        return this._next;
    }
}

(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:

// Immutable. A result accompanied by a clockstamp
public struct TimeStampedValue<T> {
    public readonly ClockStamp Time;
    public readonly T Value;
    public TimeStampedValue(ClockStamp time, T value) {
        this.Time = time;
        this.Value = value;
    }
}

// Times an empty loop.
public static TimeStampedValue<TimeSpan> TimeALoop(ClockStamp lastMeasurement) {
    var start = lastMeasurement.NextMeasurement();
    for (var i = 0; i < 10000000; i++) {
    }
    var end = start.NextMeasurement();
    var duration = end.Time - start.Time;
    return new TimeStampedValue<TimeSpan>(end, duration);
}

public static void Main(String[] args) {
    var clock = ClockStamp.ProgramStartTime;
    var r = TimeALoop(clock);
    var duration = r.Value; //the result
    clock = r.Time; //must now use returned clock, to avoid seeing old measurements
}

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.

Craig Gidney
źródło
Ciekawe, ale to i++w pętli for nie jest referencyjnie przezroczyste;)
snim2
@ snim2 Nie jestem idealny. : P Pociesz się tym, że brudna zmienność nie wpływa na referencyjną przezroczystość wyniku. Jeśli zdasz dwa razy ten sam „ostatni pomiar”, otrzymujesz przestarzały następny pomiar i zwrócisz ten sam wynik.
Craig Gidney
@Strilanc Dzięki za to. Myślę w kodzie imperatywnym, więc interesujące jest, aby pojęcia funkcjonalne zostały wyjaśnione w ten sposób. Mogę sobie wyobrazić język, w którym ten naturalny i syntaktycznie czystszy.
WW.
W rzeczywistości można również przejść monadą w języku C #, unikając w ten sposób wyraźnego upływu znaczników czasu. Potrzebujesz czegoś takiego struct TimeKleisli<Arg, Res> { private delegate Res(TimeStampedValue<Arg>); }. Ale kod z tym nadal nie wyglądałby tak ładnie jak Haskell ze doskładnią.
leftaroundabout
@leftaroundabout możesz udawać, że masz monadę w języku C #, implementując funkcję bind jako metodę o nazwie 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 :(
sara,
16

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.)

Jeffrey Aguilera
źródło
3
Jak to się ma do tego pytania?
Nawaz
5
Twoje pytanie dotyczyło modelowania zachowania zależnego od czasu w sposób czysto funkcjonalny, np. Funkcja, która zwraca bieżący zegar systemowy. Możesz albo przewlec coś równoważnego monadzie we / wy przez wszystkie funkcje i ich drzewo zależności, aby uzyskać dostęp do tego stanu; lub możesz modelować stan, definiując reguły obserwacji zamiast reguł konstruktywnych. Dlatego modelowanie stanu złożonego indukcyjnie w programowaniu funkcjonalnym wydaje się tak nienaturalne, ponieważ stan ukryty jest tak naprawdę właściwością koindukcyjną .
Jeffrey Aguilera
Świetne źródło! Czy jest coś nowszego? Społeczność JS nadal wydaje się mieć problemy z abstrakcyjnymi danymi strumieniowymi.
Dmitri Zaitsev,
12

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).

Conal
źródło
11

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ą.

MduSenthil
źródło
11

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.

Ankur
źródło
1
„W końcu oprogramowanie, które nie wchodzi w interakcje ze światem zewnętrznym, nie jest użyteczne poza wykonywaniem obliczeń matematycznych”. O ile rozumiem, nawet w tym przypadku dane wejściowe do obliczeń byłyby zakodowane na stałe w programie, również niezbyt przydatne. Gdy tylko chcesz odczytać dane wejściowe do obliczeń matematycznych z pliku lub terminala, potrzebujesz nieczystego kodu.
Giorgio
1
@Ankur: To dokładnie to samo. Jeśli program wchodzi w interakcję z czymś innym niż samym sobą (np. Światem za pomocą klawiatury, że tak powiem), nadal jest nieczysty.
tożsamość
1
@Ankur: Tak, myślę, że masz rację! Chociaż przekazywanie dużych danych wejściowych w wierszu poleceń może nie być zbyt praktyczne, może to być czysty sposób.
Giorgio
2
Posiadanie „obiektu świata”, w tym liczby osób mieszkających na świecie, podnosi komputer wykonujący pracę do niemal wszechwiedzącego poziomu. Myślę, że normalnym przypadkiem jest to, że obejmuje rzeczy takie jak liczba plików na dysku HD i katalog domowy bieżącego użytkownika.
ziggystar
4
@ziggystar - „obiekt świata” tak naprawdę niczego nie obejmuje - jest to po prostu proxy dla zmieniającego się stanu świata poza programem. Jego jedynym celem jest wyraźne oznaczenie stanu zmiennego w sposób umożliwiający jego identyfikację przez system typów.
Kris Nuttycombe,
7

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:

-- this is a pure function, written in functional style.
fib 0 = 0
fib 1 = 1
fib n = fib (n-1) + fib (n-2)

-- This is an impure wrapper around the pure function, written in imperative style
-- It depends on inputs and produces outputs.
main = do
    putStrLn "Please enter the input parameter"
    inputStr <- readLine
    putStrLn "Starting time:"
    getCurrentTime >>= print
    let inputInt = read inputStr    -- this line is pure
    let result = fib inputInt       -- this is also pure
    putStrLn "Result:"
    print result
    putStrLn "Ending time:"
    getCurrentTime >>= print
NovaDenizen
źródło
4
Byłoby pomocne, gdybyś mógł poradzić sobie z konkretnym problemem związanym z uzyskaniem czasu i wyjaśnił nieco, w jakim stopniu uważamy IOwartości i wyniki za czyste.
AndrewC,
W rzeczywistości nawet w 100% czyste programy nagrzewają procesor, co jest efektem ubocznym.
Jörg W Mittag
3

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ę, mainktó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ównaniu main = 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:

type Clock = Integer -- To make it more similar to the C code

-- An action that returns nothing, but might do something
main :: IO ()
main = do
    -- An action that returns an Integer, which we view as CPU Clock values
    c <- getCPUTime :: IO Clock
    -- An action that prints data, but returns nothing
    print (c + 2*1000*1000*1000*1000) :: IO ()

To ostatnie wydaje się konieczne, prawda?

MauganRa
źródło
1

Jeśli tak, to jak może istnieć? Czy nie narusza to zasady programowania funkcjonalnego? W szczególności narusza to przejrzystość referencyjną

Nie istnieje w sensie czysto funkcjonalnym.

A jeśli nie, to jak poznać aktualny czas w programowaniu 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 timewartości, odwołujemy się do IO Timewartości. Wszystko to byłoby czysto funkcjonalne. Nie odwołujemy się, timeale coś w stylu „odczytaj wartość rejestru czasu” .

Gdy faktycznie wykonamy program Haskell, akcja IO faktycznie miałaby miejsce.

Chris Stryczyński
źródło