使用Either monad时如何处理回滚("铁路导向编程")

Oen*_*ria 19 f# haskell functional-programming

我正在使用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
Run Code Online (Sandbox Code Playgroud)

Gru*_*oon 19

正如人们在评论中指出的那样,有几个选项可以用来解决这个问题.

一种方法是使用补偿交易.

在这种方法中,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..])
    *)
Run Code Online (Sandbox Code Playgroud)

如果每个步骤的结果都无法撤消,则第二个选项根本不是在每个步骤中做不可逆转的事情,而是延迟不可逆的比特直到所有步骤都正常.

在这种方法中,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 ",[])
    *)
Run Code Online (Sandbox Code Playgroud)

我希望你能得到这个想法.我确信还有其他方法 - 这两种方法显而易见且简单.:)

  • 这是一个快速,好的答案.我不敢相信你说'模式'你被同化了. (3认同)
  • 抵抗是徒劳的! (2认同)