Come si chiama una funzione in cui lo stesso input restituirà sempre lo stesso output, ma ha anche effetti collaterali?


43

Supponiamo che abbiamo una normale funzione pura come

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

E poi lo alteriamo in modo tale che abbia un effetto collaterale

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

Per quanto ne so, non è considerata una funzione pura perché sento spesso che le persone chiamano funzioni pure "funzioni senza effetti collaterali". Tuttavia, si comporta come una funzione pura per il fatto che restituirà lo stesso output per gli stessi input.

Esiste un nome diverso per questo tipo di funzione, è senza nome o è ancora in realtà puro e mi sbaglio sulla definizione di purezza?


91
"Non una pura funzione".
Ross Patterson,

2
@RossPatterson è quello che ho pensato anch'io, ma chiedendo di aver appreso la trasparenza referenziale, quindi sono contento di non averlo tenuto per me.
m0meni,

9
Se writeToDatabasefallisce, potrebbe innescare un'eccezione, facendo sì che la tua seconda addfunzione produca un'eccezione a volte anche se chiamata con gli stessi argomenti che prima non avevano problemi ... il più delle volte avere effetti collaterali introduce questo tipo di condizioni legate all'errore che si rompono "purezza input-output".
Bakuriu,

25
Qualcosa che dà sempre lo stesso output per un dato input è chiamato deterministico .
njzk2,

2
@ njzk2: vero, ma è anche apolide . Una funzione deterministica con stato potrebbe non fornire lo stesso output per ogni input. Esempio: F(x)è definito per il ritorno truese si chiama con lo stesso argomento come chiamata precedente. Chiaramente con la sequenza {1,2,2} => {undefined, false, true}questo è deterministico, ma dà risultati diversi per F(2).
MSalters

Risposte:


85

Non sono sicuro delle definizioni universali di purezza, ma dal punto di vista di Haskell (un linguaggio in cui i programmatori tendono a preoccuparsi di cose come la purezza e la trasparenza referenziale), solo la prima delle tue funzioni è "pura". La seconda versione di addnon è pura . Quindi, in risposta alla tua domanda, lo definirei "impuro";)

Secondo questa definizione, una funzione pura è una funzione che:

  1. Dipende solo dal suo input. Cioè, dato lo stesso input, restituirà sempre lo stesso output.
  2. È referenzialmente trasparente: la funzione può essere liberamente sostituita dal suo valore e il "comportamento" del programma non cambierà.

Con questa definizione, è chiaro che la tua seconda funzione non può essere considerata pura, poiché infrange la regola 2. Cioè, i seguenti due programmi NON sono equivalenti:

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

e

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

Questo perché anche se entrambe le funzioni restituiranno lo stesso valore, la funzione fscriverà due volte nel database ma gscriverà una volta! È molto probabile che le scritture nel database facciano parte del comportamento osservabile del tuo programma, nel qual caso ho mostrato che la tua seconda versione di addnon è "pura".

Se le scritture nel database non sono una parte osservabile del comportamento del tuo programma, entrambe le versioni addpossono essere considerate equivalenti e pure. Ma non riesco a pensare a uno scenario in cui la scrittura nel database non ha importanza. Anche la registrazione conta!


1
"Dipende solo dal suo input" non è ridondante data la trasparenza referenziale? Cosa implicherebbe RT è sinonimo di purezza? (Sto diventando più confuso su questo più fonti guardo)
Ixrec

Sono d'accordo che è confuso. Posso solo pensare ad esempi inventati. Dire f(x)non dipende solo da x, ma anche da alcune variabili globali esterne y. Quindi, se fha la proprietà di RT, puoi scambiare liberamente tutte le sue occorrenze con il suo valore di ritorno purché non tocchi y . Sì, il mio esempio è dubbio. Ma la cosa importante è: se fscrive nel database (o scrive in un registro) perde la proprietà di RT: ora non importa se non lasci nulla di globale y, sai che il significato del tuo programma cambia a seconda che tu effettivamente chiama fo usa semplicemente il suo valore di ritorno.
Andres F.

Bah. Diciamo che abbiamo una tale funzione che è pura ad eccezione degli effetti collaterali ed è anche garantito che abbia un tale comportamento in cui i tuoi due campioni sono equivalenti. (Ho avuto questo caso in effetti, quindi non è ipotetico.) Penso che non abbiamo ancora finito.
Giosuè,

2
Direi che anche la seconda funzione potrebbe infrangere la regola n. 1. A seconda della lingua e delle pratiche di gestione degli errori dell'API del database in uso, la funzione potrebbe non restituire nulla se il database non è disponibile o la scrittura del database non riesce per qualche motivo.
Zach Lipton,

1
Da quando è stato citato Haskell: in Haskell l'aggiunta di un effetto collaterale del genere richiede la modifica della firma della funzione . (pensalo come dare al database originale come input aggiuntivo e ottenere il database modificato come valore di ritorno aggiuntivo della funzione). In realtà è possibile modellare gli effetti collaterali in modo abbastanza elegante nel sistema di tipi in quel modo, è solo che i linguaggi tradizionali di oggi non si preoccupano abbastanza degli effetti collaterali e della purezza per farlo.
ComicSansMS,

19

Come si chiama una funzione [per la quale] lo stesso input restituirà sempre lo stesso output, ma ha anche effetti collaterali?

Tale funzione è chiamata

deterministico

Un algoritmo il cui comportamento può essere completamente previsto dall'input.

termwiki.com

Per quanto riguarda lo stato:

A seconda della definizione di funzione utilizzata, una funzione non ha stato. Se vieni dal mondo orientato agli oggetti, ricorda che x.f(y)è un metodo. Come funzione sembrerebbe f(x,y). E se ti piacciono le chiusure con ambito lessicale chiuso, ricorda che lo stato immutabile potrebbe anche far parte dell'espressione delle funzioni. È solo uno stato mutabile che avrebbe un impatto sulla natura deterministica delle funzioni. Quindi f (x) = x + 1 è deterministico fintanto che l'1 non cambia. Non importa dove è memorizzato l'1.

Le tue funzioni sono entrambe deterministiche. Il tuo primo è anche una funzione pura. Il tuo secondo non è puro.

Funzione pura

  1. La funzione valuta sempre lo stesso valore di risultato dati gli stessi valori di argomento. Il valore del risultato della funzione non può dipendere da alcuna informazione o stato nascosto che può cambiare mentre procede l'esecuzione del programma o tra diverse esecuzioni del programma, né può dipendere da alcun input esterno dai dispositivi I / O.

  2. La valutazione del risultato non provoca alcun effetto collaterale o output osservabile semanticamente, come la mutazione di oggetti mutabili o l'output a dispositivi I / O.

wikipedia.org

Il punto 1 significa deterministico . Il punto 2 significa trasparenza referenziale . Insieme significano che una funzione pura consente solo la modifica dei suoi argomenti e del valore restituito. Nient'altro causa il cambiamento. Nient'altro è cambiato.


-1. La scrittura nel database dipende dallo stato esterno che generalmente non può essere determinato osservando gli input. Il database potrebbe non essere disponibile per una serie di motivi e se l'operazione avrà esito positivo non è prevedibile. Questo non è un comportamento deterministico.
Frax,

@Frax La memoria di sistema potrebbe non essere disponibile. La CPU potrebbe non essere disponibile. Essere deterministici non garantisce il successo. Garantisce che il comportamento di successo sia prevedibile.
candied_orange

OOM non è specifico per nessuna funzione, è diversa categoria di problema. Ora diamo un'occhiata al punto 1. della definizione di "funzione pura" (che in effetti significa "deterministico"): "Il valore del risultato della funzione non può dipendere da informazioni o stati nascosti che possono cambiare mentre procede l'esecuzione del programma o tra diverse esecuzioni di il programma , né può dipendere da alcun input esterno dai dispositivi I / O ". Il database è quel tipo di stato, quindi la funzione dei PO chiaramente non soddisfa questa condizione - non è deterministica.
Frax,

@candied_orange Concordo sul fatto che la scrittura nel DB dipendesse solo dagli input. Ma lo è Math.random(). Quindi no, a meno che non assumiamo un PRNG (invece di un RNG fisico) E consideriamo che i PRNG dichiarano parte dell'input (che non è, il riferimento è codificato), non è deterministico.
Marstato,

1
@candied_orange la tua citazione di stati deterministici "un algoritmo il cui comportamento può essere completamente previsto dall'input". Scrivere su IO, per me, è sicuramente un comportamento, non un risultato.
Marstato,

9

Se non ti interessa l'effetto collaterale, allora è referenzialmente trasparente. Naturalmente è possibile che non ti interessi, ma lo fa qualcun altro, quindi l'applicabilità del termine dipende dal contesto.

Non conosco un termine generale proprio per le proprietà che descrivi, ma un sottoinsieme importante sono quelli che sono idempotenti . Nell'informatica, leggermente diversa dalla matematica *, una funzione idempotente può essere ripetuta con lo stesso effetto; vale a dire il risultato netto di effetti collaterali di farlo molte volte è lo stesso di farlo una volta.

Quindi, se il tuo effetto collaterale fosse aggiornare un database con un certo valore in una determinata riga o creare un file con contenuti esattamente coerenti, sarebbe idempotente , ma se aggiunto al database o aggiunto a un file , quindi non lo sarebbe.

Le combinazioni di funzioni idempotenti possono o meno essere idempotenti nel loro insieme.

* L'uso dell'idempotente in modo diverso nell'informatica rispetto alla matematica sembra derivare da un uso errato del termine matematico che è stato poi adottato perché il concetto è utile.


3
il termine "referenzialmente trasparente" è più rigorosamente definito se "a chiunque importa". anche se si prescinde problemi IO come ad esempio problemi di connessione, le stringhe di connessione mancanti, timeout, ecc, quindi è ancora facile dimostrare che un programma che sostituisce (f x, f x)con let y = f x in (y, y)verrà eseguito in fuori dello spazio-eccezioni disco due volte più veloce si potrebbe sostenere che questi sono casi marginali che non ti interessano, ma con una definizione così confusa potremmo anche chiamare new Random().Next()referenzialmente trasparente perché diamine, non mi interessa comunque quale numero ottengo.
Sara,

@kai: a seconda del contesto, è possibile ignorare gli effetti collaterali. D'altra parte, il valore di ritorno di una funzione come casuale non è un effetto collaterale: è il suo effetto principale.
Giorgio,

@Giorgio Random.Nextin .NET ha davvero effetti collaterali. Così tanto. Se è possibile Next, assegnarlo a una variabile, quindi richiamare Nextnuovamente e assegnarlo a un'altra variabile, è probabile che non siano uguali. Perché? Perché invocare Nextcambia uno stato interno nascosto Randomnell'oggetto. Questo è l'opposto polare della trasparenza referenziale. Non capisco la tua affermazione che gli "effetti principali" non possono essere effetti collaterali. Nel codice imperativo è più comune che no che l'effetto principale sia un effetto collaterale, perché i programmi imperativi sono statali per natura.
Sara

3

Non so come si chiamino tali funzioni (o se esiste anche un nome sistematico), ma chiamerei la funzione che non è pura (come altre risposte nascoste) ma restituisce sempre lo stesso risultato se fornita con gli stessi parametri "funzione della sua parametri "(rispetto alla funzione dei suoi parametri e qualche altro stato). La chiamerei solo funzione, ma sfortunatamente quando diciamo "funzione" nel contesto della programmazione, intendiamo qualcosa che non deve essere affatto una funzione reale.


1
Concordato! È (informalmente) la definizione matematica di "funzione", ma come dici tu, sfortunatamente "funzione" significa qualcosa di diverso nei linguaggi di programmazione, dove è più vicino a "una procedura passo-passo richiesta per ottenere un valore".
Andres F.

2

Dipende fondamentalmente dal fatto che ti preoccupi o meno dell'impurità. Se la semantica di questa tabella è che non ti importa quante voci ci sono, allora è puro. Altrimenti, non è puro.

O, per dirla in altro modo, va bene purché le ottimizzazioni basate sulla purezza non rompano la semantica del programma.

Un esempio più realistico sarebbe se si cercasse di eseguire il debug di questa funzione e di aggiungere istruzioni di registrazione. Tecnicamente, la registrazione è un effetto collaterale. I registri lo rendono impuro? No.


Beh, dipende. Forse i registri lo rendono impuro, ad esempio se ti preoccupi di quante volte e in quali orari, "INFO f () chiamato" appare nel tuo registro. Cosa che fai spesso :)
Andres F.

8
-1 I log sono importanti. Sulla maggior parte delle piattaforme l'output di qualsiasi tipo sincronizza implicitamente il thread di esecuzione. Il comportamento del programma dipende da altre scritture di thread, da scrittori di log esterni, a volte da letture di log, dallo stato dei descrittori di file. È puro come un secchio di terra.
Basilevs

@AndresF. Bene, probabilmente non ti interessa il numero letterale di volte. Probabilmente ti interessa solo che sia registrato tutte le volte che è stata chiamata la funzione.
DeadMG

@Basilevs Il comportamento della funzione non dipende affatto da loro. Se la scrittura del registro fallisce, vai avanti.
DeadMG

2
Si tratta di scegliere se definire o meno il logger come parte dell'ambiente di esecuzione. Per un altro esempio, la mia pura funzione è ancora pura se allego un debugger al processo e imposto un breakpoint su di esso? Dal punto di vista della persona che utilizza il debugger, chiaramente la funzione ha effetti collaterali, ma normalmente analizziamo un programma con la convenzione che questo "non conta". Lo stesso può (ma non è necessario) utilizzare la registrazione utilizzata per il debug, che presumo sia il motivo per cui trace sta nascondendo la sua impurità. Importanza critica registrazione, ad esempio per revisione, naturalmente è un significativo effetto collaterale.
Steve Jessop,

1

Direi che la cosa migliore da chiedere non è come la chiameremmo, ma come analizzeremmo un simile codice. E la mia prima domanda chiave in tale analisi sarebbe:

  • L'effetto collaterale dipende dall'argomento della funzione o dal risultato dall'effetto collaterale?
    • No: la "funzione efficace" può essere trasformata in una funzione pura, un'azione efficace e un meccanismo per combinarle.
    • Sì: la "funzione efficace" è una funzione che produce un risultato monadico.

Questo è semplice da illustrare in Haskell (e questa frase è solo una mezza battuta). Un esempio del caso "no" sarebbe qualcosa del genere:

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

In questo esempio l'azione che eseguiamo (stampa la linea "I'm doubling some number") non ha alcun impatto sulla relazione tra xe il risultato. Questo significa che possiamo refactoring in questo modo (usando la Applicativeclasse e il suo *>operatore), il che dimostra che la funzione e l'effetto sono in effetti ortogonali:

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"

Quindi in questo caso, personalmente, direi che è un caso in cui è possibile calcolare una funzione pura. Molta programmazione di Haskell riguarda questo aspetto: imparare a separare le parti pure dal codice efficace.

Un esempio del tipo "sì", in cui le parti pure ed efficaci non sono ortogonali:

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

Ora, la stringa che stampi dipende dal valore di x. La parte della funzione (moltiplicata xper due), tuttavia, non dipende affatto dall'effetto, quindi possiamo ancora tenerne conto:

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)

Potrei continuare a svelare altri esempi, ma spero che questo sia abbastanza per illustrare il punto con cui ho iniziato: non lo "chiami" qualcosa, analizzi come le parti pure ed efficaci si collegano e le fattori quando sono a tuo vantaggio.

Questo è uno dei motivi per cui Haskell usa la sua Monadclasse così ampiamente. Le monadi sono (tra le altre cose) uno strumento per eseguire questo tipo di analisi e refactoring.


-2

Le funzioni che hanno lo scopo di causare effetti collaterali sono spesso chiamate efficaci . Esempio https://slpopejoy.github.io/posts/Effectful01.html


Rispondo solo per menzionare il termine ampiamente riconosciuto efficace e viene votato per difetto ... L'ignoranza è felicità suppongo. ..
Ben Hutchison,

"efficace" è una parola che l'autore di quel post ha cooptato nel senso di "avere effetti collaterali". Lo dice lui stesso.
Robert Harvey,

La funzione efficace di Google rivela molte prove che è un termine ampiamente usato. Il post sul blog è stato fornito come uno dei tanti esempi, non come definizione. Nei circoli di programmazione funzionale in cui le funzioni pure sono predefinite, è necessario un termine positivo per descrivere deliberatamente le funzioni con effetti collaterali. Vale a dire più che l' assenza di purezza . Questo termine è efficace. Ora, considerati istruito.
Ben Hutchison,

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.