【问题标题】:How to deal with rollbacks when using the Either monad ("railway-oriented programming")使用 Either monad 时如何处理回滚(“面向铁路的编程”)
【发布时间】:2016-07-31 20:35:40
【问题描述】:

我正在使用 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

【问题讨论】:

  • 结果是否足以回滚?我可能会选择累积实际回滚操作的结果类型,因此在任何步骤中,值都是成功/失败的元组(就像现在一样)和回滚函数() -&gt; ()
  • 很遗憾,您将此作为功能性问题提出。立即想到使用 Prolog 或 inferencing。您可以在 F# 中实现推理,而且效果很好。
  • Ganesh:嗯,这是一个有趣的想法。我会玩一玩,看看效果如何!
  • 更详细地阅读本文并思考它的副作用是什么。它们都可以回滚吗?仍然使用推理的另一种方法是一开始不做副作用,而是积累导致成功的动作。如果您成功了,那么您执行累积的操作以获得成功,如果没有成功,则无需回滚。或者甚至放弃推理,如果最终成功,则执行累积的操作。
  • Guy:在我目前的情况下,副作用是:1)向第三方 API 发出 POST 请求,我可以通过发送适当的 DELETE 请求来回滚。 2)将文件写入磁盘(通过删除回滚) 3)写入数据库(再次,易于恢复)但我认为这是普遍遇到的问题。我很惊讶没有更多关于它的文章!除了在大学里玩了大约两周之外,我对序言或推理知之甚少,但我会做一些阅读。

标签: haskell f# functional-programming


【解决方案1】:

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

一种方法是使用compensating transactions

在这种方法中,Success 案例包含一个“撤消”函数列表。每个可以撤消的步骤都会在此列表中添加一个功能。 当任何步骤失败时,列表中的每个撤消函数都会执行(以相反的顺序)。

当然有更复杂的方法可以做到这一点(例如,在崩溃的情况下持久存储撤消功能, 或this kind of thing)。

下面是一些演示这种方法的代码:

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

我希望你明白了。我敢肯定还有其他方法——这是两种显而易见且简单的方法。 :)

【讨论】:

  • 这很快,很好的答案。我不敢相信你说pattern你被同化了。
  • 抵抗是徒劳的!
猜你喜欢
  • 1970-01-01
  • 2019-10-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-12-03
  • 2016-06-26
  • 1970-01-01
  • 2021-09-04
相关资源
最近更新 更多