在F#中重试计算表达式或其他构造

时间:2011-05-09 20:02:05

标签: f# computation-expression

我希望能够在F#中编写一个计算表达式,如果它抛出异常,它将能够重试一个操作。现在我的代码看起来像:

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)

我需要能够重试每个函数一定次数,我已经定义了elswhere。

我在想一个计算表达式可以帮助我,但我不知道它如何帮助我删除明确地将每个右侧包裹到一个可重试的<'T>

我可以看到计算表达式看起来像:

let! x = Retryable( fun() -> GetResourceX())
etc.

据我所知,Monads以粗俗的方式是包装类型,但我希望能解决这个问题。我知道我可以重载一个操作符并且有一个非常简洁的语法将操作转换为Retryable&lt;'T&gt;,但对我来说这只是让重复/包装更简洁;它还在那里。我可以将每个函数包装成一个可重试的&lt;'T&gt;,但是再一次,我没有看到在帖子顶部完成所做的操作的值(在每个操作上调用retry。至少它非常明确)。< / p>

也许计算表达式在这里是错误的抽象,我不确定。关于在这里可以做些什么的任何想法?

3 个答案:

答案 0 :(得分:6)

计算表达式有一些扩展(除了标准的monadic功能之外),它们为您提供了一种很好的方法。

正如你所说,monad基本上是包装器(创建例如Retryable<'T>),它们有一些额外的行为。但是,F#计算表达式还可以定义自动展开值的Run成员,因此retry { return 1 }的结果只能是int类型。

以下是一个示例(构建器如下):

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

以下是retry构建器的定义。它实际上不是monad,因为它没有定义let!(当你在另一个retry块中使用retry创建的计算时,它只会重试内部的X次,并且根据需要,外面的Y次。)

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)

答案 1 :(得分:3)

一个简单的功能可以工作。

let rec retry times fn = 
    if times > 1 then
        try
            fn()
        with 
        | _ -> retry (times - 1) fn
    else
        fn()

测试代码。

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

答案 2 :(得分:0)

这是在单个计算表达式中首次尝试这样做。但请注意,这只是第一次尝试; 我没有彻底测试过它。此外,在重新设置计算表达式中的尝试次数时,它有点难看。我认为语法可以在这个基本框架中清理一下。

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。但我更喜欢Tomas和gradbot的版本。我只是想看看这种类型的解决方案可能是什么样的。