Cos'è la trasparenza referenziale?


38

L'ho visto nei paradigmi imperativi

f (x) + f (x)

potrebbe non essere lo stesso di:

2 * f (x)

Ma in un paradigma funzionale dovrebbe essere lo stesso. Ho provato a implementare entrambi i casi in Python e Scheme , ma per me sembrano abbastanza semplici lo stesso.

Quale sarebbe un esempio che potrebbe evidenziare la differenza con la data funzione?


7
Puoi, e spesso fai, scrivere funzioni referenzialmente trasparenti in Python. La differenza è che la lingua non la applica.
Karl Bielefeldt,

5
in C e simili: f(x++)+f(x++)potrebbe non essere lo stesso di 2*f(x++)(in C è particolarmente adorabile quando roba del genere è nascosta all'interno delle macro - mi sono rotto il naso? Ci scommetti)
moscerino

A mio avviso, l'esempio di @ gnat è il motivo per cui linguaggi orientati alla funzionalità come R utilizzano riferimenti pass-by ed evitano esplicitamente funzioni che modificano i loro argomenti. In R, almeno, può effettivamente essere difficile evitare queste restrizioni (almeno, in modo stabile e portatile) senza scavare nel complicato sistema del linguaggio di ambienti, spazi dei nomi e percorsi di ricerca.
Shadowtalker

4
@ssdecontrol: In realtà, quando si ha trasparenza referenziale, pass-by-value e pass-by-reference producono sempre lo stesso risultato esatto, quindi non importa quale lingua usi. I linguaggi funzionali sono spesso specificati con qualcosa di simile al pass-by-value per chiarezza semantica, ma le loro implementazioni usano spesso il pass-by-reference per le prestazioni (o anche entrambi, a seconda di quale è più veloce per il dato contesto).
Jörg W Mittag,

4
@gnat: In particolare, f(x++)+f(x++)può essere assolutamente qualsiasi cosa, dal momento che sta invocando un comportamento indefinito. Ma ciò non è realmente correlato alla trasparenza referenziale - che non sarebbe d'aiuto per questa chiamata, è "indefinito" anche per funzioni referenzialmente trasparenti come in sin(x++)+sin(x++). Potrebbe essere 42, formattare il disco rigido, avere demoni che volano fuori dal naso degli utenti ...
Christopher Creutzig

Risposte:


62

La trasparenza referenziale, riferita a una funzione, indica che è possibile determinare il risultato dell'applicazione di quella funzione solo osservando i valori dei suoi argomenti. È possibile scrivere funzioni referenzialmente trasparenti in qualsiasi linguaggio di programmazione, ad esempio Python, Scheme, Pascal, C.

D'altra parte, nella maggior parte delle lingue è anche possibile scrivere funzioni non referenzialmente trasparenti. Ad esempio, questa funzione Python:

counter = 0

def foo(x):
  global counter

  counter += 1
  return x + counter

non è referenzialmente trasparente, infatti chiama

foo(x) + foo(x)

e

2 * foo(x)

produrrà valori diversi, per qualsiasi argomento x. La ragione di ciò è che la funzione usa e modifica una variabile globale, quindi il risultato di ogni invocazione dipende da questo stato che cambia e non solo dall'argomento della funzione.

Haskell, un linguaggio puramente funzionale, separa rigorosamente la valutazione dell'espressione in cui vengono applicate le funzioni pure e che è sempre referenzialmente trasparente, dall'esecuzione dell'azione (elaborazione di valori speciali), che non è referenzialmente trasparente, ovvero l'esecuzione della stessa azione può avere ogni volta che un risultato diverso.

Quindi, per qualsiasi funzione di Haskell

f :: Int -> Int

e qualsiasi numero intero x, è sempre vero che

2 * (f x) == (f x) + (f x)

Un esempio di un'azione è il risultato della funzione di libreria getLine:

getLine :: IO String

Come risultato della valutazione dell'espressione, questa funzione (in realtà una costante) produce innanzitutto un valore puro di tipo IO String. I valori di questo tipo sono valori come tutti gli altri: è possibile passarli, inserirli in strutture di dati, comporli utilizzando funzioni speciali e così via. Ad esempio, puoi creare un elenco di azioni in questo modo:

[getLine, getLine] :: [IO String]

Le azioni sono speciali in quanto puoi dire al runtime di Haskell di eseguirle scrivendo:

main = <some action>

In questo caso, all'avvio del programma Haskell, il runtime attraversa l'azione associata maine la esegue , producendo probabilmente effetti collaterali. Pertanto, l'esecuzione dell'azione non è referenzialmente trasparente poiché l'esecuzione della stessa azione due volte può produrre risultati diversi a seconda di ciò che il runtime ottiene come input.

Grazie al sistema di tipi di Haskell, un'azione non può mai essere utilizzata in un contesto in cui è previsto un altro tipo e viceversa. Quindi, se vuoi trovare la lunghezza di una stringa puoi usare la lengthfunzione:

length "Hello"

restituirà 5. Ma se si desidera trovare la lunghezza di una stringa letta dal terminale, non è possibile scrivere

length (getLine)

perché si ottiene un errore di tipo: si lengthaspetta un input di tipo list (e una stringa è, in effetti, un elenco) ma getLineè un valore di tipo IO String(un'azione). In questo modo, il sistema di tipi assicura che un valore di azione simile getLine(la cui esecuzione viene eseguita al di fuori del linguaggio di base e che può essere trasparente in modo non referenziale) non possa essere nascosto all'interno di un valore di non azione di tipo Int.

MODIFICARE

Per rispondere alla domanda exizt, ecco un piccolo programma Haskell che legge una riga dalla console e ne stampa la lunghezza.

main :: IO () -- The main program is an action of type IO ()
main = do
          line <- getLine
          putStrLn (show (length line))

L'azione principale è costituita da due sottoazioni eseguite in sequenza:

  1. getline di tipo IO String ,
  2. il secondo è costruito valutando la funzione putStrLndi tipo String -> IO ()sul suo argomento.

Più precisamente, la seconda azione è costruita da

  1. rilegatura line per il valore letto dalla prima azione,
  2. valutare le funzioni pure length(calcolare la lunghezza come un numero intero) e quindishow (trasformare l'intero in una stringa),
  3. costruire l'azione applicando la funzione putStrLnal risultato di show.

A questo punto, la seconda azione può essere eseguita. Se hai digitato "Ciao", verrà stampato "5".

Nota che se ottieni un valore da un'azione usando la <-notazione, puoi usare quel valore solo all'interno di un'altra azione, ad esempio non puoi scrivere:

main = do
          line <- getLine
          show (length line) -- Error:
                             -- Expected type: IO ()
                             --   Actual type: String

perché show (length line)ha tipo Stringmentre la notazione do richiede che un'azione ( getLinedi tipo IO String) sia seguita da un'altra azione (ad es. putStrLn (show (length line))di tipo IO ()).

MODIFICA 2

La definizione di trasparenza referenziale di Jörg W Mittag è più generale della mia (ho votato a favore della sua risposta). Ho usato una definizione limitata perché l'esempio nella domanda si concentra sul valore di ritorno delle funzioni e volevo illustrare questo aspetto. Tuttavia, RT in generale si riferisce al significato dell'intero programma, comprese le modifiche allo stato globale e le interazioni con l'ambiente (IO) causate dalla valutazione di un'espressione. Quindi, per una corretta definizione generale, dovresti fare riferimento a quella risposta.


10
Il downvoter può suggerire come posso migliorare questa risposta?
Giorgio,

Quindi come si ottiene la lunghezza di una stringa letta dal terminale di Haskell?
sbichenko,

2
Questo è estremamente pedante, ma per completezza, non è il sistema di tipi di Haskell che assicura che azioni e funzioni pure non si mescolino; è il fatto che la lingua non fornisce alcuna funzione impura che puoi chiamare direttamente. Puoi effettivamente implementare il IOtipo di Haskell abbastanza facilmente in qualsiasi lingua con lambda e generici, ma poiché chiunque può chiamare printlndirettamente, l'implementazione IOnon garantisce la purezza; sarebbe semplicemente una convenzione.
Doval,

Volevo dire che (1) tutte le funzioni sono pure (ovviamente, sono pure perché il linguaggio non fornisce alcuna impura, anche se per quanto ne so ci sono alcuni meccanismi per aggirare quello), e (2) funzioni pure e le azioni impure hanno tipi diversi, quindi non possono essere mescolate. A proposito, cosa intendi per chiamata direttamente ?
Giorgio,

6
Il tuo punto di getLinenon essere referenzialmente trasparente non è corretto. Ti stai presentando getLinecome se valuti o si riduca a qualche stringa, la cui stringa particolare dipende dall'input dell'utente. Questo non è corretto IO Stringnon contiene più una stringa Maybe String. IO Stringè una ricetta per forse, forse ottenere una stringa e, come espressione, pura come qualsiasi altra in Haskell.
Modalità di lusso

25
def f(x): return x()

from random import random
f(random) + f(random) == 2*f(random)
# => False

Tuttavia, non è questo che significa Trasparenza referenziale. RT significa che è possibile sostituire qualsiasi espressione nel programma con il risultato della valutazione di quell'espressione (o viceversa) senza cambiare il significato del programma.

Prendi, ad esempio, il seguente programma:

def f(): return 2

print(f() + f())
print(2)

Questo programma è referenzialmente trasparente. Posso sostituire una o entrambe le occorrenze di f()con 2e funzionerà sempre allo stesso modo:

def f(): return 2

print(2 + f())
print(2)

o

def f(): return 2

print(f() + 2)
print(2)

o

def f(): return 2

print(2 + 2)
print(f())

si comporteranno tutti allo stesso modo.

Beh, in realtà, ho tradito. Dovrei essere in grado di sostituire la chiamata a printcon il suo valore di ritorno (che non ha alcun valore) senza cambiare il significato del programma. Tuttavia, chiaramente, se rimuovo semplicemente i dueprint affermazioni, il significato del programma cambierà: prima, stampava qualcosa sullo schermo, dopo che non lo faceva. L'I / O non è referenzialmente trasparente.

La semplice regola empirica è: se è possibile sostituire qualsiasi espressione, sottoespressione o chiamata di subroutine con il valore di ritorno di tale espressione, sottoespressione o chiamata di subroutine in qualsiasi parte del programma, senza che il programma cambi il suo significato, allora si ha un riferimento trasparenza. E ciò significa, in pratica, che non puoi avere alcun I / O, non puoi avere uno stato mutabile, non puoi avere effetti collaterali. In ogni espressione, il valore dell'espressione deve dipendere esclusivamente dai valori delle parti costituenti dell'espressione. E in ogni chiamata di subroutine, il valore di ritorno deve dipendere esclusivamente dagli argomenti.


4
"non può avere uno stato mutabile": Beh, puoi averlo se è nascosto e non influenza il comportamento osservabile del tuo codice. Pensa ad esempio alla memoizzazione.
Giorgio,

4
@Giorgio: Questo è forse soggettivo, ma direi che i risultati memorizzati nella cache non sono in realtà uno "stato mutabile" se sono nascosti e non hanno effetti osservabili. L'immutabilità è sempre un'astrazione implementata su hardware mutabile; spesso viene fornito dal linguaggio (fornendo l'astrazione di "un valore" anche se il valore può spostarsi tra i registri e le posizioni di memoria durante l'esecuzione e può svanire una volta noto che non verrà mai più usato), ma non è meno valido quando è fornito da una biblioteca o quant'altro. (Supponendo che sia implementato correttamente, ovviamente.)
ruakh

1
+1 Mi piace molto l' printesempio. Forse un modo per vederlo è che ciò che è stampato sullo schermo fa parte del "valore di ritorno". Se è possibile sostituire printcon la sua funzione il valore restituito e la scrittura equivalente sul terminale, l'esempio funziona.
Pierre Arlaud,

1
@Giorgio L'uso dello spazio / tempo non può essere considerato un effetto collaterale ai fini della trasparenza referenziale. Ciò renderebbe 4e 2 + 2non intercambiabili poiché hanno tempi di esecuzione diversi e l'intero punto di trasparenza referenziale è che puoi sostituire un'espressione con qualunque cosa valuti. La considerazione importante sarebbe la sicurezza del thread.
Doval,

1
@overexchange: la trasparenza referenziale significa che è possibile sostituire ogni sottoespressione con il suo valore senza cambiare il significato del programma. listOfSequence.append(n)ritorna None, quindi dovresti essere in grado di sostituire ogni chiamata listOfSequence.append(n)con Nonesenza cambiare il significato del tuo programma. Puoi farlo? In caso contrario, non è referenzialmente trasparente.
Jörg W Mittag,

1

Parti di questa risposta sono tratte direttamente da un tutorial incompiuto sulla programmazione funzionale , ospitato sul mio account GitHub:

Si dice che una funzione sia referenzialmente trasparente se, dati gli stessi parametri di input, produce sempre lo stesso output (valore di ritorno). Se si è alla ricerca di una ragion d'essere per la pura programmazione funzionale, la trasparenza referenziale è un buon candidato. Quando si ragiona con le formule in algebra, aritmetica e logica, questa proprietà - chiamata anche sostituibilità di uguali a uguali - è così fondamentalmente importante che di solito è data per scontata ...

Considera un semplice esempio:

x = 42

In un linguaggio funzionale puro, il lato sinistro e il lato destro del segno uguale sono sostituibili l'uno con l'altro in entrambi i modi. Cioè, a differenza di un linguaggio come C, la notazione di cui sopra afferma davvero un'uguaglianza. Una conseguenza di ciò è che possiamo ragionare sul codice del programma proprio come le equazioni matematiche.

Dal wiki di Haskell :

I calcoli puri danno lo stesso valore ogni volta che vengono invocati. Questa proprietà si chiama trasparenza referenziale e consente di condurre ragionamenti equazionali sul codice ...

Per contrastare ciò, il tipo di operazione eseguita da linguaggi simil-C viene talvolta definito un compito distruttivo .

Il termine puro è spesso usato per descrivere una proprietà di espressioni, rilevante per questa discussione. Perché una funzione sia considerata pura,

  • non è consentito esibire effetti collaterali e
  • deve essere referenzialmente trasparente.

Secondo la metafora della scatola nera, che si trova in numerosi libri di testo matematici, gli interni di una funzione sono completamente sigillati dal mondo esterno. Un effetto collaterale è quando una funzione o espressione viola questo principio, ovvero la procedura è autorizzata a comunicare in qualche modo con altre unità del programma (ad esempio per condividere e scambiare informazioni).

In sintesi, la trasparenza referenziale è un must per le funzioni che si comportano come vere funzioni matematiche anche nella semantica dei linguaggi di programmazione.


questo sembra aprirsi con una copia parola per parola presa da qui : "Si dice che una funzione sia referenzialmente trasparente se, dati gli stessi parametri di input, produce sempre lo stesso output ..." Stack Exchange ha regole per plagio , sono sei a conoscenza di questi? "Il plagio è l'atto senz'anima di copiare pezzi del lavoro di qualcun altro, di schiaffeggiare il tuo nome su di esso e di passarti come autore originale ..."
moscerino

3
Ho scritto quella pagina.
yesthisisuser

se questo è il caso, considera di far sembrare meno un plagio - perché i lettori non hanno modo di dirlo. Sai come farlo a SE? 1) Fai riferimento alla fonte degli originali, come "Come (ho) scritto [here](link to source)..." seguito da 2) formattazione corretta delle virgolette (usa virgolette, o meglio ancora, > simbolo per quello). Inoltre, non farebbe male se, oltre a fornire una guida generale, gli indirizzi di risposta alle domande concrete poste, in questo caso su f(x)+f(x)/ 2*f(x), vedi Come rispondere - altrimenti potrebbe sembrare che tu stia semplicemente pubblicizzando la tua pagina
moscerino

1
Teoricamente, ho capito questa risposta. Ma, praticamente seguendo queste regole, ho bisogno di restituire l'elenco delle sequenze di grandine in questo programma . Come faccio a fare questo?
scambio eccessivo del
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.