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 main
e 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 length
funzione:
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 length
aspetta 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:
getline
di tipo IO String
,
- il secondo è costruito valutando la funzione
putStrLn
di tipo String -> IO ()
sul suo argomento.
Più precisamente, la seconda azione è costruita da
- rilegatura
line
per il valore letto dalla prima azione,
- valutare le funzioni pure
length
(calcolare la lunghezza come un numero intero) e quindishow
(trasformare l'intero in una stringa),
- costruire l'azione applicando la funzione
putStrLn
al 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 String
mentre la notazione do richiede che un'azione ( getLine
di 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.