Czy to czysta funkcja?


117

Większość źródeł definiuje czystą funkcję jako posiadającą następujące dwie właściwości:

  1. Jego wartość zwracana jest taka sama dla tych samych argumentów.
  2. Jego ocena nie ma skutków ubocznych.

To pierwszy warunek, który mnie dotyczy. W większości przypadków łatwo jest to ocenić. Rozważ następujące funkcje JavaScript (jak pokazano w tym artykule )

Czysty:

const add = (x, y) => x + y;

add(2, 4); // 6

Zanieczyszczony:

let x = 2;

const add = (y) => {
  return x += y;
};

add(4); // x === 6 (the first time)
add(4); // x === 10 (the second time)

Łatwo zauważyć, że druga funkcja daje różne wyjścia dla kolejnych połączeń, naruszając w ten sposób pierwszy warunek. I dlatego jest nieczyste.

Dostaję tę część.


Teraz, na moje pytanie, rozważmy tę funkcję, która zamienia daną kwotę w dolarach na euro:

(EDYCJA - Użycie constw pierwszym wierszu. Użyto letwcześniej przypadkowo.)

const exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x) => {
  return x * exchangeRate;
};

dollarToEuro(100) //90 today

dollarToEuro(100) //something else tomorrow

Załóżmy, że pobieramy kurs wymiany z bazy danych i zmienia się on codziennie.

Teraz, bez względu na to, ile razy dzisiaj wywołuję tę funkcję , da mi to samo wyjście dla wejścia100 . Jednak jutro może dać mi inne wyjście. Nie jestem pewien, czy to narusza pierwszy warunek, czy nie.

IOW, sama funkcja nie zawiera żadnej logiki do mutowania danych wejściowych, ale opiera się na zewnętrznej stałej, która może ulec zmianie w przyszłości. W takim przypadku jest absolutnie pewne, że zmieni się codziennie. W innych przypadkach może się zdarzyć; może nie.

Czy takie funkcje możemy nazwać funkcjami czystymi. Jeśli odpowiedź brzmi NIE, to jak możemy to zmienić na jedno?


6
Czystość tak dynamicznego języka, jakim jest JS, jest bardzo skomplikowanym tematem:function myNumber(n) { this.n = n; }; myNumber.prototype.valueOf = function() { console.log('impure'); return this.n; }; const n = new myNumber(42); add(n, 1);
zerkms

29
Czystość oznacza, że ​​można zastąpić wywołanie funkcji wartością wynikową na poziomie kodu bez zmiany zachowania programu.
Bob

1
Aby pójść nieco dalej o tym, co stanowi efekt uboczny, oraz z bardziej teoretyczną terminologią, zobacz cs.stackexchange.com/questions/116377/…
Gilles 'SO - przestań być zły'

3
Dziś funkcja jest (x) => {return x * 0.9;}. Jutro będziesz miał inną funkcję, która może być również czysta (x) => {return x * 0.89;}. Zauważ, że przy każdym uruchomieniu (x) => {return x * exchangeRate;}tworzy nową funkcję, która jest czysta, ponieważ exchangeRatenie można jej zmienić.
user253751,

2
To jest nieczysta funkcja, jeśli chcesz ją uczynić czystą, możesz użyć jej const dollarToEuro = (x, exchangeRate) => { return x * exchangeRate; }; do czystej funkcji, Its return value is the same for the same arguments.powinna ona zawsze obowiązywać, 1 sekunda, 1 dekada .. później bez względu na wszystko
Vikash Tiwari,

Odpowiedzi:


133

W dollarToEuro„s zwracana wartość zależy od zmiennej zewnętrznej, która nie jest argumentem; dlatego funkcja jest nieczysta.

W odpowiedzi brzmi NIE, w jaki więc sposób możemy przefiltrować funkcję, aby była czysta?

Jedną z opcji jest przejście exchangeRate. W ten sposób, co argumenty czasowe (something, somethingElse), wyjście jest gwarantowana być something * somethingElse:

const exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x, exchangeRate) => {
  return x * exchangeRate;
};

Pamiętaj, że w przypadku programowania funkcjonalnego należy unikać let- zawsze używaj, constaby uniknąć zmiany przypisania.


6
Nie mając zmienne wolne nie jest to wymóg dla funkcją być czysty: const add = x => y => x + y; const one = add(42);Tutaj zarówno addi onesą czyste funkcje.
zerkms

7
const foo = 42; const add42 = x => x + foo;<- to kolejna czysta funkcja, która ponownie używa wolnych zmiennych.
zerkms

8
@zerkms - Bardzo chętnie zobaczę twoją odpowiedź na to pytanie (nawet jeśli po prostu przepisuje CertainPerformance, aby użyć innej terminologii). Nie sądzę, żeby był powielany i pouczający, szczególnie gdy jest cytowany (najlepiej z lepszymi źródłami niż artykuł w Wikipedii powyżej, ale jeśli to wszystko, co dostajemy, nadal wygrywamy). (Łatwo byłoby przeczytać ten komentarz w jakimś negatywnym świetle. Zaufaj mi, że jestem szczery, myślę, że taka odpowiedź byłaby świetna i chciałbym ją przeczytać.)
TJ Crowder

17
Myślę, że ty i @zerkms się mylicie. Wydaje ci się, że dollarToEurofunkcja w przykładzie w twojej odpowiedzi jest nieczysta, ponieważ zależy od wolnej zmiennej exchangeRate. To absurdalne. Jak wskazał Zerkms, czystość funkcji nie ma nic wspólnego z tym, czy ma wolne zmienne. Jednak Zerkms również się myli, ponieważ uważa, że dollarToEurofunkcja jest nieczysta, ponieważ zależy od tego, exchangeRatektóre dane pochodzą z bazy danych. Mówi, że jest nieczyste, ponieważ „zależy to od IO w sposób tranzytowy”.
Aadit M Shah

9
(cd.) Ponownie, to absurdalne, ponieważ sugeruje, że dollarToEurojest nieczyste, ponieważ exchangeRatejest zmienną swobodną. Sugeruje to, że gdyby exchangeRatenie była zmienną swobodną, ​​tj. Gdyby był argumentem, dollarToEurobyłby czysty. Dlatego sugeruje, że dollarToEuro(100)jest to nieczyste, ale dollarToEuro(100, exchangeRate)czyste. Jest to oczywiście absurdalne, ponieważ w obu przypadkach zależysz od tego, exchangeRateco pochodzi z bazy danych. Jedyną różnicą jest to, czy exchangeRatejest wolną zmienną w ramach dollarToEurofunkcji.
Aadit M Shah

76

Technicznie rzecz biorąc, każdy program uruchamiany na komputerze jest nieczysty, ponieważ ostatecznie kompiluje się do instrukcji takich jak „przenieś tę wartość do eax” i „dodaj tę wartość do zawartościeax ”, które są nieczyste. To nie jest bardzo pomocne.

Zamiast tego myślimy o czystości za pomocą czarnych skrzynek . Jeśli jakiś kod zawsze generuje te same dane wyjściowe, gdy otrzyma te same dane wejściowe, wówczas jest uważany za czysty. Zgodnie z tą definicją następująca funkcja jest również czysta, chociaż wewnętrznie korzysta z nieczystej tabeli notatek.

const fib = (() => {
    const memo = [0, 1];

    return n => {
      if (n >= memo.length) memo[n] = fib(n - 1) + fib(n - 2);
      return memo[n];
    };
})();

console.log(fib(100));

Nie dbamy o elementy wewnętrzne, ponieważ używamy metodologii czarnej skrzynki do sprawdzania czystości. Podobnie nie obchodzi nas, że cały kod jest ostatecznie konwertowany na nieczyste instrukcje maszynowe, ponieważ myślimy o czystości przy użyciu metodologii czarnej skrzynki. Elementy wewnętrzne nie są ważne.

Teraz rozważ następującą funkcję.

const greet = name => {
    console.log("Hello %s!", name);
};

greet("World");
greet("Snowman");

Czy greetfunkcja jest czysta czy nieczysta? Według naszej metodologii czarnej skrzynki, jeśli damy jej ten sam wkład (npWorld ), To zawsze wypisuje to samo wyjście na ekran (tj Hello World!.). W tym sensie, czy to nie jest czyste? Nie, nie jest. Powodem, dla którego nie jest czysty, jest to, że uważamy wydrukowanie czegoś na ekranie za efekt uboczny. Jeśli nasza czarna skrzynka wywołuje skutki uboczne, to nie jest czysta.

Co to jest efekt uboczny? Tutaj przydatna jest koncepcja przejrzystości referencyjnej . Jeśli funkcja jest referencyjnie przezroczysta, zawsze możemy zastąpić zastosowania tej funkcji ich wynikami. Pamiętaj, że to nie to samo co wstawianie funkcji .

Podczas wstawiania funkcji zastępujemy aplikacje funkcji treścią funkcji bez zmiany semantyki programu. Jednak referencyjnie przezroczysta funkcja może zawsze zostać zastąpiona wartością zwracaną bez zmiany semantyki programu. Rozważ następujący przykład.

console.log("Hello %s!", "World");
console.log("Hello %s!", "Snowman");

Tutaj wprowadziliśmy definicję greet i nie zmieniło to semantyki programu.

Teraz rozważ następujący program.

undefined;
undefined;

Tutaj zastąpiliśmy aplikacje greet funkcji ich wartościami zwracanymi i zmieniło to semantykę programu. Nie drukujemy już powitań na ekranie. To jest powód, dla którego drukowanie jest uważane za efekt uboczny, i dlategogreet funkcja jest nieczysta. Nie jest referencyjnie przejrzysty.

Rozważmy teraz inny przykład. Rozważ następujący program.

const main = async () => {
    const response = await fetch("https://time.akamai.com/");
    const serverTime = 1000 * await response.json();
    const timeDiff = time => time - serverTime;
    console.log("%d ms", timeDiff(Date.now()));
};

main();

Oczywiście mainfunkcja jest nieczysta. Czy jednak timeDifffunkcja jest czysta czy nieczysta? Chociaż zależy to od tego, serverTimeco pochodzi z nieczystego połączenia sieciowego, nadal jest referencyjnie przezroczyste, ponieważ zwraca te same dane wyjściowe dla tych samych danych wejściowych i ponieważ nie ma żadnych skutków ubocznych.

Zerkms prawdopodobnie się ze mną nie zgodzi w tej kwestii. W swojej odpowiedzi powiedział, że dollarToEurofunkcja w poniższym przykładzie jest nieczysta, ponieważ „zależy to od IO w sposób tranzytowy”.

const exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x, exchangeRate) => {
  return x * exchangeRate;
};

Nie mogę się z nim nie zgodzić, ponieważ fakt, że exchangeRatepochodzi z bazy danych, jest nieistotny. Jest to wewnętrzny szczegół, a nasza metodologia czarnej skrzynki do określania czystości funkcji nie dba o szczegóły wewnętrzne.

W czysto funkcjonalnych językach, takich jak Haskell, mamy luk awaryjny do wykonywania dowolnych efektów We / Wy. Nazywa się unsafePerformIO, a jak sama nazwa wskazuje, jeśli nie użyjesz go poprawnie, nie jest to bezpieczne, ponieważ może złamać przejrzystość referencyjną. Jeśli jednak wiesz, co robisz, korzystanie z niego jest całkowicie bezpieczne.

Zwykle służy do ładowania danych z plików konfiguracyjnych w pobliżu początku programu. Ładowanie danych z plików konfiguracyjnych jest nieczystą operacją We / Wy. Jednak nie chcemy być obciążani przez przekazywanie danych jako danych wejściowych do każdej funkcji. Zatem jeśli użyjemy unsafePerformIO, możemy załadować dane na najwyższym poziomie, a wszystkie nasze czyste funkcje mogą zależeć od niezmiennych globalnych danych konfiguracyjnych.

Pamiętaj, że fakt, że funkcja zależy od niektórych danych załadowanych z pliku konfiguracyjnego, bazy danych lub połączenia sieciowego, nie oznacza, że ​​funkcja jest nieczysta.

Rozważmy jednak twój oryginalny przykład, który ma inną semantykę.

let exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x) => {
  return x * exchangeRate;
};

dollarToEuro(100) //90 today

dollarToEuro(100) //something else tomorrow

Tutaj zakładam, że ponieważ exchangeRatenie jest zdefiniowany jako const, zostanie zmodyfikowany podczas działania programu. W takim przypadku dollarToEurojest to z pewnością funkcja nieczysta, ponieważ po exchangeRatezmodyfikowaniu spowoduje to przerwanie przezroczystości referencyjnej.

Jeśli jednak exchangeRatezmienna nie zostanie zmodyfikowana i nigdy nie będzie modyfikowana w przyszłości (tj. Jeśli będzie stałą wartością), to nawet jeśli zostanie zdefiniowana jako let, nie złamie przejrzystości odniesienia. W takim przypadku dollarToEurojest rzeczywiście funkcją czystą.

Zauważ, że wartość exchangeRatemoże się zmieniać za każdym razem, gdy uruchomisz program ponownie i nie spowoduje to naruszenia przejrzystości odniesienia. Przerywa przezroczystość referencyjną tylko wtedy, gdy zmienia się podczas działania programu.

Na przykład, jeśli uruchomisz mój timeDiffprzykład wiele razy, otrzymasz różne wartości, serverTimea zatem różne wyniki. Ponieważ jednak wartość serverTimenigdy się nie zmienia podczas działania programu, timeDifffunkcja jest czysta.


3
To było bardzo pouczające. Dzięki. I chciałem użyć constw moim przykładzie.
Snowman

3
Jeśli chciałeś użyć, constto dollarToEurofunkcja jest rzeczywiście czysta. Jedynym sposobem zmiany wartości exchangeRatebyłoby ponowne uruchomienie programu. W takim przypadku stary proces i nowy proces są różne. Dlatego nie narusza przejrzystości odniesienia. To jak dwukrotne wywołanie funkcji z różnymi argumentami. Argumenty mogą być różne, ale w funkcji wartość argumentów pozostaje stała.
Aadit M Shah

3
Brzmi to jak mała teoria o teorii względności: stałe są tylko względnie stałe, nie absolutnie, mianowicie w stosunku do uruchomionego procesu. Jest to oczywiście jedyna właściwa odpowiedź tutaj. +1.
Bob

5
Nie zgadzam się z „jest nieczyste, ponieważ ostatecznie kompiluje się do instrukcji takich jak„ przenieś tę wartość do eax ”i„ dodaj tę wartość do zawartości eax ” . Jeśli eaxzostanie wyczyszczony - przez obciążenie lub wyczyszczenie - kod pozostanie deterministyczny niezależnie od co jeszcze się dzieje i dlatego jest czyste. W przeciwnym razie bardzo wyczerpująca odpowiedź
3Dave

3
@Bergi: W rzeczywistości w czystym języku z niezmiennymi wartościami tożsamość jest nieistotna. To, czy dwa odniesienia, które oceniają tę samą wartość, są dwoma odniesieniami do tego samego obiektu lub do różnych obiektów, można zaobserwować tylko poprzez mutację obiektu za pomocą jednego z odniesień i obserwację, czy wartość zmienia się również po odzyskaniu przez inne odniesienie. Bez mutacji tożsamość staje się nieistotna. (Jak powiedziałby Rich Hickey: Tożsamość to seria stanów w czasie.)
Jörg W Mittag

23

Odpowiedź purystów (gdzie „ja” to dosłownie ja, ponieważ myślę, że to pytanie nie ma ani jednego formalnego „właściwej” odpowiedzi):

W tak dynamicznym języku jak JS z tak wieloma możliwościami małpowania typów bazowych łatek lub tworzenia niestandardowych typów za pomocą takich funkcji, jak Object.prototype.valueOfnie można stwierdzić, czy funkcja jest czysta, patrząc na nią, ponieważ to od dzwoniącego zależy, czy chce wywoływać skutki uboczne.

Demo:

const add = (x, y) => x + y;

function myNumber(n) { this.n = n; };
myNumber.prototype.valueOf = function() {
    console.log('impure'); return this.n;
};

const n = new myNumber(42);

add(n, 1); // this call produces a side effect

Odpowiedź me-pragmatyka:

Z samej definicji z wikipedii

W programowaniu komputerowym czysta funkcja to funkcja, która ma następujące właściwości:

  1. Zwracana wartość jest taka sama dla tych samych argumentów (bez zmian z lokalnymi zmiennymi statycznymi, zmiennymi nielokalnymi, zmiennymi argumentami referencyjnymi lub strumieniami wejściowymi z urządzeń I / O).
  2. Jego ocena nie ma skutków ubocznych (brak mutacji lokalnych zmiennych statycznych, zmiennych nielokalnych, zmiennych argumentów referencyjnych lub strumieni We / Wy).

Innymi słowy, ma znaczenie tylko to, jak zachowuje się funkcja, a nie sposób jej implementacji. I dopóki konkretna funkcja posiada te 2 właściwości - jest czysta, niezależnie od tego, jak dokładnie została zaimplementowana.

Teraz do twojej funkcji:

const exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x, exchangeRate) => {
  return x * exchangeRate;
};

Jest to nieczyste, ponieważ nie spełnia wymogu 2: zależy on tranzytowo od zamówienia.

Zgadzam się, że powyższe stwierdzenie jest nieprawidłowe, zobacz drugą odpowiedź w celu uzyskania szczegółowych informacji: https://stackoverflow.com/a/58749249/251311

Inne odpowiednie zasoby:


4
@TJCrowder mejako zerkms, który udziela odpowiedzi.
zerkms

2
Tak, w Javascripcie chodzi o pewność, a nie gwarancje
Bob

4
@ Bob ... lub to połączenie blokujące.
zerkms

1
@zerkms - Dzięki. Tylko w 100% jestem pewien, że kluczową różnicą między twoim add42a moim addXjest to, że moje xmoże zostać zmienione, a twoje ftnie może zostać zmienione (a zatem add42wartość zwracana nie zależy od ft)?
TJ Crowder

5
Nie zgadzam się, że dollarToEurofunkcja w twoim przykładzie jest nieczysta. Wyjaśniłem, dlaczego nie zgadzam się w mojej odpowiedzi. stackoverflow.com/a/58749249/783743
Aadit M Shah

14

Podobnie jak inne odpowiedzi, sposób, w jaki zostałeś wdrożony dollarToEuro,

let exchangeRate = fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x) => { return x * exchangeRate; }; 

jest rzeczywiście czysty, ponieważ kurs wymiany nie jest aktualizowany podczas działania programu. Pod względem koncepcyjnym dollarToEurowydaje się jednak, że powinna być nieczystą funkcją, ponieważ korzysta z najbardziej aktualnego kursu walutowego. Najprostszym sposobem wyjaśnienia tej rozbieżności jest to, że nie wdrożyłeś, dollarToEuroale dollarToEuroAtInstantOfProgramStart.

Kluczem tutaj jest to, że istnieje kilka parametrów, które są wymagane do obliczenia przeliczenia waluty i że prawdziwie czysta wersja generała dollarToEurozapewniłaby wszystkie z nich. Najbardziej bezpośrednie parametry to kwota USD do przeliczenia oraz kurs wymiany. Ponieważ jednak chcesz uzyskać kurs wymiany z opublikowanych informacji, masz teraz trzy parametry do zapewnienia:

  • Ilość pieniędzy do wymiany
  • Historyczny autorytet do konsultacji w sprawie kursów walut
  • Data transakcji (w celu zindeksowania organu historycznego)

Autorytet historyczny tutaj jest twoją bazą danych i przy założeniu, że baza danych nie zostanie naruszona, zawsze zwróci ten sam wynik dla kursu wymiany w danym dniu. Dlatego dzięki kombinacji tych trzech parametrów możesz napisać w pełni czystą, samowystarczalną wersję generała dollarToEuro, która może wyglądać mniej więcej tak:

function dollarToEuro(x, authority, date) {
    const exchangeRate = authority(date);
    return x * exchangeRate;
}

dollarToEuro(100, fetchFromDatabase, Date.now());

Wdrożenie przechwytuje stałe wartości zarówno dla autorytetu historycznego, jak i daty transakcji w momencie utworzenia funkcji - autorytetem historycznym jest twoja baza danych, a przechwycona data jest datą uruchomienia programu - pozostaje tylko kwota w dolarach , które zapewnia osoba dzwoniąca. Zanieczyszczona wersja, dollarToEuroktóra zawsze otrzymuje najbardziej aktualną wartość, zasadniczo przyjmuje parametr date domyślnie, ustawiając go na moment wywołania funkcji, co nie jest czyste, ponieważ nigdy nie można wywołać funkcji z tymi samymi parametrami dwukrotnie.

Jeśli chcesz mieć czystą wersję, dollarToEuroktóra nadal może uzyskać najbardziej aktualną wartość, możesz nadal powiązać autorytet historyczny, ale pozostaw parametr date niezwiązany i poproś o podanie argumentu osoby wywołującej jako argumentu, co zakończy się z czymś takim:

function dollarToEuro(x, date) {
    const exchangeRate = fetchFromDatabase(date);
    return x * exchangeRate;
}

dollarToEuro(100, Date.now());

@Snowman Nie ma za co! Zaktualizowałem nieco odpowiedź, aby dodać więcej przykładów kodu.
TheHansinator,

8

Chciałbym wycofać się trochę z konkretnych szczegółów JS i abstrakcji formalnych definicji, i porozmawiać o tym, jakie warunki należy spełnić, aby umożliwić określone optymalizacje. Jest to zwykle najważniejsza rzecz, na której nam zależy podczas pisania kodu (chociaż pomaga to również udowodnić poprawność). Programowanie funkcjonalne nie jest ani przewodnikiem po najnowszych modzie, ani zakonnym ślubem samozaparcia. Jest to narzędzie do rozwiązywania problemów.

Gdy masz taki kod:

let exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x) => {
  return x * exchangeRate;
};

dollarToEuro(100) //90 today

dollarToEuro(100) //something else tomorrow

Jeśli exchangeRatenigdy nie można go zmodyfikować między dwoma połączeniami do dollarToEuro(100), można zapamiętać wynik pierwszego połączenia dollarToEuro(100)i zoptymalizować drugie połączenie. Wynik będzie taki sam, więc możemy po prostu zapamiętać wartość sprzed.

The exchangeRateMożna ustawić raz, przed wywołaniem jakiejkolwiek funkcji, która wygląda go i nigdy modyfikowane. Mniej restrykcyjnie, możesz mieć kod, który wyszukuje exchangeRateraz określoną funkcję lub blok kodu i używa tego samego kursu wymiany konsekwentnie w tym zakresie. Lub, jeśli tylko ten wątek może modyfikować bazę danych, masz prawo założyć, że jeśli nie zaktualizujesz kursu walutowego, nikt inny go nie zmienił.

Jeśli fetchFromDatabase()sama w sobie jest czystą funkcją ewaluującą do stałej, iexchangeRate jest niezmienna, moglibyśmy złożyć tę stałą przez cały czas obliczeń. Kompilator, który wie, że tak jest, może dokonać tej samej dedukcji, którą zrobiłeś w komentarzu, która ma dollarToEuro(100)wartość 90.0, i zastąpić całe wyrażenie stałą 90.0.

Jeśli jednak fetchFromDatabase()nie wykonuje operacji wejścia / wyjścia, co uważa się za efekt uboczny, jego nazwa narusza zasadę najmniejszego zdziwienia.


8

Ta funkcja nie jest czysta, opiera się na zmiennej zewnętrznej, która prawie na pewno się zmieni.

Dlatego funkcja zawodzi w pierwszym punkcie, który podałeś, nie zwraca tej samej wartości dla tych samych argumentów.

Aby ta funkcja była „czysta”, przekaż exchangeRatejako argument.

Spełniałoby to oba warunki.

  1. Zawsze zwracałby tę samą wartość przy przekazywaniu tej samej wartości i kursu walutowego.
  2. Nie miałoby to również skutków ubocznych.

Przykładowy kod:

const dollarToEuro = (x, exchangeRate) => {
  return x * exchangeRate;
};

dollarToEuro(100, fetchFromDatabase())

1
„który prawie na pewno się zmieni” - tak nie jest const.
zerkms

7

Aby rozwinąć kwestie, które inni mówili o przezroczystości referencyjnej: możemy zdefiniować czystość jako po prostu przezroczystość referencyjną wywołań funkcji (tj. Każde wywołanie funkcji można zastąpić wartością zwracaną bez zmiany semantyki programu).

Dwie podane właściwości są konsekwencjami przezroczystości referencyjnej. Na przykład następująca funkcja f1jest nieczysta, ponieważ za każdym razem nie daje tego samego rezultatu (właściwość o numerze 1):

function f1(x, y) {
  if (Math.random() > 0.5) { return x; }
  return y;
}

Dlaczego ważne jest, aby uzyskać ten sam wynik za każdym razem? Ponieważ uzyskiwanie różnych wyników jest jednym ze sposobów, aby wywołanie funkcji miało inną semantykę od wartości, a tym samym złamało przejrzystość referencyjną.

Powiedzmy, że piszemy kod f1("hello", "world"), uruchamiamy go i otrzymujemy wartość zwracaną "hello". Jeśli wykonamy wyszukiwanie / zamianę każdego wywołania f1("hello", "world")i zastąpimy je, "hello"zmienimy semantykę programu (wszystkie wywołania zostaną teraz zastąpione przez "hello", ale pierwotnie oceniłaby je około połowa "world"). W związku z tym wezwania do f1nie są względnie przejrzyste, a zatem f1są nieczyste.

Innym sposobem, w jaki wywołanie funkcji może mieć inną semantykę niż wartość, jest wykonywanie instrukcji. Na przykład:

function f2(x) {
  console.log("foo");
  return x;
}

Zwracana wartość f2("bar")zawsze będzie "bar", ale semantyka wartości "bar"różni się od wywołania, f2("bar")ponieważ ta ostatnia również loguje się do konsoli. Zastąpienie jednego z drugim zmieniłoby semantykę programu, więc nie jest referencyjnie przezroczyste, a zatem f2nieczyste.

To, czy twoja dollarToEurofunkcja jest referencyjnie przejrzysta (a zatem czysta), zależy od dwóch rzeczy:

  • „Zakres” tego, co uważamy za relatywnie przejrzyste
  • Czy exchangeRatekiedykolwiek zmieni się w ramach tego „zakresu”

Nie ma „najlepszego” zakresu do użycia; zwykle myślimy o jednym uruchomieniu programu lub o czasie jego trwania. Analogicznie, wyobraź sobie, że zwracane wartości każdej funkcji są buforowane (podobnie jak tabela notatek w przykładzie podanym przez @ aadit-m-shah): kiedy powinniśmy wyczyścić pamięć podręczną, aby zagwarantować, że nieaktualne wartości nie będą kolidować z naszymi semantyka?

Jeśli exchangeRateużywasz var, może się zmieniać między każdym połączeniem z dollarToEuro; musielibyśmy wyczyścić wyniki z pamięci podręcznej między każdym połączeniem, więc nie byłoby mowy o przejrzystości odniesienia.

Korzystając z tej opcji const, rozszerzamy „zasięg” o przebieg programu: bezpieczne byłoby buforowanie zwracanych wartości do dollarToEuroczasu zakończenia programu. Możemy sobie wyobrazić użycie makra (w języku takim jak Lisp) w celu zastąpienia wywołań funkcji ich wartościami zwracanymi. Ta ilość czystości jest wspólna dla takich rzeczy, jak wartości konfiguracji, opcje wiersza poleceń lub unikalne identyfikatory. Jeśli ograniczymy się do myślenia o jednym uruchomieniu programu, uzyskamy większość korzyści związanych z czystością, ale musimy zachować ostrożność na różnych etapach (np. Zapisywać dane do pliku, a następnie ładować je w innym uruchomieniu). Nie nazwałbym takich funkcji „czystymi” w sensie abstrakcyjnym (np. Gdybym pisał definicję słownika), ale nie mam problemu z traktowaniem ich jako czystych w kontekście .

Jeśli traktujemy czas trwania projektu jako „zakres”, to jesteśmy „najbardziej referencyjnie przejrzysty”, a zatem „najczystszy”, nawet w sensie abstrakcyjnym. Nigdy nie musielibyśmy usuwać naszej hipotetycznej pamięci podręcznej. Możemy nawet zrobić to „buforowanie” poprzez bezpośrednie przepisanie kodu źródłowego na dysku, aby zastąpić wywołania ich wartościami zwracanymi. To działałoby nawet w przypadku różnych projektów, np. Moglibyśmy wyobrazić sobie internetową bazę danych funkcji i ich zwracanych wartości, w której każdy może wyszukać wywołanie funkcji i (jeśli jest w DB) użyć wartości zwracanej dostarczonej przez kogoś po drugiej stronie świat, który lata temu używał identycznej funkcji w innym projekcie.


4

Jak napisano, jest to czysta funkcja. Nie powoduje żadnych skutków ubocznych. Funkcja ma jeden parametr formalny, ale ma dwa wejścia i zawsze będzie generować tę samą wartość dla dowolnych dwóch wejść.


2

Czy takie funkcje możemy nazwać funkcjami czystymi. Jeśli odpowiedź brzmi NIE, to jak możemy to zmienić na jedno?

Jak należycie zauważyłeś, „jutro może dać mi inne wyniki” . W takim przypadku odpowiedź brzmiałaby „nie” . Jest to szczególnie ważne, jeśli zamierzone zachowanie dollarToEurozostało poprawnie zinterpretowane jako:

const dollarToEuro = (x) => {
  const exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;
  return x * exchangeRate;
};

Istnieje jednak inna interpretacja, w której można ją uznać za czystą:

const dollarToEuro = ( () => {
    const exchangeRate =  fetchFromDatabase();

    return ( x ) => x * exchangeRate;
} )();

dollarToEuro bezpośrednio powyżej jest czyste.


Z punktu widzenia inżynierii oprogramowania konieczne jest zadeklarowanie zależności od dollarToEurofunkcji fetchFromDatabase. Dlatego refaktoryzuj definicję w dollarToEuronastępujący sposób:

const dollarToEuro = ( x, fetchFromDatabase ) => {
  return x * fetchFromDatabase();
};

Na podstawie tego wyniku, biorąc pod uwagę założenie, które fetchFromDatabasedziała w sposób zadowalający, możemy stwierdzić, że rzut fetchFromDatabasena dollarToEuromusi być zadowalający. Albo „ fetchFromDatabaseczysta” Oznacza dollarToEuroto czysta (ponieważ fetchFromDatabasejest to podstawa dla dollarToEuroprzez skalarne współczynnik x.

Z oryginalnego postu rozumiem, że fetchFromDatabasejest to czas funkcji. Poprawmy wysiłki związane z refaktoryzacją, aby to zrozumienie było przejrzyste, a zatem wyraźnie kwalifikuje się fetchFromDatabasejako czysta funkcja:

fetchFromDatabase = (timestamp) => {/ * tutaj idzie implementacja * /};

Ostatecznie przefakturowałbym tę funkcję w następujący sposób:

const fetchFromDatabase = ( timestamp ) => { /* here goes the implementation */ };

// Do a partial application of `fetchFromDatabase` 
const exchangeRate = fetchFromDatabase.bind( null, Date.now() );

const dollarToEuro = ( dollarAmount, exchangeRate ) => dollarAmount * exchangeRate();

W związku z tym dollarToEuromożna go przetestować jednostkowo, po prostu wykazując, że poprawnie wywołuje fetchFromDatabase(lub jego pochodną exchangeRate).


1
To było bardzo pouczające. +1. Dzięki.
Snowman

Chociaż uważam, że twoja odpowiedź jest bardziej pouczająca, a być może lepsze refaktoryzacja dla konkretnego przypadku użycia dollarToEuro; Wspomniałem w OP, że mogą istnieć inne przypadki użycia. Wybrałem dollarToEuro, ponieważ natychmiast przywołuje to, co próbuję zrobić, ale może być coś mniej subtelnego, co zależy od wolnej zmiennej, która może się zmienić, ale niekoniecznie jako funkcja czasu. Mając to na uwadze, uważam, że najlepiej oceniany refaktor jest tym bardziej dostępnym i tym, który może pomóc innym w podobnych przypadkach użycia. Dziękuję za pomoc, niezależnie od tego.
Snowman

-1

Jestem dwujęzycznym językiem Haskell / JS, a Haskell jest jednym z języków, który ma duży wpływ na czystość funkcji, więc pomyślałem, że dam wam perspektywę z tego, jak Haskell to widzi.

Jak mówili inni, w Haskell, czytając zmienny zmienną jest powszechnie uważany za nieczyste. Istnieje różnica między zmiennymi a definicjami, ponieważ zmienne mogą się później zmieniać, definicje są takie same na zawsze. Jeśli więc to zadeklarowałeś const(zakładając, że jest to tylko a numberi nie ma zmiennej struktury wewnętrznej), czytanie z tego byłoby oparte na definicji, która jest czysta. Ale chciałeś modelować kursy wymiany zmieniające się w czasie, a to wymaga pewnej zmienności, a potem wpadniesz w nieczystość.

Aby opisać tego rodzaju nieczyste rzeczy (możemy je nazwać „efektami”, a ich użycie jako „efektywne” w przeciwieństwie do „czystego”) w Haskell, robimy to, co można nazwać metaprogramowaniem . Dzisiaj metaprogramowanie zwykle odnosi się do makr, co nie mam na myśli, ale po prostu pomysł napisania programu do napisania innego programu w ogóle.

W tym przypadku w Haskell piszemy czyste obliczenia, które obliczają skuteczny program, który zrobi to, co chcemy. Tak więc cały punkt pliku źródłowego Haskell (przynajmniej taki, który opisuje program, a nie bibliotekę) polega na opisaniu czystego obliczenia efektywnego programu, który wywołuje void main. Następnie zadaniem kompilatora Haskell jest pobranie tego pliku źródłowego, wykonanie czystego obliczenia i umieszczenie tego efektywnego programu jako binarnego pliku wykonywalnego gdzieś na dysku twardym, aby można go było później uruchomić w wolnym czasie. Innymi słowy, istnieje przerwa między czasem, w którym wykonuje się czyste obliczenia (podczas gdy kompilator czyni plik wykonywalny), a czasem, w którym działa skuteczny program (za każdym razem, gdy uruchamiasz plik wykonywalny).

Tak więc dla nas skuteczne programy są tak naprawdę strukturą danych i same w sobie nie robią nic, po prostu wspomniane (nie mają * efektów ubocznych * oprócz ich wartości zwracanej; ich wartość zwrotna zawiera ich efekty). Dla bardzo lekkiego przykładu klasy TypeScript, która opisuje niezmienne programy i niektóre rzeczy, które możesz z nimi zrobić,

export class Program<x> {
   // wrapped function value
   constructor(public run: () => Promise<x>) {}
   // promotion of any value into a program which makes that value
   static of<v>(value: v): Program<v> {
     return new Program(() => Promise.resolve(value));
   }
   // applying any pure function to a program which makes its input
   map<y>(fn: (x: x) => y): Program<y> {
     return new Program(() => this.run().then(fn));
   }
   // sequencing two programs together
   chain<y>(after: (x: x) => Program<y>): Program<y> {
    return new Program(() => this.run().then(x => after(x).run()));
   }
}

Kluczem jest to, że jeśli nie masz Program<x>żadnych efektów ubocznych, to są to istoty całkowicie funkcjonalnie czyste. Mapowanie funkcji nad programem nie wywołuje żadnych skutków ubocznych, chyba że funkcja nie była funkcją czystą; sekwencjonowanie dwóch programów nie wywołuje żadnych skutków ubocznych; itp.

Na przykład, jak zastosować to w twoim przypadku, możesz napisać kilka czystych funkcji, które zwracają programy, aby uzyskać użytkowników według ID i zmienić bazę danych i pobrać dane JSON, takie jak

// assuming a database library in knex, say
function getUserById(id: number): Program<{ id: number, name: string, supervisor_id: number }> {
    return new Program(() => knex.select('*').from('users').where({ id }));
}
function notifyUserById(id: number, message: string): Program<void> {
    return new Program(() => knex('messages').insert({ user_id: id, type: 'notification', message }));
}
function fetchJSON(url: string): Program<any> {
  return new Program(() => fetch(url).then(response => response.json()));
}

a następnie możesz opisać zadanie crona, aby zwinąć adres URL i wyszukać jakiegoś pracownika i powiadomić jego przełożonego w czysto funkcjonalny sposób, jak

const action =
  fetchJSON('http://myapi.example.com/employee-of-the-month')
    .chain(eotmInfo => getUserById(eotmInfo.id))
    .chain(employee => 
        getUserById(employee.supervisor_id)
          .chain(supervisor => notifyUserById(
            supervisor.id,
            'Your subordinate ' + employee.name + ' is employee of the month!'
          ))
    );

Chodzi o to, że każda funkcja tutaj jest funkcją całkowicie czystą; tak naprawdę nic się nie wydarzyło, dopóki nie action.run()uruchomiłem go. Ponadto mogę pisać funkcje, takie jak,

// do two things in parallel
function parallel<x, y>(x: Program<x>, y: Program<y>): Program<[x, y]> {
    return new Program(() => Promise.all([x.run(), y.run()]));
}

a gdyby JS miał anulować obietnicę, moglibyśmy ścigać się w dwóch programach i wziąć pierwszy wynik i anulować drugi. (Mam na myśli, że nadal możemy, ale staje się mniej jasne, co robić).

Podobnie w twoim przypadku możemy opisać zmiany kursów walut

declare const exchangeRate: Program<number>;

function dollarsToEuros(dollars: number): Program<number> {
  return exchangeRate.map(rate => dollars * rate);
}

i exchangeRatemoże być programem, który patrzy na zmienną wartość,

let privateExchangeRate: number = 0;
export function setExchangeRate(value: number): Program<void> {
  return new Program(() => { privateExchangeRate = value; return Promise.resolve(undefined); });
}
export const exchangeRate: Program<number> = new Program(() => {
  return Promise.resolve(privateExchangeRate); 
});

ale mimo to funkcja ta dollarsToEurosjest teraz funkcją czystą od liczby do programu, który generuje liczbę, i można o tym wnioskować w ten deterministyczny sposób równań, który można argumentować o dowolnym programie, który nie ma skutków ubocznych.

Koszt oczywiście polega na tym, że w końcu trzeba to .run() gdzieś zadzwonić , a to będzie nieczyste. Ale całą strukturę twojego obliczenia można opisać czystym obliczeniem i możesz przesunąć nieczystości na margines swojego kodu.


Jestem ciekawy, dlaczego to wciąż jest przegłosowane, ale mam na myśli, że nadal jestem przy nim (tak naprawdę manipulujesz programami w Haskell, w których domyślnie wszystko jest czyste) i chętnie przejmie opinie negatywne. Mimo to, jeśli downvoterzy chcieliby zostawić komentarze wyjaśniające, co im się nie podoba, mogę spróbować to poprawić.
CR Drost

Tak, zastanawiałem się, dlaczego jest tak wiele głosów negatywnych, ale ani jednego komentarza, oczywiście oprócz autora.
Buda Örs
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.