Ответ 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 ",[])
*)
Надеюсь, вы получите эту идею. Я уверен, что есть и другие подходы - это два, которые очевидны и просты.:)