Come viene applicata la trasparenza referenziale?

8

Nei linguaggi FP, il richiamo di una funzione con gli stessi parametri più e più volte restituisce lo stesso risultato più e più volte (cioè trasparenza referenziale).

Ma una funzione come questa (pseudo-codice):

function f(a, b) {
    return a + b + currentDateTime.seconds;
}

non restituirà lo stesso risultato per gli stessi parametri.

In che modo questi casi vengono gestiti in FP?

Come viene applicata la trasparenza referenziale? O non lo è e dipende dai programmatori comportarsi da soli?

    
posta JohnDoDo 09.05.2013 - 13:43
fonte

3 risposte

20

a e b sono Number s, mentre currentDateTime.seconds restituisce IO<Number> . Questi tipi sono incompatibili, non è possibile aggiungerli insieme, quindi la tua funzione non è ben scritta e semplicemente non verrà compilata. Almeno questo è il modo in cui è fatto in lingue pure con un sistema di tipo statico, come Haskell. Nei linguaggi impuri come ML, Scala o F #, spetta al programmatore assicurare la trasparenza referenziale e, naturalmente, in linguaggi tipizzati dinamicamente come Clojure o Scheme, non esiste un sistema di tipo statico per applicare la trasparenza referenziale.

    
risposta data 09.05.2013 - 14:27
fonte
8

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.
risposta data 09.05.2013 - 14:29
fonte
4

L'approccio abituale è quello di consentire al compilatore di tracciare se una funzione è pura attraverso l'intero grafo delle chiamate e di rifiutare il codice che dichiara le funzioni come pure che fanno cose impure (dove "chiamare una funzione impura" è anche impuro cosa).

Haskell lo fa rendendo tutto puro nel linguaggio stesso; tutto ciò che è impuro funziona nel runtime, non il linguaggio stesso. Il linguaggio costruisce semplicemente azioni IO usando pure funzioni. Il runtime trova quindi la funzione pura chiamata main dal modulo designato Main , la valuta ed esegue l'azione risultante (impura).

Altre lingue sono più pragmatiche al riguardo; un approccio comune consiste nell'aggiungere la sintassi per contrassegnare le funzioni 'pure' e vietare qualsiasi azione impura (aggiornamenti variabili, chiamata funzioni impure, costrutti di I / O) all'interno di tali funzioni.

Nel tuo esempio, currentDateTime è una funzione impura (o qualcosa che si comporta come uno), quindi chiamarla all'interno di un blocco puro è vietata e causerebbe un errore del compilatore. In Haskell, la tua funzione sarebbe simile a questa:

f :: Int -> Int -> IO Int
f a b = do
    ct <- getCurrentTime
    return (a + b + timeSeconds ct)

Se hai provato a farlo in una funzione non IO, come questa:

f :: Int -> Int -> Int
f a b =
    let ct = getCurrentTime
    in a + b + timeSeconds ct

... quindi il compilatore ti direbbe che i tuoi tipi non vengono estratti - getCurrentTime è di tipo IO Time , non Time , ma timeSeconds prevede Time . In altre parole, Haskell sfrutta il suo sistema di tipi per modellare (e rafforzare) la purezza.

    
risposta data 09.05.2013 - 18:35
fonte