Cercherò di illustrare l'approccio di Haskell (non sono sicuro che la mia intuizione sia corretta al 100% dal momento che non sono un esperto di Haskell, le correzioni sono benvenute).
Il tuo codice può essere scritto in Haskell come segue:
import System.CPUTime
f :: Integer -> Integer -> IO Integer
f a b = do
t <- getCPUTime
return (a + b + (div t 1000000000000))
Quindi, dov'è la trasparenza referenziale?
f
è una funzione che, dato due interi a
e b
, creerà un'azione, come puoi dire dal tipo di ritorno IO Integer
.
Questa azione sarà sempre la stessa, dati i due numeri interi, quindi la funzione che associa una coppia di interi alle azioni IO è referenzialmente trasparente.
Quando viene eseguita questa azione, il valore intero che produce dipenderà dal tempo attuale della CPU: l'esecuzione delle azioni NON è l'applicazione della funzione.
Riassumere: In Haskell è possibile utilizzare le funzioni pure per costruire e combinare azioni complesse (sequenziamento, composizione di azioni e così via) in modo referenzialmente trasparente. Ancora una volta, nota che nell'esempio sopra la pura funzione f
non restituisce un intero: restituisce un'azione.
Modifica
Altri dettagli sulla domanda di JohnDoDo.
Che cosa significa "l'esecuzione delle azioni NON è l'applicazione di funzione"?
Dato set T1, T2, Tn, T, una funzione f è una mappatura (relazione) che associa a ciascuna tupla in T1 x T2 x ... x Tn un valore in T.
Quindi l'applicazione di funzione produce un valore di output dato alcuni valori di input.
Utilizzando questo meccanismo puoi costruire espressioni che valutano in valori ad es. il valore 10
è il risultato della valutazione dell'espressione 4 + 6
. Nota che, quando si mappano valori su valori in questo modo, non si sta eseguendo alcun tipo di input / output.
In Haskell, azioni sono valori di tipi speciali che possono essere costruiti valutando espressioni contenenti funzioni pure appropriate che funzionano con le azioni. In questo modo, un programma Haskell è un'azione composita ottenuta valutando la funzione main
. Questa azione principale ha tipo IO ()
.
Una volta definita questa azione composita, viene utilizzato un altro meccanismo (non l'applicazione di funzione) per invocare / eseguire l'azione (si veda ad esempio qui ). L'intera esecuzione del programma è il risultato del richiamo dell'azione principale che a sua volta può richiamare sotto-azioni.
Questo meccanismo di chiamata (i cui dettagli interni non conosco) si occupa di eseguire tutte le chiamate IO necessarie, possibilmente accedendo al terminale, al disco, alla rete e così via.
Tornando all'esempio.
La funzione f
sopra non restituisce un intero e non puoi scrivere una funzione che esegue IO e restituisce un intero allo stesso tempo: devi scegliere uno dei due.
Ciò che puoi fare è incorporare l'azione restituita da f 2 3
in un'azione più complessa. Ad esempio, se vuoi stampare il numero intero prodotto da quell'azione, puoi scrivere:
main :: IO ()
main = do
x <- f 2 3
putStrLn (show x)
La notazione do
indica che l'azione restituita dalla funzione principale è ottenuta da una composizione sequenziale di due azioni più piccole e la notazione x <-
indica che il valore prodotto nella prima azione deve essere passato alla seconda azione .
Nella seconda azione
putStrLn (show x)
il nome x
è associato al numero intero prodotto dall'esecuzione dell'azione
f 2 3
Un punto importante è che il numero intero prodotto quando viene invocata la prima azione può solo vivere all'interno di azioni IO: può essere passato da un'azione IO al successivo ma non può essere estratto come un valore intero semplice.
Confronta la funzione main
sopra con questa:
main = do
let y = 2 + 3
putStrLn (show y)
In questo caso c'è solo un'azione, cioè putStrLn (show y)
, e y
è legata al risultato dell'applicazione della funzione pura +
. Potremmo anche definire
questa azione principale come segue:
main = putStrLn "5"
Quindi, nota la diversa sintassi
x <- f 2 3 -- Inject the value produced by an action into
-- the following IO actions.
-- The value may depend on when the action is
-- actually executed. What happens when the action is
-- executed is not known here: it may get user input,
-- access the disk, the network, the system clock, etc.
let y = 2 + 3 -- Bind y to the result of applying the pure function '+'
-- to the arguments 2 and 3.
-- The value depends only on the arguments 2 and 3.
Riepilogo
- In Haskell le funzioni pure sono utilizzate per costruire le azioni che costituiscono un programma.
- Le azioni sono valori di un tipo speciale.
- Poiché le azioni sono costruite applicando funzioni pure, la costruzione dell'azione è referenzialmente trasparente.
- Dopo che un'azione è stata costruita, può essere richiamato utilizzando un meccanismo separato.