Una continuazione rappresenta lo stato di controllo del calcolo in un determinato punto durante la valutazione. Continuazioni delimitate è un meccanismo di programmazione che può essere utilizzato per implementare vari costrutti di flusso di controllo. Le continuazioni del termometro implementano la continuazione delimitata usando eccezioni e stato, in particolare focalizzate sul risparmio e nel riassumere calcoli interattivi o simultanei in punti specifici; Spesso richiedono un contesto o costrutti aggiuntivi per essere realizzati pienamente.
In un documento del 2018, “Catturare il futuro riproducendo il passato“Koppel et al. Hanno mostrato che in qualsiasi lingua con eccezione e stato, è possibile utilizzare proceed termometriche. Ciò consente alla maggior parte delle lingue di implementare le proceed del termometro. Koppel ha fornito Java, OCAML e Implementazioni di riferimento SML come parte del documento; L’implementazione di Python è disponibile Quidove l’autore ha ideato un modo per superare Generatore di PythonL’incapacità di eseguire la continuazione più volte.
Una nozione curiosa è quella Scala ha fornito un plug -in di continuazione delimitato nelle versioni precedenti, ma è sospeso in Scala 3 a causa di varie difficoltà tecniche, con l’intenzione di aggiungere la continuazione come nuova funzionalità linguistica. Questa intenzione è in Stadio pre-sip per raccogliere suggestions della comunità. Il pre-sip ha proposto due opzioni: Aggiungi una nuova parola chiave sospendereo l’uso del compilatore-desugato Sospendere funzioni di contesto.
Poiché Scala supporta le eccezioni e lo stato, è praticabile l’implementazione di proceed delimitate nella libreria customary Scala. Poiché questo approccio è più semplice e richiede i minimi cambiamenti linguistici, questa può essere la terza opzione. L’implementazione può far luce per implementare una continuazione delimitata più generalizzata e altre proceed specializzate come proceed di fuga, continuazioni di backtracking, proceed delle coroutine e proceed logiche, ecc.
In questo articolo, dimostriamo che la continuazione del termometro può essere implementata Scala; Spieghiamo anche le sfumature e i colpi di scena nell’implementazione. Mustifichiamo l’implementazione attraverso vari scenari di take a look at.
Continuazione delimitata in Scala
Lo studio della continuazione nella programmazione rimane indietro. Non è intenzione di questo articolo discutere la storia e l’estensione della ricerca. È possibile trovare una spiegazione dettagliata Qui.
A differenza della continuazione non delimitata che rappresenta l’intero resto del calcolo da un certo punto, la continuazione delimitata è composibile: cattura una parte specifica del programma, delimitata da delimitatori, in quanto consente di abbattere le operazioni di flusso di controllo complesse in pezzi più piccoli, gestibili e riutilizzabili. Questa composibilità rende più semplice combinare e impilare diversi costrutti di flusso di controllo in modo modulare. Le proceed delimitate operano entro i confini espliciti definiti dai costrutti di ripristino e spostamento nello schema. Ciò significa che possiamo isolare segmenti specifici del nostro codice per l’acquisizione e la manipolazione della continuazione.
L’implementazione varia a seconda dei linguaggi di programmazione. I plug -in Scala precedenti utilizzati operatori di reset/shift con strategia CPS (Continuation Passing Type). Il plugin trasforma le parti del programma contenute in una chiamata per ripristinare il modulo di passaggio di continuazione. All’interno della parte trasformata del programma, è possibile accedere alle proceed chiamando Shift. Continuiamo a utilizzare questa strategia per implementare questi operatori.
Con questa strategia, reset {} definisce un blocco che stabilisce un “confine” per la continuazione; Maiusc {} cattura l’attuale continuazione fino al ripristino di rallentamento più vicino e la passa alla funzione okay, okay è chiamata come qualsiasi altra funzione, alterando efficacemente il flusso del programma. Questo carta Spiega questo concetto in dettaglio.
Advert esempio, una continuazione prende questo modulo:
reset(2 * shift(1 + okay(5))) //”okay” replaces as much as the closest “reset”
Il turno cattura la continuazione (*2 …), e al suo interno, è chiamato Okay (5), rendimenti 5. Questo è sostituito dalla moltiplicazione 2*5, rendimenti 10. Aggiunta di 1 all’interno del blocco del cambio, provoca 1 + 10. Quindi la valutazione produce 1+ (2*5) = 11.
Scrivendolo in Scala 3, il codice sembra questo:
reset {
2 * shift { (okay: Int => Int) => 1 + okay(5)
}
Continuazione del termometro
L’concept alla base delle proceed del termometro è quella di implementare proceed delimitate usando eccezioni e stato. Invece di catturare direttamente uno stato intermedio del calcolo, le proceed del termometro riproducono l’intero calcolo dall’inizio, guidandolo usando una registrazione in modo che la stessa cosa accada fino al punto catturato.
Salvando gli attuali valori del passato e del futuro in uno stack prima di ogni esecuzione e ripristinandoli in seguito, l’algoritmo ottiene efficacemente la capacità di salvare lo stato di ciascuna esecuzione per il replay, fino a quando il ripristino {} raggiunge un valore.
Se la valutazione all’interno di un reset raggiunge una chiamata a Shift, l’argomento di Shift viene invocato con una continuazione delimitata Okay corrispondente all’intero contesto di valutazione fino alla chiamata di ripristino più vicina.
Ci sono due situazioni in cui si chiama Shift. In un caso in cui viene invocato il turno da Gestire la continuazione, quindi lo stato non sarà nessuno e dovrebbe restituire il valore nello stato; Il secondo caso è quando viene eseguito il turno con La continuazione, imposta una continuazione, esegue il turno con quella continuazione e passa il risultato al ripristino racchiuso aumentando un’eccezione.
Implementazione della continuazione del termometro in Scala 3
Diamo un’occhiata al codice per vedere come viene implementata la continuazione del termometro.
Per prima cosa definiamo la struttura dei dati che utilizzeremo nell’implementazione.
Tre attributi mutabili – Future, Previous e CureXPR – sono usati per catturare lo stato attuale dell’esecuzione: valori di esecuzioni future, valori delle esecuzioni passate e funzione dell’attuale esecuzione.
var future = Listing.empty(Choice(A))
var previous = Listing.empty(Choice(A))
var curExpr: Choice(() =>A) = None
Uno stack mutabile viene utilizzato come registrazione di ogni continuazione:
var recording: mutable.Stack((Choice(() => A), Listing(Choice(A)), Listing(Choice(A)))) = mutable.Stack((curExpr, previous, future))
È definita un’eccezione per catturare il risultato di un’esecuzione:
last personal case class Performed(A)(worth: A) extends Exception
La funzione reset () prende il corpo del programma come funzione e inizia l’esecuzione passando il corpo della funzione al termometro, ripristina lo stato a nessuno e esegue il corpo. Il corpo della funzione è efficacemente il confine della continuazione.
thermometer(() =>func, Listing.empty(Choice(A)))
La funzione Maiusc () prende la funzione di continuazione come parametro; Se rileva che non esiste un’esecuzione futura, viene lanciata un’eccezione per contrassegnare la high-quality dell’esecuzione, l’eccezione restituisce il valore dell’esecuzione corrente al termometro:
state.future match
case Nil => thermoDone(func)
case None :: tail =>
state.future = tail
thermoDone(func)
L’eccezione verrà catturata nel termometro. Quando il futuro è esaurito, prima di lanciare l’eccezione, invertiamo il futuro al passato per il replay, aggiungiamo il valore della funzione di continuazione al futuro (nella nostra implementazione, funzione okay ()), rieseguono il termometro in modo efficace, sostituirà la funzione di continuazione con il calcolo al ripristino più vicino, come il nome di Koppel suggerisce, “catturare il futuro sostituendo il passato”:
val newFuture = state.previous.reverse
val newExpr = state.curExpr.orNull
val okay = (v: A) => thermometer(newExpr, newFuture :+ Some(v))
state.pushPast(None)
throw Performed(A)(func(okay))
Altrimenti, il futuro viene consumato e il risultato dell’attuale esecuzione viene spinto al passato.
case Some(worth) :: tail =>
state.future = tail
state.pushPast(Some(worth))
worth
La funzione termometro () spinge l’attuale esecuzione nella registrazione, esegue il corpo della funzione information, restituisce il risultato nell’eccezione se non più futura esecuzione, altrimenti, continua al futuro.
state.pushRecording(func, funcFuture)
attempt {
func()
} catch {
case Performed(e) => e.asInstanceOf(A)
}
val outcome = run()
state.popRecording()
outcome
Il processo continua fino a quando tutti gli stati non sono stati riprodotti all’indietro in modo ricorsivo, fino a quando il risultato non viene restituito alla funzione reset ().
Si noti che il contesto di esecuzione viene passato alle funzioni reset (), shift () e termometro () implicitamente, riducendo le piastre delle caldaie:
(utilizing state:ContinuationState(A))
Take a look at
Il nostro take a look at copre i seguenti scenari:
reset {
2 * shift { (okay: Int => Int) => 1 + okay(5)
}
}
Okay (5) valutato come 5, sostituito da 2*5, continua la valutazione. Il take a look at produce 2*5 + 1 = 11.
- Funzioni di continuazione parallele a number of:
reset {
1 + shift{
(okay: Int =>Int) => okay(1) * okay(2) * okay(3)
}
}
Okay (1) è sostituito come 1+1, Okay (2) è sostituito di 1+2, Okay (3) è sostituito di 1+3.
Il take a look at produce (1+1) * (1+2) * (1+3) = 24.
1 + reset {
2 + shift {
(okay: Int => Int) => 3 * shift{(l: Int => Int) => l(okay(5))}
}
}
Okay (5) è la continuazione dello spostamento esterno, quindi è sostituito di 2+5 = 7; L (7) è la funzione di continuazione dello spostamento interno, quindi è sostituita di 3* 7.
I take a look at producono 1 + (3*(2 + 5)) = 22.
reset {
2 * shift{(okay: Int => Int) => 1 +okay(5)} + shift{(okay: Int => Int) => 2 +okay(6)}
}
Okay (5) è sostituito da 2*5, il primo termine viene valutato come 1+2*5 = 11; Okay (6) è sostituito di 11+6;
Il take a look at produce 2 + (((1 + (2*5)) +6) = 19.
- Funzione di continuazione nidificata:
reset {
2 * shift { (okay: Int => Int) => 1 + okay(okay(5)) }
}
Viene valutata la funzione di continuazione interna, Okay (5) è sostituita di 2*5 = 10; La funzione di continuazione esterna okay (10) viene valutata in seguito, sostituita di 2*20, i rendimenti del take a look at
1+2*2*5 = 21
reset {
1 + shift {
(okay: Int => Int) => reset{
{
2 * shift{
(l: Int => Int) => l(3) + l(5) + okay(4)
}
}
}
}
}
La funzione di continuazione Okay appartiene al ripristino esterno, sostituito da 1+4, L appartiene al ripristino interno, L (3) e L (5) sono sostituiti rispettivamente di 2*3 e 2*5;
Il take a look at produce (2*3) + (2*5) + (1 + 4) = 21
- Non è presente una funzione di continuazione:
reset{
1 + shift{(okay: Int => Int) => 2+3}
}
Quindi non è presente una funzione di continuazione in Shift, la continuazione non ha luogo.
Il take a look at produce 2+3 = 5.
- Tipo di risposta diverso da int:
reset{
"a" + shift{(okay: String => String) => "b" + okay("c")}
}
Possiamo vedere che il tipo di risposta può essere di tipi diversi da int.
Il take a look at produce “B”+(“A”+”C”) = “BAC”.
Limitazioni
Poiché Scala è stata digitata staticamente, non è possibile implementare i tipi polimorfici per ripristinare e spostare. Abbiamo dimostrato che diversi tipi possono essere supportati da diversi tipi di donazioni, tuttavia il tipo di risposta è statico in un determinato contesto. Riteniamo che questa limitazione si applichi a tutte le lingue tipizzate staticamente.
Riepilogo
Nel documento di Koppel del 2018, è stata dimostrata un’concept interessante per implementare la continuazione delimitata a qualsiasi lingua che supporti eccezioni e stato. Prendiamo Scala 3 come lingua ospite e dimostriamo l’implementazione. Abbiamo dimostrato che, nonostante la semplicità, l’impianto copre una vasta gamma di scenari.
È possibile trovare l’implementazione di riferimento Qui.