Questa risposta è più o meno nel contesto del tuo attuale approccio piuttosto che in un approccio alternativo completamente diverso.
In primo luogo, la tua soluzione presenta alcuni problemi come sembra. La soluzione avrebbe più senso se il passaggio 1 fosse concomitante con il passaggio 2, piuttosto che una serie sequenziale di passaggi. Invece, considera cosa succederebbe se eseguissi actions
due volte in sequenza, cioè li combinassi in sequenza in un'espressione di calcolo più grande: primo pass1Action
verrebbe eseguito, quindi smetterebbe di attendere il passaggio 2. Quando passava 2, pass2Action
sarebbe Esegui, quindi la seconda istanza di pass1Action
verrebbe eseguita, quindi smetterebbe di attendere il passaggio 2 di nuovo (che presumibilmente non sarà mai (?)).
Essenzialmente, il problema è che stai trattando le azioni del passaggio 2 come sincronizzate in modo sincrono dal passaggio 1 che non ha senso: passare 2 azioni non possono restituire i valori per passare 1. Chiamare un passaggio successivo da una passata precedente dovrebbe essere visualizzato come una chiamata asincrona non di ritorno.
Puoi correggere questo e semplificare la tua implementazione (non avendo bisogno di usare le continuazioni) usando monitore Writer o Output . Fondamentalmente, si aggiunge un'operazione runInNextPass
(o anche runInPass
che prende quale pass a target come parametro). Il tuo codice sarà quindi il seguente:
let actions =
passHandler {
let myObject = pass1Action()
runInNextPass pass2Action
}
runInNextPass
è quindi solo tell
o una leggera variante dell'articolo. L'esecuzione del calcolo nel suo complesso lo farebbe funzionare normalmente, ottenendo un valore di Writer
, quindi eseguendo il valore di output. Funziona bene se vuoi eseguire solo N passaggi per un numero di passate noto (ma probabilmente solo al runtime): basta ripetere il pattern di esecuzione N volte. (Nella sua forma N = 2, questo è un modello a portata di mano quando si vuole eseguire un codice che richiede una fase di inizializzazione prima di una "vera" fase di esecuzione.) Tuttavia, semplicemente cambiando il monoide che stiamo usando (vedere la articolo), possiamo ottenere un paio di effetti diversi.
Lo scenario sopra riportato corrisponde ad avere l'output del Writer
monad, cioè il monoide, che è o le azioni che hanno effetti collaterali (o una monade IO) rappresentate come funzioni di tipo () -> ()
o Writer
monade stessa . Ogni strato di costruttori di tipo Writer
corrisponde a un passaggio aggiuntivo. Quindi Writer<() -> (), 'a>
rappresenta un sistema a 2 passaggi, Writer<Writer<() -> (), ()>,a>
rappresenta un sistema a 3 passaggi e così via. In questo caso, il numero di passaggi verrà applicato in modo statico, non è possibile utilizzare un'operazione di passaggio 2 (digitare Writer<() -> (), ()>
) nel passaggio 3 (operazioni di tipo () -> ()
). Se si desidera un numero di passaggi non noto staticamente, si crea il tipo ricorsivo type Pass<'a> = NextPass of Writer<Pass (), 'a> | FinalPass of () -> 'a
. Se F # supporta il polimorfismo di tipo superiore, potresti scrivere qualcosa come
type GPass<'f, 'a> = NextPass of Writer<'f<GPass<'f, 'a>>
| FinalPass of () -> 'a
Il vantaggio di questo è che possiamo cambiare quale monoid stiamo usando. Ad esempio, quando 'f
è Option
, allora possiamo avere una sequenza di passaggi che vengono eseguiti fino a quando non vengono richiesti passaggi successivi. Quando 'f
è una variante di Map
puoi avere passaggi che sono etichettati in modo tale che il tuo pass "parse" potrebbe richiedere direttamente il lavoro da svolgere nel passaggio "ottimizzazione" senza dover sapere quanti passaggi sono intermedi. In questo approccio, tuttavia, potresti voler passare informazioni su quali passaggi sono già stati eseguiti (possibilmente aggiungendo un aspetto di Reader o Environment monad), perché non c'è nulla che impedisca il passaggio di "ottimizzazione" dalla richiesta di lavoro da eseguire nel " parse "pass.