我希望能够在 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 中定义了这些次数。
我认为计算表达式可以在这里帮助我,但我看不出它如何帮助我删除将每个右侧显式包装到 Retryable<'T 的内容>
我可以看到计算表达式看起来像这样:
let! x = Retryable( fun() -> GetResourceX())
etc.
我知道Monads以一种粗略的方式是包装类型,但我希望有办法解决这个问题。我知道我可以重载运算符,并且有一个非常简洁的语法将操作转换为可重试<'T>,但对我来说,这只是使重复/包装更加简洁;它仍然在那里。我可以将每个函数包装为可重试<'T>,但再一次,我看不到在帖子顶部执行所做的事情的价值(在每个操作上调用重试。至少它非常明确)。
也许计算表达式在这里是错误的抽象,我不确定。关于这里可以做什么的任何想法?
计算表达式有一些扩展(除了标准的一元特征之外),这为您提供了一种很好的方法来执行此操作。
正如你所说,monads 本质上是包装器(例如创建 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)
一个简单的函数就可以工作。
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))
这是在单个计算表达式中执行此操作的第一次尝试。 但请注意,这只是第一次尝试;我还没有彻底测试过它。 此外,在计算表达式中重新设置尝试次数时,它有点丑陋。 我认为语法可以在这个基本框架内清理一下。
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."
附言但我更喜欢托马斯和格拉德博特的版本。 我只是想看看这种类型的解决方案会是什么样子。