La risposta è un sì qualificato. La tua intuizione che la pigrizia possa essere implementata in un linguaggio rigoroso in cui le funzioni sono oggetti di prima classe è corretta. Ma entrare nei dettagli rivela una serie di sottigliezze.
Prendiamo un linguaggio funzionale (con cui intendo un linguaggio in cui le funzioni possono essere costruite e manipolate come oggetti di prima classe, come nel calcolo lambda), dove l'applicazione della funzione è rigorosa (cioè la funzione¹ e il suo argomento (s) sono completamente valutati prima dell'applicazione della funzione). Userò la sintassi di Standard ML, poiché si tratta di un linguaggio funzionale rigoroso e storicamente importante. Un'applicazione rigorosa FA
(dove F
e A
sono due espressioni) può essere ritardato di codifica come
Thunk (F, A)
Questo oggetto contiene una funzione e un argomento è chiamato thunk. Siamo in grado di definire un tipo di thunk:
datatype ('a, 'b) thunk = Thunk of ('a -> 'b) * 'a;
e una funzione per valutare un thunk:
fun evaluate (Thunk (f, x)) = f x;
Nizza e facile finora. Ma non abbiamo, in effetti, implementato una valutazione pigra! Ciò che abbiamo implementato è la valutazione dell'ordine normale, nota anche come call-by-name. La differenza è che se il valore del thunk viene utilizzato più di una volta, viene calcolato ogni volta. La valutazione lenta (nota anche come call-by-need) richiede la valutazione dell'espressione al massimo una volta.
In un linguaggio puro e rigoroso, la valutazione lenta è infatti impossibile da implementare. Il motivo è che la valutazione di un thunk modifica lo stato del sistema: passa da non valutato a valutato. L'implementazione della valutazione lenta richiede un modo per modificare lo stato del sistema.
C'è una sottigliezza qui: se la semantica della lingua è definita puramente in termini di stato di terminazione delle espressioni e il valore delle espressioni di terminazione, quindi, in un linguaggio puro, call-by-need e call-by- il nome è indistinguibile. Call-by-value (vale a dire valutazione rigorosa) è distinguibile perché meno espressioni terminano - call-by-need e call-by-name nascondono qualsiasi non-terminazione che si verifica in un thunk che non viene mai valutato. L'equivalenza di call-by-need e call-by-name consente di considerare la valutazione lazy come un'ottimizzazione della valutazione di ordine normale (che ha delle buone proprietà teoriche). Ma in molti programmi, l'utilizzo di call-by-name anziché call-by-value farebbe saltare in aria il tempo di esecuzione calcolando il valore delle stesse espressioni più e più volte.
In una lingua con stato mutabile, la valutazione lazy può essere espressa memorizzando il valore nel thunk quando viene calcolato.
datatype ('a, 'b) lazy_state = Lazy of ('a -> 'b) * 'a | Value of 'a;
type ('a, 'b) lazy_state = ('a, 'b) lazy_state ref;
let lazy (f, x) = ref (Lazy (f, x));
fun force r =
case !r of Value y => y
| Lazy (f, x) => let val y = f x in r := Value y; y end;
Questo codice non è molto complicato, quindi, anche in dialetti ML che forniscono valutazione pigra come una funzione di libreria (eventualmente con zucchero sintattico), non viene utilizzato tutto ciò che spesso in pratica - spesso, il punto in quale valore sarà necessario è una posizione nota nei programmi, e i programmatori usano semplicemente una funzione e passano il suo argomento in quella posizione.
Mentre questo sta entrando in territorio soggettivo, direi che è molto più facile implementare una valutazione pigra come questa, piuttosto che implementare una valutazione rigorosa in un linguaggio come Haskell. Costringere la rigorosa valutazione in Haskell è praticamente impossibile (eccetto che avvolgere tutto in una monade di stato e scrivere essenzialmente codice ML nella sintassi Haskell). Ovviamente, una valutazione rigorosa non modifica i valori calcolati dal programma, ma può avere un impatto significativo sulle prestazioni (e, in particolare, a volte è molto apprezzata perché rende le prestazioni molto più prevedibili - prevedendo le prestazioni di un Il programma Haskell può essere molto difficile).
Questo meccanismo di valutazione e archiviazione è effettivamente il nucleo di ciò che un compilatore Haskell fa sotto il cofano. Haskell è puro², quindi non puoi implemente farlo nel linguaggio stesso! Tuttavia, sembra che il compilatore lo faccia sotto il cofano, perché questo particolare uso di effetti collaterali non infrange la purezza del linguaggio, quindi non invalida alcuna trasformazione del programma. Il motivo per cui il valore di un thunk è memorizzato è che trasforma la valutazione call-by-name in call-by-need, e come abbiamo visto sopra, questo non modifica i valori delle espressioni di terminazione, né cambia le espressioni che terminano.
Questo approccio può essere in qualche modo problematico in un linguaggio che combina una valutazione locale puramente funzionale con un ambiente multithread e un passaggio di messaggi tra i thread. (Questo è in particolare il modello di Erlang.) Se un thread inizia a valutare un thunk, e un altro thread ha bisogno del suo valore solo allora, che cosa succederà? Se non vengono prese precauzioni, entrambi i thread calcoleranno il valore e lo memorizzeranno. In un linguaggio puro, questo è innocuo nel senso che entrambi i thread calcoleranno lo stesso valore comunque³. Tuttavia questo può danneggiare le prestazioni. Per garantire che un thunk venga valutato solo una volta, il calcolo del valore deve essere racchiuso in un lock; questo aiuta con lunghi calcoli eseguiti molte volte ma fa male i calcoli che vengono eseguiti solo una volta, poiché prendere e rilasciare un blocco richiede del tempo.
¹ La funzione, non il corpo della funzione, ovviamente.
² O meglio, il frammento di Haskell che non usa una monade effetto collaterale è puro. ³ È necessario che la transizione tra un thunk ritardato e un valore calcolato sia atomico: i thread concorrenti devono essere in grado di leggere un valore lazy e ottenere un valore valido o un valore calcolato valido, non una combinazione di i due non è un oggetto valido. A livello di processore, la transizione da thunk ritardato a valore calcolato è solitamente un'assegnazione di puntatore, che per la maggior parte delle architetture è atomica, per fortuna.
Sembra che tu stia usando la parola "funzionale" per significare "ha funzioni come valori di prima classe". Molti programmatori di haskell considerano "funzionale" il significato di "puro" o "funzioni come nelle funzioni matematiche". Se intendi quest'ultimo, io penso ** la risposta è: i programmi che funzionano in un linguaggio rigoroso avranno la stessa semantica quando vengono spostati in un linguaggio pigro, ma l'inverso non è assolutamente vero. – jberryman
alla tua ultima osservazione - a seconda di ciò che chiameresti * prossima versione * (penso di aver visto anche questo discorso - ma l'ho preso come se: se avessimo ricominciato da capo sarebbe probabilmente di default) questo potrebbe interessarti : https://github.com/ghc/ghc/commit/46a03fbec6a02761db079d1746532565f34c340f – Carsten
Questa potrebbe essere una domanda migliore per http://programmers.stackexchange.com/ – Vlad274