La trasparenza referenziale, riferita a una funzione, indica che è possibile determinare il risultato dell'applicazione di tale funzione solo osservando i valori dei suoi argomenti. Puoi scrivere funzioni referenzialmente trasparenti in qualsiasi linguaggio di programmazione, ad es. Python, Scheme, Pascal, C.
D'altra parte, nella maggior parte delle lingue è anche possibile scrivere funzioni trasparenti non referenziali. 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
. Il motivo è che la funzione utilizza e modifica una variabile globale, quindi il risultato di ogni chiamata dipende da questo stato di modifica e non solo dall'argomento della funzione.
Haskell, un linguaggio puramente funzionale, separa rigorosamente valutazione dell'espressione in cui vengono applicate pure funzioni e che è sempre referenzialmente trasparente, da esecuzione di azioni (elaborazione di valori speciali), che non è referenzialmente trasparente, cioè l'esecuzione della stessa azione può avere ogni volta un risultato diverso.
Quindi, per qualsiasi funzione di Haskell
f :: Int -> Int
e qualsiasi 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 gli altri: puoi passarli in giro, 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, quando il tuo programma Haskell viene avviato, il runtime passa attraverso l'azione associata a main
e esegue , producendo probabilmente effetti collaterali. Pertanto, l'esecuzione dell'azione non è referenzialmente trasparente perché 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
dove è previsto un altro tipo e viceversa. Quindi, se vuoi trovare la lunghezza di una stringa, puoi utilizzare la funzione length
:
length "Hello"
restituirà 5. Ma se vuoi trovare la lunghezza di una stringa letta dal terminale, non puoi scrivere
length (getLine)
perché si ottiene un errore di tipo: length
si aspetta un input di tipo list (e una String è, infatti, una lista) ma getLine
è un valore di tipo IO String
(un'azione). In questo modo, il sistema di tipi garantisce che un valore di azione come getLine
(la cui esecuzione viene eseguita al di fuori del linguaggio principale e che può essere trasparente non referenzialmente) non possa essere nascosto all'interno di un valore non di azione di tipo Int
.
Modifica
Per rispondere a questa domanda, ecco un piccolo programma Haskell che legge una linea 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 consiste di due sottoazioni che vengono 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
- vincolante
line
al valore letto dalla prima azione,
- valutazione delle funzioni pure
length
(lunghezza di calcolo come numero intero) e poi show
(trasformare il numero 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".
Tieni presente che se ottieni un valore da un'azione utilizzando la notazione <-
, puoi utilizzare tale valore solo all'interno di un'altra azione, ad es. 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 richiede che un'azione ( getLine
di tipo IO String
) sia seguita da un'altra azione (ad esempio putStrLn (show (length line))
di tipo IO ()
).
EDIT 2
La definizione di trasparenza referenziale di Jörg W Mittag è più generale della mia (ho alzato la sua risposta). Ho usato una definizione limitata perché l'esempio nella domanda si concentra sul valore restituito delle funzioni e volevo illustrare questo aspetto. Tuttavia, RT in generale fa riferimento 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 definizione generale corretta, dovresti fare riferimento a quella risposta.