Effetti collaterali che interrompono la trasparenza referenziale

10

La programmazione funzionale in Scala spiega l'impatto di un effetto collaterale sulla violazione della trasparenza referenziale:

side effect, which implies some violation of referential transparency.

Ho letto parte di SICP , che discute utilizzando il "modello di sostituzione" per valutare un programma.

Siccome io approssimativamente capisco il modello di sostituzione con trasparenza referenziale (RT), puoi decomporre una funzione nelle sue parti più semplici. Se l'espressione è RT, puoi deselezionare l'espressione e sempre ottenere lo stesso risultato.

Tuttavia, come afferma la citazione precedente, l'uso degli effetti collaterali può / interromperà il modello di sostituzione.

Esempio:

val x = foo(50) + bar(10)

Se foo e bar non hanno effetti collaterali, l'esecuzione di entrambe le funzioni sempre restituirà lo stesso risultato a x . Ma, se hanno effetti collaterali, modificheranno una variabile che interrompe / getta una chiave nel modello di sostituzione.

Mi sento a mio agio con questa spiegazione, ma non la prendo completamente.

Per favore correggimi e riempia tutti i buchi rispetto agli effetti collaterali che spezzano RT, discutendo anche gli effetti sul modello di sostituzione.

    
posta Kevin Meredith 07.01.2014 - 04:43
fonte

3 risposte

17

Iniziamo con una definizione per trasparenza referenziale :

An expression is said to be referentially transparent if it can be replaced with its value without changing the behavior of a program (in other words, yielding a program that has the same effects and output on the same input).

Ciò significa che (ad esempio) è possibile sostituire 2 + 5 con 7 in qualsiasi parte del programma, e il programma dovrebbe comunque funzionare. Questo processo è chiamato sostituzione. La sostituzione è valida se, e solo se, 2 + 5 può essere sostituita con 7 senza influenzare altre parti del programma .

Diciamo che ho una classe chiamata Baz , con le funzioni Foo e Bar in essa. Per semplicità, diremo semplicemente che Foo e Bar restituiscono entrambi il valore passato. Quindi Foo(2) + Bar(5) == 7 , come ci si aspetterebbe. La trasparenza referenziale garantisce che è possibile sostituire l'espressione Foo(2) + Bar(5) con l'espressione 7 in qualsiasi punto del programma e il programma continuerà a funzionare in modo identico.

Ma cosa succede se Foo ha restituito il valore passato, ma Bar ha restituito il valore passato, più l'ultimo valore fornito a Foo ? È abbastanza facile da fare se si memorizza il valore di Foo in una variabile locale all'interno della classe Baz . Bene, se il valore iniziale di tale variabile locale è 0, l'espressione Foo(2) + Bar(5) restituirà il valore previsto di 7 la prima volta che lo invocherai, ma restituirà 9 la seconda volta che lo invocherai.

Ciò viola la trasparenza referenziale in due modi. Innanzitutto, non è possibile contare su Bar per restituire la stessa espressione ogni volta che viene chiamata. Secondo, si è verificato un effetto collaterale, vale a dire che la chiamata di Foo influenza il valore di ritorno di Bar. Poiché non puoi più garantire che Foo(2) + Bar(5) sarà uguale a 7, non puoi più sostituirlo.

Questo è ciò che indica la trasparenza referenziale nella pratica; una funzione referentially transparent accetta un valore e restituisce un valore corrispondente, senza influenzare altro codice altrove nel programma, e restituisce sempre lo stesso output dato lo stesso input.

    
risposta data 07.01.2014 - 04:55
fonte
3

Immagina di provare a costruire un muro e ti è stato dato un assortimento di scatole di diverse dimensioni e forme. È necessario riempire un particolare foro a forma di L nel muro; dovresti cercare una scatola a forma di L o puoi sostituire due scatole dritte della dimensione appropriata?

Nel mondo funzionale, la risposta è che entrambe le soluzioni funzioneranno. Quando costruisci il tuo mondo funzionale, non devi mai aprire le scatole per vedere cosa c'è dentro.

Nel mondo imperativo, è pericoloso costruire il tuo muro senza ispezionare il contenuto di ogni casella e confrontandoli con il contenuto di ogni altra casella:

  • Alcuni contengono magneti potenti e spingono altre scatole magnetiche fuori dal muro se non correttamente allineate.
  • Alcuni sono molto caldi o freddi e reagiscono male se collocati in spazi adiacenti.

Penso che mi fermerò prima che sprechi il tuo tempo con metafore più improbabili, ma spero che il punto sia fatto; i mattoni funzionali non contengono sorprese nascoste e sono completamente prevedibili. Perché puoi sempre usare blocchi più piccoli della giusta dimensione e forma per sostituirne uno più grande e non c'è differenza tra due scatole della stessa dimensione e forma, hai la trasparenza referenziale. Con i mattoncini imperativi, non è sufficiente avere qualcosa della giusta dimensione e forma - devi sapere come è stato costruito il mattone. Non trasparente referenzialmente.

In un linguaggio puramente funzionale, tutto ciò che devi vedere è la firma di una funzione per sapere cosa fa. Certo, potresti voler guardare dentro per vedere come funziona, ma non avere per cercare.

In un linguaggio imperativo, non sai mai quali sorprese potrebbero nascondersi all'interno.

    
risposta data 07.01.2014 - 17:02
fonte
2

As I roughly understand the substitution model (with referential transparency(RT)), you can de-compose a function into its simplest parts. If the expression is RT, then you can de-compose the expression and always get the same result.

Sì, l'intuizione è giusta. Ecco alcuni suggerimenti per essere più precisi:

Come hai detto tu, qualsiasi espressione RT dovrebbe avere un "risultato" single . Cioè, data un'espressione factorial(5) nel programma, dovrebbe sempre dare lo stesso "risultato". Quindi, se un certo factorial(5) è nel programma e produce 120, dovrebbe sempre restituire 120 indipendentemente da quale "ordine di passo" è espanso / calcolato, indipendentemente dal tempo .

Esempio: la funzione factorial .

def factorial(n):
    if n == 1:
        return 1
    return n * factorial(n - 1)

Ci sono alcune considerazioni con questa spiegazione.

Prima di tutto, tieni presente che i diversi modelli di valutazione (vedi l'ordine rispetto all'ordine normale) possono produrre "risultati" diversi per la stessa espressione RT.

def first(y, z):
  return y

def second(x):
  return second(x)

first(2, second(3)) # result depends on eval. model

Nel codice sopra, first e second sono referenzialmente trasparenti, e tuttavia, l'espressione alla fine restituisce "risultati" diversi se valutati secondo l'ordine normale e l'ordine applicativo (sotto quest'ultimo, l'espressione non si ferma ).

.... che porta all'utilizzo del "risultato" tra virgolette. Poiché non è richiesto che un'espressione si fermi, potrebbe non produrre un valore. Quindi usare "risultato" è un po 'sfocato. Si può dire che un'espressione RT produce sempre lo stesso computations in un modello di valutazione.

In terzo luogo, potrebbe essere richiesto di vedere due foo(50) che appaiono nel programma in posizioni diverse come espressioni diverse, ognuna delle quali produce i propri risultati che potrebbero differire l'una dall'altra. Ad esempio, se la lingua consente l'ambito dinamico, entrambe le espressioni, sebbene in modo lessicale identico, sono diverse. In perl:

sub foo {
    my $x = shift;
    return $x + $y; # y is dynamic scope var
}

sub a {
    local $y = 10;
    return &foo(50); # expanded to 60
}

sub b {
    local $y = 20;
    return &foo(50); # expanded to 70
}

L'ambito dinamico risulta fuorviante perché rende facile pensare a un utente che x è l'unico input per foo , quando in realtà è x e y . Un modo per vedere la differenza è trasformare il programma in uno equivalente senza ambito dinamico, cioè passando esplicitamente i parametri, quindi invece di definire foo(x) , definiamo foo(x, y) e passiamo y esplicitamente nei chiamanti .

Il punto è che siamo sempre sotto una mentalità function : dato un certo input per un'espressione, ci viene dato un corrispondente "risultato". Se diamo lo stesso input, dovremmo sempre aspettarci lo stesso "risultato".

Ora, per quanto riguarda il seguente codice?

def foo():
   global y
   y = y + 1
   return y

y = 10
foo() # yields 11
foo() # yields 12

La procedura foo interrompe RT perché ci sono ridefinizioni. Cioè, abbiamo definito y in un punto e, in seguito, ridefinito quel stesso y . Nell'esempio perl di cui sopra, il y s sono bind diversi sebbene condividano lo stesso nome di lettera "y". Qui il y s è in realtà lo stesso. Ecco perché diciamo che (ri) l'assegnazione è un'operazione meta : in effetti stai cambiando la definizione del tuo programma.

Approssimativamente, le persone di solito descrivono la differenza come segue: in un'impostazione a effetto collaterale, si ha una mappatura da input -> output . In un'impostazione "imperativa", hai input -> ouput nel contesto di un state che può cambiare nel tempo.

Ora, invece di sostituire semplicemente le espressioni per i valori corrispondenti, è necessario applicare anche le trasformazioni a state a ogni operazione che lo richiede (e, naturalmente, le espressioni possono consultare lo stesso state per eseguire calcoli).

Quindi, se in un programma privo di effetti collaterali tutto ciò che dobbiamo sapere per calcolare un'espressione è il suo input individuale, in un programma imperativo, abbiamo bisogno di conoscere gli input e l'intero stato, per ogni fase computazionale. Il ragionamento è il primo a subire un grosso colpo (ora, per eseguire il debug di una procedura problematica, è necessario l'input e il core dump). Alcuni trucchi sono resi poco pratici, come la memoizzazione. Ma anche la concorrenza e il parallelismo diventano molto più impegnativi.

    
risposta data 07.01.2014 - 11:52
fonte

Leggi altre domande sui tag