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

3

我希望能够在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)

我需要能够对每个函数进行一定次数的重试,这些次数已在其他地方定义好。我考虑使用计算表达式来实现,但我不知道如何避免显式地将每个右侧包装到Retryable<'T>中。
我可以想象计算表达式看起来像这样:
let! x = Retryable( fun() -> GetResourceX())
etc.

我知道Monad在某种程度上是包装类型,但我希望有一种绕过它的方法。我知道我可以重载运算符,并且有一个非常简洁的语法来将操作转换为Retryable<'T>,但对我来说,这只是使重复/包装更加简洁;它仍然存在。我可以将每个函数封装为Retryable<'T>,但再次,我不认为它比在帖子顶部所做的事情(对每个操作调用retry。至少非常明确)有价值。
也许计算表达式在这里是错误的抽象,我不确定。对于这里可能有什么解决方案吗?

对我来说,这似乎是单子的一个很好的用例。 - hammar
如果DoThis抛出的异常次数超过您允许的次数会发生什么?它会让异常通过吗? - gradbot
3个回答

6
计算表达式除了标准单子特性之外,还有一些扩展功能,可以让您以更好的方式完成此操作。正如您所说,单子本质上是包装器(例如创建 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构造器的定义。它并不是真正的单子,因为它没有定义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)

这是一个不错的技巧。我想如果要将这个行为封装在一个计算表达式中,就需要定义一个每个函数都返回的单调类型(或者定义包装函数使它们返回 M<'T>)。很遗憾这里无法使用 let! 和 do!。 - ProgrammerByDay

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

我完全同意,一个简单的重试函数就是所需的。为什么要用拳击手套来数牙签呢?F#并不像Haskel那样受限制。没有必要到处使用单子... - Liviu

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

附言:不过我更喜欢Tomas和gradbot的版本。 我只是想看看这种解决方案可能会是什么样子。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接