2011-05-09 23 views
3

我希望能夠在F#中編寫一個計算表達式,如果它引發異常,它將能夠重試一個操作。現在我的代碼如下所示:重試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. 

我明白單子,在粗時尚,是包裝類,但我希望解決的辦法。我知道我可以重載一個操作符,並有一個非常簡潔的語法來將操作轉換爲Retryable,但對我來說,這只是使重複/包裝更簡潔;它仍然在那裏。我可以將每個函數都包裝爲一個Retryable <'T>,但我再次看不到在頂部完成所做的事情的價值(在每個操作中調用重試,至少非常明確)。

也許計算表達式在這裏是錯誤的抽象,我不確定。關於在這裏可以做什麼的任何想法?

+0

聽起來像一個很好的用例monad對我來說。 – hammar 2011-05-09 20:08:56

+0

如果'DoThis'拋出的異常大於允許的次數,會發生什麼?它是否讓異常通過? – gradbot 2011-05-09 20:23:19

+0

是的,它只是拋出。 – ProgrammerByDay 2011-05-09 20:26:30

回答

6

計算表達式有一些擴展(除了標準的monadic特性),這給你一個很好的方法來做到這一點。

正如你所說,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的定義。它不是一個單子,因爲它沒有定義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) 
+0

這是一個巧妙的把戲。我假設,如果我想將這種行爲封裝在一個計算表達式中,我需要定義每個函數將返回的Monadic類型(或者爲它們定義包裝函數以返回M <'T>)。這太糟糕了,沒有辦法使用let!做!這裏。 – ProgrammerByDay 2011-05-09 21:27:31

2

一個簡單的函數可以工作。

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

我同意100%簡單的重試功能將正是需要的。爲什麼使用箱子手套來計數牙籤? F#不像Haskel那樣受限制。無需在任何地方使用monads ... – Liviu 2014-05-13 20:55:58

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的版本。我只是想看看這種類型的解決方案可能是什麼樣子。