Czyste funkcje: czy „brak efektów ubocznych” oznacza „zawsze ten sam wynik, przy tym samym wejściu”?

84

Dwa warunki definiujące funkcję puresą następujące:

  1. Brak skutków ubocznych (tj. Dozwolone są tylko zmiany w zakresie lokalnym)
  2. Zawsze zwracaj to samo wyjście, mając te same dane wejściowe

Jeśli pierwszy warunek jest zawsze prawdziwy, czy zdarza się, że drugi warunek nie jest prawdziwy?

Czy naprawdę jest to konieczne tylko w przypadku pierwszego warunku?

Magnus
źródło
3
Twoje pomieszczenia są źle określone. „Dane wejściowe” są zbyt szerokie. Można sądzić, że dwie funkcje mają różne rodzaje danych wejściowych. Ich argumenty i „środowiskowe” / „kontekstowe”. Funkcja, która zwraca czas systemowy, może być uznana za czystą (nawet jeśli nie jest to obv), jeśli nie rozróżni się tych dwóch rodzajów danych wejściowych.
Alexander - Przywróć Monikę
4
@Alexander: W kontekście „czystej funkcji”, „wejście” jest powszechnie rozumiane jako oznaczające parametry / argumenty, które są przekazywane jawnie (przez dowolny mechanizm używany przez język programowania). To część definicji „czystej funkcji”. Ale masz rację, ważne jest, aby pamiętać o definicji.
sleske
3
Trywialny kontrprzykład: zwraca wartość zmiennej globalnej. Brak efektów ubocznych (globalny jest zawsze czytany!), Ale wciąż potencjalnie różne wyniki za każdym razem. (Jeśli nie lubisz zmiennych globalnych, zwróć adres zmiennej lokalnej, która zależy od stosu wywołań w czasie wykonywania).
Peter - Przywróć Monikę
2
Musisz rozszerzyć swoją definicję „skutków ubocznych”; można powiedzieć, że to czysta metoda nie wytwarzają skutki uboczne, ale trzeba również pamiętać, że to czysta metoda nie zużywają skutki uboczne wyprodukowane gdzie indziej.
Eric Lippert
2
@sleske Być może powszechnie rozumiane, ale brak tego rozróżnienia jest dokładną przyczyną zamieszania w OP.
Alexander - Przywróć Monikę

Odpowiedzi:

114

Oto kilka kontrprzykładów, które nie zmieniają zakresu zewnętrznego, ale nadal są uważane za nieczyste:

  • function a() { return Date.now(); }
  • function b() { return window.globalMutableVar; }
  • function c() { return document.getElementById("myInput").value; }
  • function d() { return Math.random(); } (co wprawdzie zmienia PRNG, ale nie jest uważane za obserwowalne)

Dostęp do zmiennych niestałych i innych niż lokalne jest wystarczający, aby móc naruszyć drugi warunek.

Zawsze myślę o dwóch warunkach czystości jako uzupełniających się:

  • ocena wyniku nie może mieć wpływu na stan uboczny
  • stan boczny nie może mieć wpływu na wynik oceny

Termin efekt uboczny odnosi się tylko do pierwszej, funkcji modyfikującej stan nielokalny. Jednak czasami operacje odczytu są również uważane za skutki uboczne: gdy są operacjami i obejmują również zapis, nawet jeśli ich głównym celem jest uzyskanie dostępu do wartości. Przykładami tego są generowanie liczby pseudolosowej, która modyfikuje stan wewnętrzny generatora, odczytywanie ze strumienia wejściowego, który przesuwa do przodu pozycję odczytu lub odczytywanie z czujnika zewnętrznego, który zawiera polecenie „wykonaj pomiar”.

Bergi
źródło
1
Dzięki Bergi. Z jakiegoś powodu pomyślałem, że efekty uboczne obejmują odczyt zmiennych poza lokalnym zakresem, ale myślę, że jest to tylko efekt uboczny, jeśli pisze takie zmienne zewnętrzne.
Magnus
17
Jeśli prompt("you choose")nie ma skutków ubocznych, powinniśmy cofnąć się o krok i wyjaśnić znaczenie skutków ubocznych.
Holger
1
@Magnus Tak, dokładnie to oznacza efekt . Spróbuję też wyjaśnić w mojej odpowiedzi, nie spodziewałem się tak dużej uwagi i chcę, aby odpowiedź zasługiwała na dziesiątki głosów :-)
Bergi
2
Cóż, z tego, co wiesz, Math.random () zwraca diodę termiczną. W rzeczywistości nie jest określone, aby używać złego RNG.
Joshua
1
Z tych dwóch warunków słyszałem, że ten pierwszy nazywa się „efektami”, a drugi „efektami”. Oba są „skutkami ubocznymi” i są nieczyste. f (współczynniki, wkład) -> efekty, wynik Współczynniki to dane wejściowe pochodzące ze zmian w szerszym środowisku, efekty to dane wyjściowe, które zmieniają szersze środowisko. Na przykład Elm i Clojurescrips zmieniają ramy pracy z tym modelem.
30

„Normalnym” sposobem wyrażenia tego, czym jest czysta funkcja , jest referencyjna przezroczystość . Funkcja jest czysta, jeśli jest referencyjnie przezroczysta .

Z grubsza mówiąc, przezroczystość referencyjna oznacza, że ​​w dowolnym momencie programu można zastąpić wywołanie funkcji jej wartością zwracaną lub odwrotnie, bez zmiany znaczenia programu.

Na przykład, jeśli C printfbyłyby referencyjnie przezroczyste, te dwa programy powinny mieć to samo znaczenie:

printf("Hello");

i

5;

a wszystkie poniższe programy powinny mieć to samo znaczenie:

5 + 5;

printf("Hello") + 5;

printf("Hello") + printf("Hello");

Ponieważ printfzwraca liczbę zapisanych znaków, w tym przypadku 5.

W przypadku funkcji staje się to jeszcze bardziej oczywiste void. Jeśli mam funkcję void foo, to

foo(bar, baz, quux);

powinien być taki sam jak

;

To znaczy, ponieważ foonic nie zwraca, powinienem być w stanie zastąpić to niczym bez zmiany znaczenia programu.

Jest więc jasne, że ani, ani printfnie foosą referencyjnie przezroczyste, a zatem żadne z nich nie jest czyste. W rzeczywistości voidfunkcja nigdy nie może być referencyjnie przezroczysta, chyba że nie jest operacją.

Uważam, że ta definicja jest znacznie łatwiejsza w obsłudze niż ta, którą podałeś. Pozwala również na zastosowanie go w dowolnej szczegółowości: możesz zastosować go do pojedynczych wyrażeń, do funkcji, do całych programów. Pozwala na przykład porozmawiać o takiej funkcji:

func fib(n):
    return memo[n] if memo.has_key?(n)
    return 1 if n <= 1
    return memo[n] = fib(n-1) + fib(n-2)

Możemy przeanalizować wyrażenia, które składają się na funkcję i łatwo stwierdzić, że nie są one referencyjnie przezroczyste, a zatem nie są czyste, ponieważ używają zmiennej struktury danych, a mianowicie memotablicy. Możemy jednak również spojrzeć na funkcję i zobaczyć, że jest ona referencyjnie przezroczysta, a zatem czysta. Nazywa się to czasem czystością zewnętrzną , tj. Funkcją, która wydaje się czysta dla świata zewnętrznego, ale wewnętrznie jest zaimplementowana jako nieczysta.

Takie funkcje są nadal przydatne, ponieważ podczas gdy zanieczyszczenie infekuje wszystko wokół siebie, zewnętrzny czysty interfejs tworzy rodzaj „bariery czystości”, w której zanieczyszczenie infekuje tylko trzy wiersze funkcji, ale nie przedostaje się do reszty programu . Te trzy wiersze są znacznie łatwiejsze do przeanalizowania pod kątem poprawności niż cały program.

Jörg W Mittag
źródło
2
Ta nieczystość wpływa na cały program, gdy masz współbieżność.
R .. GitHub PRZESTAŃ POMÓC W LODZIE
@R .. Czy możesz wymyślić sposób, w jaki współbieżność mogłaby uczynić opisaną funkcję Fibonacciego zewnętrznie nieczystą? Nie mogę. Pisanie do memo[n]jest idempotentne, a niepowodzenie odczytu z niego powoduje jedynie marnowanie cykli procesora.
Brilliand
Zgadzam się z wami obojgiem. Zanieczyszczenie może prowadzić do problemów ze współbieżnością, ale tak nie jest w tym konkretnym przypadku.
Jörg W Mittag
@R .. Nie jest trudno wyobrazić sobie wersję obsługującą współbieżność.
user253751
1
@Brilliand Na przykład, memo[n] = ...może najpierw utworzyć wpis w słowniku, a następnie zapisać w nim wartość. To pozostawia okno, w którym inny wątek może zobaczyć niezainicjowany wpis.
user253751
12

Wydaje mi się, że drugi warunek, który opisałeś, jest słabszym ograniczeniem niż pierwszy.

Podam ci przykład, przypuśćmy, że masz funkcję dodawania takiej, która również loguje się do konsoli:

function addOneAndLog(x) {
  console.log(x);
  return x + 1;
}

Drugi podany warunek jest spełniony: ta funkcja zawsze zwraca te same dane wyjściowe, gdy ma te same dane wejściowe. Nie jest to jednak czysta funkcja, ponieważ zawiera efekt uboczny logowania do konsoli.

Czysta funkcja to, ściśle mówiąc, funkcja spełniająca właściwość funkcji przezroczystości referencyjnej . Jest to właściwość, którą możemy zastąpić aplikację funkcji wartością, którą wytwarza, bez zmiany zachowania programu.

Załóżmy, że mamy funkcję, która po prostu dodaje:

function addOne(x) {
  return x + 1;
}

Możemy wymienić addOne(5) w 6dowolnym miejscu w naszym programie i nic się nie zmieni.

Natomiast nie możemy tego zastąpić addOneAndLog(x) wartością 6w naszym programie bez zmiany zachowania, ponieważ pierwsze wyrażenie powoduje, że coś jest zapisywane na konsoli, a drugie nie.

Każde z tych dodatkowych zachowań, które addOneAndLog(x)występuje poza zwracaniem danych wyjściowych, traktujemy jako efekt uboczny .

TheInnerLight
źródło
- Wydaje mi się, że drugi warunek, który opisałeś, jest słabszym ograniczeniem niż pierwszy. Nie, te dwa warunki są logicznie niezależne.
sleske
@sleske mylisz się. Podałem jasne definicje terminów „czysty” i „efekt uboczny”. W ramach tych ograniczeń nie ma nic, co by funkcja bez skutków ubocznych zwracała te same dane wyjściowe dla danego wejścia. Podałem jednak przykłady, w których drugi warunek może być spełniony bez pierwszego. Podstawową koncepcją rozumienia pojęcia czystości jest przejrzystość referencyjna.
TheInnerLight
Mała literówka: nic nie może zrobić funkcja bez efektów ubocznych , poza zwracaniem tego samego wyjścia dla danego wejścia.
TheInnerLight
A co z czymś takim jak powrót aktualnego czasu? Nie ma to skutków ubocznych, ale zwraca inny wynik dla tego samego wejścia. Mówiąc bardziej ogólnie, każda funkcja, której wynik zależy nie tylko od parametrów wejściowych, ale także od (zmienialnej) zmiennej globalnej.
sleske
2
Wygląda na to, że używasz innej definicji „efektu ubocznego” niż ta, która jest powszechnie stosowana. Efekt uboczny jest powszechnie definiowany jako „obserwowalny efekt poza zwracaniem wartości” lub „obserwowalna zmiana stanu” - patrz np. Wikipedia , ten post na softwareengineering.SE . Masz całkowitą rację, co Date.now()nie jest czyste / referencyjnie przezroczyste, ale nie dlatego, że ma skutki uboczne, ale dlatego, że jego wynik zależy od czegoś więcej niż tylko jego wkładu.
sleske
7

Może istnieć źródło losowości spoza systemu. Załóżmy, że część obliczeń obejmuje temperaturę w pomieszczeniu. Wtedy wykonanie funkcji za każdym razem da różne wyniki w zależności od losowego elementu zewnętrznego temperatury pokojowej. Stan nie jest zmieniany przez wykonanie programu.

W każdym razie wszystko, o czym mogę myśleć.

user3340459
źródło
3
Według mnie owa „przypadkowość spoza systemu” jest formą efektu ubocznego. Funkcje z takimi zachowaniami nie są „czystymi”.
Joseph M. Dion
2

Problem z definicjami PR polega na tym, że są one bardzo sztuczne. Każda ocena / obliczenie ma skutki uboczne dla oceniającego. To teoretycznie prawda. Zaprzeczanie temu pokazuje jedynie, że apologeci FP ignorują filozofię i logikę: „ocena” oznacza zmianę stanu jakiegoś inteligentnego środowiska (maszyny, mózgu itp.). Taka jest natura procesu oceny. Bez zmian - bez „kamieni”. Efekt może być bardzo widoczny: rozgrzanie procesora lub jego awaria, wyłączenie płyty głównej w przypadku przegrzania i tak dalej.

Kiedy mówisz o przezroczystości referencyjnej, powinieneś zrozumieć, że informacje o takiej przezroczystości są dostępne dla człowieka jako twórcy całego systemu i posiadacza informacji semantycznej i mogą być niedostępne dla kompilatora. Na przykład funkcja może odczytać jakiś zasób zewnętrzny i będzie miała monadę IO w podpisie, ale będzie zwracać tę samą wartość przez cały czas (na przykład wynik current_year > 0). Kompilator nie wie, że funkcja zwróci zawsze ten sam wynik, więc funkcja jest nieczysta, ale ma referencyjną przezroczystość i można ją zastąpić Truestałą.

Aby więc uniknąć takiej niedokładności, powinniśmy rozróżnić funkcje matematyczne i „funkcje” w językach programowania. Funkcje w Haskell są zawsze nieczyste, a związana z nimi definicja czystości jest zawsze bardzo warunkowa: działają one na prawdziwym sprzęcie z rzeczywistymi efektami ubocznymi i właściwościami fizycznymi, co jest złe w przypadku funkcji matematycznych. Oznacza to, że przykład z funkcją „printf” jest całkowicie niepoprawny.

Ale nie wszystkie funkcje matematyczne są również czyste: każda funkcja, która ma t(czas) jako parametr, może być nieczysta: tzawiera wszystkie efekty i stochastyczny charakter funkcji: w typowym przypadku masz sygnał wejściowy i nie masz pojęcia o rzeczywistych wartościach, może być nawet hałasem.

Losowe B.
źródło
2

Jeśli pierwszy warunek jest zawsze prawdziwy, czy zdarza się, że drugi warunek nie jest prawdziwy?

tak

Rozważ prosty fragment kodu poniżej

public int Sum(int a, int b) {
    Random rnd = new Random();
    return rnd.Next(1, 10);
}

Ten kod zwróci losowe dane wyjściowe dla tego samego podanego zestawu danych wejściowych - jednak nie ma to żadnego efektu ubocznego.

Ogólny efekt obu punktów # 1 i # 2, o których wspomniałeś, w połączeniu razem oznacza: W dowolnym momencie, jeśli funkcja Sumz tym samym i / p zostanie zastąpiona wynikiem w programie, ogólne znaczenie programu nie zmienia się . To nic innego jak przejrzystość referencyjna .

rahulaga_dev
źródło
Ale w tym przypadku pierwszy warunek nie jest weryfikowany: pisanie na konsoli jest uważane za efekt uboczny, ponieważ zmienia stan samej maszyny.
Prawa noga
@Rightleg thx za wskazanie tego. Jakoś zupełnie inaczej zrozumiałem OP. poprawiona odpowiedź.
rahulaga_dev
2
Czy to nie zmienia stanu generatora losowego?
Eric Duminil
1
Generowanie liczby losowej samo w sobie jest efektem ubocznym, chyba że stan generatora liczb losowych jest
podany
1
rndnie wymyka się funkcji, więc fakt, że zmienia się jej stan, nie ma znaczenia dla czystości funkcji, ale fakt, że Randomkonstruktor używa bieżącego czasu jako wartości początkowej, oznacza, że ​​istnieją „wejścia” inne niż ai b.
Sneftel