问题
I want to be able to write a computation expression in F# that will be able to retry an operation if it throws an exception. Right now my code looks like:
let x = retry (fun() -> GetResourceX())
let y = retry (fun() -> GetResourceY())
let z = retry (fun() -> DoThis(x, y))
etc. (this is obviously an astract representation of the actual code)
I need to be able to retry each of the functions a set number of times, which I have defined elswhere.
I was thinking a computation expression could help me here, but I don't see how it could help me remove explicitly wrapping each right hand side to a Retryable<'T>
I could see the computation expression looking something like:
let! x = Retryable( fun() -> GetResourceX())
etc.
I understand that Monads, in a crude fashion, are wrapper types, but I was hoping a way around this. I know I can overload an operator and have a very succinct syntax for converting an operation into a Retryable<'T>, but to me that's just making the repetition/wrapping more succinct; it's still there. I could wrap each function to be a Retryable<'T>, but once again, I don't see the value over doing what's done at the top of the post (calling retry on each operation. At least it's very explicit).
Maybe computation expressions are the wrong abstraction here, I'm not sure. Any ideas on what could be done here?
回答1:
Computation expressions have a few extensions (in addition to the standard monadic features), that give you a nice way to do this.
As you said, the monads are essentially wrappers (creating e.g. Retryable<'T>
) that have some additional behavior. However, F# computation expression can also define Run
member which automatically unwraps the value, so the result of retry { return 1 }
can have just a type int
.
Here is an example (the builder is below):
let rnd = new System.Random()
// The right-hand side evaluates to 'int' and automatically
// retries the specified number of times
let n = retry {
let n = rnd.Next(10)
printfn "got %d" n
if n < 5 then failwith "!" // Throw exception in some cases
else return n }
// Your original examples would look like this:
let x = retry { return GetResourceX() }
let y = retry { return GetResourceY() }
let z = retry { return DoThis(x, y) }
Here is the definition of the retry
builder. It is not really a monad, because it doesn't define let!
(when you use computation created using retry
in another retry
block, it will just retry the inner one X-times and the outer one Y-times as needed).
type RetryBuilder(max) =
member x.Return(a) = a // Enable 'return'
member x.Delay(f) = f // Gets wrapped body and returns it (as it is)
// so that the body is passed to 'Run'
member x.Zero() = failwith "Zero" // Support if .. then
member x.Run(f) = // Gets function created by 'Delay'
let rec loop(n) =
if n = 0 then failwith "Failed" // Number of retries exceeded
else try f() with _ -> loop(n-1)
loop max
let retry = RetryBuilder(4)
回答2:
A simple function could work.
let rec retry times fn =
if times > 1 then
try
fn()
with
| _ -> retry (times - 1) fn
else
fn()
Test code.
let rnd = System.Random()
let GetResourceX() =
if rnd.Next 40 > 1 then
"x greater than 1"
else
failwith "x never greater than 1"
let GetResourceY() =
if rnd.Next 40 > 1 then
"y greater than 1"
else
failwith "y never greater than 1"
let DoThis(x, y) =
if rnd.Next 40 > 1 then
x + y
else
failwith "DoThis fails"
let x = retry 3 (fun() -> GetResourceX())
let y = retry 4 (fun() -> GetResourceY())
let z = retry 1 (fun() -> DoThis(x, y))
回答3:
Here is a first try at doing this in a single computation expression. But beware that it's only a first try; I have not thoroughly tested it. Also, it's a little bit ugly when re-setting the number of tries within the computation expression. I think the syntax could be cleaned-up a good bit within this basic framework.
let rand = System.Random()
let tryIt tag =
printfn "Trying: %s" tag
match rand.Next(2)>rand.Next(2) with
| true -> failwith tag
| _ -> printfn "Success: %s" tag
type Tries = Tries of int
type Retry (tries) =
let rec tryLoop n f =
match n<=0 with
| true ->
printfn "Epic fail."
false
| _ ->
try f()
with | _ -> tryLoop (n-1) f
member this.Bind (_:unit,f) = tryLoop tries f
member this.Bind (Tries(t):Tries,f) = tryLoop t f
member this.Return (_) = true
let result = Retry(1) {
do! Tries 8
do! tryIt "A"
do! Tries 5
do! tryIt "B"
do! tryIt "C" // Implied: do! Tries 1
do! Tries 2
do! tryIt "D"
do! Tries 2
do! tryIt "E"
}
printfn "Your breakpoint here."
p.s. But I like both Tomas's and gradbot's versions better. I just wanted to see what this type of solution might look like.
来源:https://stackoverflow.com/questions/5941868/retry-computation-expression-or-other-construct-in-f