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.