Jak nazywacie funkcję, w której to samo wejście zawsze zwraca tę samą moc wyjściową, ale ma również skutki uboczne?


43

Powiedzmy, że mamy normalną czystą funkcję, taką jak

function add(a, b) {
  return a + b
}

A następnie zmieniamy go tak, aby miał efekt uboczny

function add(a, b) {
  writeToDatabase(Math.random())
  return a + b;
}

O ile wiem, nie jest to uważane za funkcję czystą, ponieważ często słyszę, jak ludzie nazywają funkcje czyste „funkcjami bez skutków ubocznych”. Zachowuje się jednak jak czysta funkcja, o ile zwraca te same dane wyjściowe dla tych samych danych wejściowych.

Czy istnieje inna nazwa dla tego typu funkcji, jest nienazwana, czy nadal jest rzeczywiście czysta i mylę się co do definicji czystości?


91
„Nie jest to czysta funkcja”.
Ross Patterson

2
@RossPatterson też tak myślałem, ale pytając dowiedziałem się o przejrzystości referencyjnej, więc cieszę się, że nie zatrzymałem tego dla siebie.
m0meni

9
Jeśli się writeToDatabasenie powiedzie, może wywołać wyjątek, powodując, że twoja druga addfunkcja generuje wyjątek, nawet jeśli wywoływana jest z tymi samymi argumentami, które wcześniej nie miały problemów ... przez większość czasu skutki uboczne wprowadzają tego rodzaju warunki związane z błędami, które się psują „czystość wejścia / wyjścia”.
Bakuriu

25
Coś, co zawsze daje taki sam wynik dla danego wejścia, nazywa się deterministyczne .
njzk2

2
@ njzk2: Prawda, ale jest także bezstanowa . Stateful deterministyczny funkcja może nie dać taki sam efekt dla każdego wejścia. Przykład: F(x)zdefiniowano, aby truezwracał, jeśli jest wywoływany z tym samym argumentem, co poprzednie wywołanie. Oczywiście w sekwencji {1,2,2} => {undefined, false, true}jest to deterministyczne, ale daje różne wyniki F(2).
MSalters

Odpowiedzi:


85

Nie jestem pewien co do uniwersalnych definicji czystości, ale z punktu widzenia Haskell (języka, w którym programiści dbają o takie rzeczy, jak czystość i przejrzystość referencyjna), tylko pierwsza z twoich funkcji jest „czysta”. Druga wersja addnie jest czysta . W odpowiedzi na twoje pytanie nazwałbym to „nieczystym”;)

Zgodnie z tą definicją funkcja czysta jest funkcją, która:

  1. Zależy tylko od jego wkładu. Oznacza to, że przy takim samym wejściu zawsze zwróci to samo wyjście.
  2. Jest referencyjnie przejrzysty: funkcję można dowolnie zastąpić jej wartością, a „zachowanie” programu się nie zmieni.

Przy tej definicji jasne jest, że drugiej funkcji nie można uznać za czystą, ponieważ łamie ona zasadę 2. Oznacza to, że następujące dwa programy NIE są równoważne:

function f(a, b) { 
    return add(a, b) + add(a, b);
}

i

function g(a, b) {
    c = add(a, b);
    return c + c;
}

Wynika to z faktu, że chociaż obie funkcje zwrócą tę samą wartość, funkcja fzapisze dwa razy w bazie danych, ale gzapisze raz! Jest bardzo prawdopodobne, że zapisy do bazy danych są częścią obserwowalnego zachowania twojego programu, w którym to przypadku pokazałem, że twoja druga wersja addnie jest „czysta”.

Jeśli zapisy do bazy danych nie są obserwowalną częścią zachowania twojego programu, wówczas obie wersje addmożna uznać za równoważne i czyste. Ale nie mogę wymyślić scenariusza, w którym zapis do bazy danych nie ma znaczenia. Nawet logowanie się ma znaczenie!


1
Czy „nie zależy tylko od danych wejściowych” nadmiarowych, biorąc pod uwagę przejrzystość referencyjną? Co oznaczałoby, że RT jest synonimem czystości? (Jestem coraz bardziej zdezorientowany tym więcej źródeł szukam)
Ixrec 30.04.16

Zgadzam się, że to mylące. Mogę tylko wymyślić wymyślone przykłady. Powiedz f(x)zależy nie tylko od x, ale także od zewnętrznej zmiennej globalnej y. Następnie, jeśli fma właściwość RT, możesz dowolnie zamieniać wszystkie jej wystąpienia wartością zwracaną, dopóki nie dotkniesz y . Tak, mój przykład jest wątpliwy. Ale ważne jest to: jeśli fzapisuje do bazy danych (lub zapisuje w dzienniku), traci właściwość RT: teraz nie ma znaczenia, czy pozostawisz globalny ynietknięty, wiesz, jakie znaczenie ma twój program, w zależności od tego, czy faktycznie zadzwoń flub po prostu użyj jego wartości zwracanej.
Andres F.,

Humph. Powiedzmy, że mamy taką funkcję, która jest czysta, z wyjątkiem skutków ubocznych, i gwarantujemy również takie zachowanie, gdy dwie próbki są równoważne. (Miałem właśnie tę sprawę, więc nie jest hipotetyczna.) Myślę, że jeszcze nie skończyliśmy.
Jozuego

2
Twierdziłbym, że druga funkcja może również złamać zasadę nr 1. W zależności od języka i praktyk obsługi błędów w używanym interfejsie API bazy danych funkcja może w ogóle nie zwracać niczego, jeśli baza danych jest niedostępna lub z jakiegoś powodu zapis db nie powiedzie się.
Zach Lipton

1
Odkąd wspomniano o Haskell: w Haskell dodanie takiego efektu ubocznego wymaga zmiany podpisu funkcji . (pomyśl o tym jak o przekazaniu oryginalnej bazy danych jako dodatkowego wejścia i uzyskaniu zmodyfikowanej bazy danych jako dodatkowej wartości zwracanej przez funkcję). W ten sposób możliwe jest dość eleganckie modelowanie efektów ubocznych w systemie pisma, po prostu współczesne języki głównego nurtu nie dbają wystarczająco o efekty uboczne i czystość, aby to zrobić.
ComicSansMS

19

Jak nazywasz funkcję [dla której] to samo wejście zawsze zwróci to samo wyjście, ale ma również skutki uboczne?

Taka funkcja jest nazywana

deterministyczny

Algorytm, którego zachowanie można całkowicie przewidzieć na podstawie danych wejściowych.

termwiki.com

W odniesieniu do stanu:

W zależności od tego, z której definicji funkcji korzystasz, funkcja nie ma stanu. Jeśli pochodzisz ze świata obiektowego, pamiętaj, że x.f(y)jest to metoda. W funkcji mogłoby to wyglądać f(x,y). A jeśli jesteś w zamknięciach z zamkniętym zakresem leksykalnym, pamiętaj, że stan niezmienny może równie dobrze być częścią wyrażenia funkcji. Jest to tylko stan zmienny, który miałby wpływ na charakter deterministyczny funkcji. Zatem f (x) = x + 1 jest deterministyczne, o ile 1 się nie zmienia. Nie ma znaczenia, gdzie 1 jest przechowywany.

Twoje funkcje są deterministyczne. Twoja pierwsza jest również czystą funkcją. Twoja sekunda nie jest czysta.

Czysta funkcja

  1. Funkcja zawsze ocenia tę samą wartość wyniku, biorąc pod uwagę te same wartości argumentu. Wartość wyniku funkcji nie może zależeć od żadnych ukrytych informacji lub stanów, które mogą ulec zmianie w trakcie wykonywania programu lub między różnymi wykonaniami programu, ani też nie może zależeć od jakichkolwiek zewnętrznych danych wejściowych z urządzeń I / O.

  2. Ocena wyniku nie powoduje żadnych semantycznie obserwowalnych skutków ubocznych lub wyników, takich jak mutacja obiektów podlegających mutacji lub wyjście do urządzeń I / O.

wikipedia.org

Punkt 1 oznacza deterministyczny . Punkt 2 oznacza przejrzystość referencyjną . Razem oznaczają, że czysta funkcja pozwala tylko na zmianę argumentów i zwracanej wartości. Nic innego nie powoduje zmiany. Nic innego się nie zmienia.


-1. Zapis do bazy danych zależy od stanu zewnętrznego, którego na ogół nie można ustalić na podstawie danych wejściowych. Baza danych może być niedostępna z wielu powodów i nie można przewidzieć, czy operacja się powiedzie. To nie jest zachowanie deterministyczne.
Frax,

@Frax Pamięć systemowa może być niedostępna. Procesor może być niedostępny. Bycie deterministycznym nie gwarantuje sukcesu. Gwarantuje to, że udane zachowanie jest przewidywalne.
candied_orange

OOMing nie jest specyficzny dla żadnej funkcji, jest to inna kategoria problemu. Spójrzmy teraz na punkt 1. definicji „czystej funkcji” (co tak naprawdę oznacza „deterministyczny”): „Wartość wyniku funkcji nie może zależeć od żadnych ukrytych informacji lub stanów, które mogą ulec zmianie w trakcie wykonywania programu lub między różnymi wykonaniami program , nie może też zależeć od żadnego zewnętrznego wejścia z urządzeń I / O ". Baza danych jest tego rodzaju stanem, więc funkcja PO wyraźnie nie spełnia tego warunku - nie jest deterministyczna.
Frax

@candied_orange Zgadzam się, jeśli zapis do DB byłby zależny tylko od danych wejściowych. Ale to jest Math.random(). Więc nie, chyba że założymy PRNG (zamiast fizycznego RNG) ORAZ nie weźmiemy pod uwagę, że PRNG stanowią część danych wejściowych (co nie jest, odniesienie jest zakodowane na stałe), nie jest deterministyczne.
marstato,

1
@candied_orange twoje cytowanie stanów deterministycznych „algorytm, którego zachowanie można całkowicie przewidzieć na podstawie danych wejściowych”. Pisanie do IO jest dla mnie zdecydowanie zachowaniem, a nie rezultatem.
marstato,

9

Jeśli nie obchodzi Cię efekt uboczny, jest on względnie przezroczysty. Oczywiście możliwe jest, że Cię to nie obchodzi, ale robi to ktoś inny, więc zastosowanie tego terminu zależy od kontekstu.

Nie znam ogólnego terminu określającego dokładnie opisywane właściwości, ale ważnym podzbiorem są te, które są idempotentne . W informatyce, nieco inaczej niż w matematyce *, funkcja idempotentna to taka, którą można powtórzyć z tym samym skutkiem; to znaczy, że efekt uboczny nett robienia tego wiele razy jest taki sam jak robienie tego raz.

Tak więc, jeśli efektem ubocznym było zaktualizowanie bazy danych o określonej wartości w określonym wierszu lub utworzenie pliku o dokładnie spójnej zawartości, byłoby to idempotentne , ale gdyby zostało dodane do bazy danych lub dołączone do pliku , to nie byłoby.

Kombinacje idempotentnych funkcji mogą, ale nie muszą, być idempotentnymi jako całością.

* Wydaje się, że użycie idempotenta w informatyce inaczej niż w matematyce wynika z niewłaściwego użycia matematycznego terminu, który został następnie przyjęty, ponieważ ta koncepcja jest przydatna.


3
termin „referencyjnie przejrzysty” jest zdefiniowany bardziej rygorystycznie niż to, czy „kogokolwiek to obchodzi”. nawet jeśli pominiemy kwestie IO, takie jak problemy z połączeniem, brakujących ciągów połączeń, limity czasu itp, to wciąż łatwo wykazać, że program, który zastępuje (f x, f x)ze let y = f x in (y, y)będzie napotkasz z Disk Space-wyjątkami dwukrotnie szybciej można argumentować, że są to na marginesie przypadków, na których ci nie zależy, ale przy tak niewyraźnej definicji równie dobrze moglibyśmy nazwać new Random().Next()referencyjnie przezroczystymi, bo do cholery, nie dbam o to, jaki numer i tak otrzymam.
sara

@ kai: W zależności od kontekstu możesz zignorować skutki uboczne. Z drugiej strony, zwracana wartość funkcji takiej jak losowa nie jest efektem ubocznym: jest to jej główny efekt.
Giorgio

@Giorgio Random.Nextw .NET rzeczywiście ma skutki uboczne. Bardzo tak. Jeśli możesz Next, przypisz ją do zmiennej, a następnie wywołaj Nextponownie i przypisz do innej zmiennej, prawdopodobnie nie będą one równe. Dlaczego? Ponieważ wywołanie Nextzmienia ukryty stan wewnętrzny w Randomobiekcie. Jest to biegunowe przeciwieństwo przezroczystości referencyjnej. Nie rozumiem twojego twierdzenia, że ​​„główne efekty” nie mogą być skutkami ubocznymi. W kodzie imperatywnym bardziej powszechne jest to, że głównym efektem jest efekt uboczny, ponieważ programy imperatywne są z natury stanowe.
sara

3

Nie wiem, jak wywoływane są takie funkcje (ani czy istnieje jakaś systematyczna nazwa), ale nazwałbym funkcję, która nie jest czysta (jak w przypadku innych odpowiedzi), ale zawsze zwraca ten sam wynik, jeśli jest dostarczana z tymi samymi parametrami parametry ”(w porównaniu do funkcji jego parametrów i niektórych innych stanów). Nazwałbym to po prostu funkcją, ale niestety, kiedy mówimy „funkcja” w kontekście programowania, mamy na myśli coś, co wcale nie musi być rzeczywistą funkcją.


1
Zgoda! Jest to (nieformalnie) matematyczna definicja „funkcji”, ale, jak mówisz, niestety „funkcja” oznacza coś innego w językach programowania, gdzie jest ona bliższa „krok po kroku wymaganej do uzyskania wartości”.
Andres F.

2

Zasadniczo zależy to od tego, czy zależy ci na nieczystości. Jeśli semantyką tej tabeli jest to, że nie obchodzi Cię, ile jest wpisów, to jest czysta. W przeciwnym razie to nie jest czyste.

Innymi słowy, jest w porządku, o ile optymalizacje oparte na czystości nie naruszają semantyki programu.

Bardziej realistycznym przykładem może być próba debugowania tej funkcji i dodania instrukcji rejestrowania. Technicznie rejestrowanie jest efektem ubocznym. Czy dzienniki czynią to nieczystym? Nie.


Cóż, to zależy. Być może dzienniki sprawiają, że jest nieczyste, na przykład, jeśli zależy Ci na tym, ile razy i kiedy „INFO f () wywoływane” pojawia się w twoim dzienniku. Co często robisz :)
Andres F.,

8
-1 Logi mają znaczenie. Na większości platform dane wyjściowe dowolnego rodzaju domyślnie synchronizują wątek wykonania. Zachowanie się programu zależy od innych zapisów wątków, zewnętrznych rejestratorów dziennika, czasami odczytywania dziennika, stanu deskryptorów plików. Jest czysty jak wiadro brudu.
Basilevs

@AndresF. Cóż, prawdopodobnie nie obchodzi Cię dosłowna liczba razy. Prawdopodobnie zależy Ci tylko na tym, że jest on rejestrowany tyle razy, ile razy wywołano tę funkcję.
DeadMG

@Basilevs Zachowanie funkcji w ogóle nie zależy od nich. Jeśli zapis dziennika nie powiedzie się, po prostu kontynuuj.
DeadMG

2
Jest kwestią tego, czy zdecydujesz się zdefiniować program rejestrujący, aby był częścią środowiska wykonawczego, czy nie. Na przykład: czy moja czysta funkcja jest nadal czysta, jeśli dołączę debuger do procesu i ustawię dla niego punkt przerwania? Z POV osoby korzystającej z debuggera wyraźnie widać, że funkcja ta ma skutki uboczne, ale zwykle analizujemy program z taką konwencją, że to „się nie liczy”. To samo może (ale nie musi) dotyczyć rejestrowania używanego do debugowania, co, jak przypuszczam, powoduje, że trace ukrywa swoją nieczystość. Rejestrowanie krytyczne dla misji, np. Do audytu, jest oczywiście znaczącym efektem ubocznym.
Steve Jessop

1

Powiedziałbym, że najlepszą rzeczą, o którą należy zapytać, nie jest to, jak to nazwiemy, ale jak przeanalizujemy taki fragment kodu. A moim pierwszym kluczowym pytaniem w takiej analizie byłoby:

  • Czy efekt uboczny zależy od argumentu funkcji lub wyniku od efektu ubocznego?
    • Nie: „Funkcjonalną funkcję” można przekształcić w czystą funkcję, skuteczne działanie i mechanizm ich łączenia.
    • Tak: „skuteczna funkcja” to funkcja, która daje wynik monadyczny.

Można to łatwo zilustrować w Haskell (a zdanie to tylko pół żartu). Przykładem przypadku „nie” może być coś takiego:

double :: Num a => a -> IO a
double x = do
  putStrLn "I'm doubling some number"
  return (x*2)

W tym przykładzie działanie, które podejmujemy (drukujemy linię "I'm doubling some number") nie ma wpływu na związek między xi wynikiem. Oznacza to, że możemy to zmienić w ten sposób (używając Applicativeklasy i jej *>operatora), co pokazuje, że funkcja i efekt są w rzeczywistości ortogonalne:

double :: Num a => a -> IO a
double x = action *> pure (function x)
  where
    -- The pure function 
    function x = x*2  
    -- The side effect
    action = putStrLn "I'm doubling some number"

Tak więc w tym przypadku osobiście powiedziałbym, że jest to przypadek, w którym można wyróżnić czystą funkcję. Dużo o tym mówi programowanie Haskell - uczenie się, jak oddzielić czyste części od efektywnego kodu.

Przykład rodzaju „tak”, w którym czyste i skuteczne części nie są ortogonalne:

double :: Num a => a -> IO a
double x = do
  putStrLn ("I'm doubling the number " ++ show x)
  return (x*2)

Teraz ciąg, który drukujesz, zależy od wartości x. Część funkcji (pomnóż xprzez dwa) nie zależy jednak w ogóle od efektu, więc nadal możemy go rozdzielić:

logged :: (a -> b) -> (a -> IO x) -> IO b
logged function logger a = do
  logger a
  return (function a)

double x = logged function logger
  where function = (*2) 
        logger x putStrLn ("I'm doubling the number " ++ show x)

Mógłbym dalej wymieniać inne przykłady, ale mam nadzieję, że to wystarczy, aby zilustrować punkt, od którego zacząłem: nie „nazywasz” tego czymś, analizujesz, w jaki sposób łączą się czyste i skuteczne części, i rozróżniasz je, kiedy jest na twoją korzyść.

Jest to jeden z powodów, dla których Haskell Monadtak często wykorzystuje swoją klasę. Monady są (między innymi) narzędziem do przeprowadzania tego rodzaju analiz i refaktoryzacji.


-2

Funkcje, które są przeznaczone do wywoływania skutków ubocznych są często nazywane effectful . Przykład https://slpopejoy.github.io/posts/Effectful01.html


Wystarczy odpowiedzieć, by wspomnieć o powszechnie uznawanym słowie skutecznym i przegłosowanym ... Niewiedza to chyba błogość. ..
Ben Hutchison

„skuteczne” to słowo, które autor tego postu zdecydował się na „mające skutki uboczne”. Sam tak mówi.
Robert Harvey

Skuteczna funkcja Google ujawnia wiele dowodów na to, że jest to powszechnie używany termin. Wpis na blogu podano jako jeden z wielu przykładów, a nie jako definicję. W kręgach programowania funkcjonalnego, w których domyślnymi funkcjami są czyste funkcje, potrzebny jest pozytywny termin opisujący celowo funkcje uboczne. Tj. Więcej niż brak czystości . Skuteczny jest ten termin. Teraz uważaj się za wykształconego.
Ben Hutchison

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.