简体   繁体   中英

Retry Computation expression or other construct in F#

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?

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 eg 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)

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))

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."

ps But I like both Tomas's and gradbot's versions better. I just wanted to see what this type of solution might look like.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM