2016-04-10 13 views
17

Sto usando F # e Chessie per comporre una sequenza di attività (con effetti collaterali) che possono avere successo o fallire.Come gestire i rollback quando si utilizza l'Either monad ("programmazione orientata alla ferrovia")

Se qualcosa non funziona, voglio interrompere l'esecuzione delle attività rimanenti e il rollback di quelle che sono già riuscite.

Sfortunatamente, una volta raggiunto il percorso di 'errore', non è più possibile recuperare i risultati delle attività riuscite in modo da poterli ripristinare.

Esiste uno "schema" di programmazione funzionale che si occupa di questo scenario?

Esempio:

let refuel = 
    async { 
    printfn "1 executed" 
    // Fill missile with fuel 
    return Result<string,string>.Succeed "1" 
    } |> AR 

let enterLaunchCodes = 
    async { 
    printfn "2 executed" 
    // 
    return Result<string,string>.FailWith "2" 
    } |> AR 

let fireMissile = 
    async { 
    printfn "3 executed" 
    return Result<string,string>.Succeed "3" 
    } |> AR 

let launchSequence = 
    asyncTrial { 
    let! a = refuel 
    let! b = enterLaunchCodes 
    let! c = fireMissile 
    return a,b,c 
    } 

let result = launchSequence 
    |> Chessie.ErrorHandling.AsyncExtensions.Async.ofAsyncResult 
    |> Async.RunSynchronously 

// Result is a failure... how do I know the results of the successful operations here so I can roll them back? 

printfn "Result: %A" result 
+4

I risultati sarebbero sufficienti per tornare indietro con lo stesso? Probabilmente opterei per un tipo di risultato che accumuli le operazioni di rollback effettive, quindi in qualsiasi momento il valore è una tupla di success/failure (come ora) e una funzione di rollback '() ->()'. –

+0

Per quanto riguarda il problema, l'hai chiesto come una domanda funzionale. Mi viene subito in mente l'utilizzo di Prolog o [inferencing] (https://en.wikipedia.org/wiki/Inference_engine). È possibile implementare l'inferenza in F # e funziona benissimo. –

+0

Ganesh: Hmm è un'idea interessante. Avrò un gioco e vedrò come funziona! – Oenotria

risposta

18

Dato che le persone hanno fatto notare nei commenti, ci sono un paio di opzioni che possono essere utilizzate per risolvere questo problema.

Un modo è utilizzare compensating transactions.

In questo approccio, la custodia Success contiene un elenco di funzioni di "annullamento". Ogni passaggio che può essere annullato aggiunge una funzione a questo elenco. Quando un passo fallisce, ogni funzione di annullamento nella lista viene eseguita (nell'ordine inverso).

Ci sono modi più sofisticati per farlo naturalmente (ad es. Memorizzare le funzioni di annullamento in modo permanente in caso di arresti anomali, o this kind of thing).

Ecco po 'di codice che illustra questo approccio:

/// ROP design with compensating transactions  
module RopWithUndo = 

    type Undo = unit -> unit 

    type Result<'success> = 
     | Success of 'success * Undo list 
     | Failure of string 

    let bind f x = 
     match x with 
     | Failure e -> Failure e 
     | Success (s1,undoList1) -> 
      match f s1 with 
      | Failure e -> 
       // undo everything in reverse order 
       undoList1 |> List.rev |> List.iter (fun undo -> undo()) 
       // return the error 
       Failure e 
      | Success (s2,undoList2) -> 
       // concatenate the undo lists 
       Success (s2, undoList1 @ undoList2) 

/// Example 
module LaunchWithUndo = 

    open RopWithUndo 

    let undo_refuel() = 
     printfn "undoing refuel" 

    let refuel ok = 
     if ok then 
      printfn "doing refuel" 
      Success ("refuel", [undo_refuel]) 
     else 
      Failure "refuel failed" 

    let undo_enterLaunchCodes() = 
     printfn "undoing enterLaunchCodes" 

    let enterLaunchCodes ok refuelInfo = 
     if ok then 
      printfn "doing enterLaunchCodes" 
      Success ("enterLaunchCodes", [undo_enterLaunchCodes]) 
     else 
      Failure "enterLaunchCodes failed" 

    let fireMissile ok launchCodesInfo = 
     if ok then 
      printfn "doing fireMissile " 
      Success ("fireMissile ", []) 
     else 
      Failure "fireMissile failed" 

    // test with failure at refuel 
    refuel false 
    |> bind (enterLaunchCodes true) 
    |> bind (fireMissile true) 
    (* 
    val it : Result<string> = Failure "refuel failed" 
    *) 

    // test with failure at enterLaunchCodes 
    refuel true 
    |> bind (enterLaunchCodes false) 
    |> bind (fireMissile true) 
    (* 
    doing refuel 
    undoing refuel 
    val it : Result<string> = Failure "enterLaunchCodes failed" 
    *) 

    // test with failure at fireMissile 
    refuel true 
    |> bind (enterLaunchCodes true) 
    |> bind (fireMissile false) 
    (* 
    doing refuel 
    doing enterLaunchCodes 
    undoing enterLaunchCodes 
    undoing refuel 
    val it : Result<string> = Failure "fireMissile failed" 
    *) 

    // test with no failure 
    refuel true 
    |> bind (enterLaunchCodes true) 
    |> bind (fireMissile true) 
    (* 
    doing refuel 
    doing enterLaunchCodes 
    doing fireMissile 
    val it : Result<string> = 
     Success ("fireMissile ",[..functions..]) 
    *) 

Se i risultati di ogni non possono essere annullate, una seconda opzione non è fare le cose irreversibili in ogni passaggio a tutti, ma di ritardare il bit irreversibili finché tutti i passaggi non sono OK.

In questo approccio, la custodia Success contiene un elenco di funzioni "esegui". Ogni passo che riesce aggiunge una funzione a questa lista. Alla fine, viene eseguito l'intero elenco di funzioni.

Il rovescio della medaglia è che una volta commesso, tutte le funzioni vengono eseguite (anche se si potrebbe anche concatenare quelli monadico troppo!)

Questo è fondamentalmente una versione molto rozza del modello interprete.

Ecco po 'di codice che illustra questo approccio:

/// ROP design with delayed executions 
module RopWithExec = 

    type Execute = unit -> unit 

    type Result<'success> = 
     | Success of 'success * Execute list 
     | Failure of string 

    let bind f x = 
     match x with 
     | Failure e -> Failure e 
     | Success (s1,execList1) -> 
      match f s1 with 
      | Failure e -> 
       // return the error 
       Failure e 
      | Success (s2,execList2) -> 
       // concatenate the exec lists 
       Success (s2, execList1 @ execList2) 

    let execute x = 
     match x with 
     | Failure e -> 
      Failure e 
     | Success (s,execList) -> 
      execList |> List.iter (fun exec -> exec()) 
      Success (s,[]) 

/// Example 
module LaunchWithExec = 

    open RopWithExec 

    let exec_refuel() = 
     printfn "refuel" 

    let refuel ok = 
     if ok then 
      printfn "checking if refuelling can be done" 
      Success ("refuel", [exec_refuel]) 
     else 
      Failure "refuel failed" 

    let exec_enterLaunchCodes() = 
     printfn "entering launch codes" 

    let enterLaunchCodes ok refuelInfo = 
     if ok then 
      printfn "checking if launch codes can be entered" 
      Success ("enterLaunchCodes", [exec_enterLaunchCodes]) 
     else 
      Failure "enterLaunchCodes failed" 

    let exec_fireMissile() = 
     printfn "firing missile" 

    let fireMissile ok launchCodesInfo = 
     if ok then 
      printfn "checking if missile can be fired" 
      Success ("fireMissile ", [exec_fireMissile]) 
     else 
      Failure "fireMissile failed" 

    // test with failure at refuel 
    refuel false 
    |> bind (enterLaunchCodes true) 
    |> bind (fireMissile true) 
    |> execute 
    (* 
    val it : Result<string> = Failure "refuel failed" 
    *) 

    // test with failure at enterLaunchCodes 
    refuel true 
    |> bind (enterLaunchCodes false) 
    |> bind (fireMissile true) 
    |> execute 
    (* 
    checking if refuelling can be done 
    val it : Result<string> = Failure "enterLaunchCodes failed" 
    *) 

    // test with failure at fireMissile 
    refuel true 
    |> bind (enterLaunchCodes true) 
    |> bind (fireMissile false) 
    |> execute 
    (* 
    checking if refuelling can be done 
    checking if launch codes can be entered 
    val it : Result<string> = Failure "fireMissile failed" 
    *) 

    // test with no failure 
    refuel true 
    |> bind (enterLaunchCodes true) 
    |> bind (fireMissile true) 
    |> execute 
    (* 
    checking if refuelling can be done 
    checking if launch codes can be entered 
    checking if missile can be fired 
    refuel 
    entering launch codes 
    firing missile 
    val it : Result<string> = Success ("fireMissile ",[]) 
    *) 

Si ottiene l'idea, spero. Sono sicuro che ci sono anche altri approcci - questi sono due che sono ovvi e semplici. :)

+3

Questa è stata una risposta veloce e piacevole. Non posso credere che tu abbia detto "schema" se sei stato assimilato. –

+2

la resistenza è inutile! – Grundoon

Problemi correlati