Question

I am trying to use the Retry Monad I took from our beloved stack overflow:

type RetryBuilder(max, sleep : TimeSpan) = 
      member x.Return(a) = a
      member x.Delay(f) = f
      member x.Zero() = failwith "Zero"
      member x.Run(f) =
        let rec loop(n) = 
            if n = 0 then failwith "Failed"
            else 
                try f() 
                with ex -> 
                    sprintf "Call failed with %s. Retrying." ex.Message |> printfn "%s"
                    Thread.Sleep(sleep); 
                    loop(n-1)
        loop max

I would like to use it to make my file copy code a bit more robust:

let retry = RetryBuilder(3, TimeSpan.FromSeconds(1.))
retry {
    System.IO.File.Move("a", "b")
}

Now I noticed that it sometimes fails with "Zero" exception. I tried to remove the member x.Zero() = failwith "Zero" but now I get a compile time error:

This construct can be only used if builder defines a 'Zero' method.

Any ideas how to proceed?

Was it helpful?

Solution 2

It looks like the simplest fix is to return a value at the end:

retry {
    System.IO.File.Move("a", "b")
    return ()
}

If you look at how computation expressions are de-sugared your code seems to be converted into

retry.Run(retry.Delay(fun () -> System.IO.File.Move("a", "b"); retry.Zero()))

this causes the exception to be thrown during the evaluation. If you return a value this will not happen.

OTHER TIPS

Lee suggested that you can use return () at the end of computations that would otherwise throw, because they call the Zero member. This is a good trick - but you can actually integrate this directly into the computation builder.

The Zero member is used when your computation ends without returning. You can change it to do the same thing as return ():

type RetryBuilder(max, sleep : TimeSpan) = 
  member x.Return(a) = ...
  member x.Zero() = x.Return( () )

Then you can just write the original code and you will get unit result back:

let retry = RetryBuilder(3, TimeSpan.FromSeconds(1.))
retry {
  System.IO.File.Move("a", "b")
}

Firstly there need to be a type annotation to your x.Run function to get the compiler to be happy, since the File.Move takes unit and returns unit. Like so:

open System
open System.Threading
type RetryBuilder(max, sleep : TimeSpan) = 
      member x.Return(a) = a
      member x.Delay(f) = f
      member x.Zero() = failwith "Zero"
      member x.Run(f : unit -> unit) =
        let rec loop(n) = 
            if n = 0 then failwith "Failed"
            else 
                try 
                    f() 
                with ex -> 
                    sprintf "Call failed with %s. Retrying." ex.Message |> printfn "%s"
                    Thread.Sleep(sleep); 
                    loop(n-1)
        loop max

Then having a look at the documentation on the Zero() function we see "Called for empty else branches of if...then expressions in computation expressions." So that explains why the compiler requires that you have the Zero present in the computation expression. Then if we set a break point on the 'if' and watch it execute we see that the file move returns unit so the else has nothing to return, thus it calls Zero. So that explains why you get the Zero popping up when the move succeeds (and then retries when it fails because the file has been moved and no longer exists).

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top