Как бороться с откатами при использовании любой монады ( "железнодорожно-ориентированное программирование" )

Я использую F # и Chessie для создания последовательности задач (с побочными эффектами), которые могут преуспеть или сбой.

Если что-то не удается, я хочу прекратить выполнение оставшихся задач и отменить те, которые уже выполнены.

К сожалению, когда я нахожусь на пути "сбой", больше нет способа получить результаты успешных задач, чтобы я мог их отбросить назад.

Существует ли "шаблон" функционального программирования, который имеет дело с этим сценарием?

Пример:

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

Ответы

Ответ 1

Как отметили люди в комментариях, есть несколько вариантов, которые можно использовать для решения этой проблемы.

Один из способов - использовать компенсационные транзакции.

В этом подходе случай Success содержит список функций "отменить". Каждый шаг, который можно отменить, добавляет функцию в этот список. Когда какой-либо шаг выходит из строя, каждая функция отмены в списке выполняется (в обратном порядке).

Есть более сложные способы сделать это, конечно (например, сохранение функций отмены настойчиво в случае сбоев, или такого рода вещи).

Здесь приведен код, демонстрирующий этот подход:

/// 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..])
    *)

Если результаты каждого из них не могут быть отменены, второй вариант не должен делать необратимые вещи на каждом шаге, но для задержки необратимых бит до тех пор, пока все шаги не будут выполнены.

В этом подходе случай Success содержит список функций "выполнить". Каждый успешный шаг добавляет функцию в этот список. В самом конце выполняется весь список функций.

Недостатком является то, что после того, как он был зафиксирован, все функции запущены (хотя вы также можете чередовать их также монадически!)

Это в основном очень грубая версия шаблона интерпретатора.

Здесь приведен код, демонстрирующий этот подход:

/// 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 ",[])
    *)

Надеюсь, вы получите эту идею. Я уверен, что есть и другие подходы - это два, которые очевидны и просты.:)