2017-10-05 49 views
1

我想爲這兩個表達式構建一個計算表達式。 這足以如何構建一個累積的構建器

type Result<'TSuccess> = 
| Success of 'TSuccess 
| Failure of List<string> 

type Foo = { 
    a: int 
    b: string 
    c: bool 
} 

type EitherBuilder() = 
    member this.Bind(x, f) = 
     match x with 
     | Success s -> f s 
     | Failure f -> Failure f 

     member this.Return x = Success x 

let either = EitherBuilder() 

let Ok = either { 
    let! a = Success 1 
    let! b = Success "foo" 
    let! c = Success true 
    return 
     { 
      a = a 
      b = b 
      c = c 
     } 
} 

let fail1 = either { 
    let! a = Success 1 
    let! b = Failure ["Oh nose!"] 
    let! c = Success true 
    return 
     { 
      a = a 
      b = b 
      c = c 
     } 
    } //returns fail1 = Failure ["Oh nose!"] 

但在故障(多)的情況下,簡單的我希望積累和那些如下返回失敗。

let fail2 = either { 
    let! a = Success 1 
    let! b = Failure ["Oh nose!"] 
    let! c = Failure ["God damn it, uncle Bob!"] 
    return 
     { 
      a = a 
      b = b 
      c = c 
     } 
    } //should return fail2 = Failure ["Oh nose!"; "God damn it, uncle Bob!"] 

我對如何做一個想法,通過重寫Bind始終返回Success(雖然這意味着累計erors一些額外的結構)。但是,如果我這樣做,那麼我錯過了停止信號,我總是得到返回值(實際上不是真的,因爲我會遇到運行時異常,但原則上)

+0

我,你打算什麼真實的生活場景來使用呢?你不需要一個Writer Monad嗎? – Gustavo

+0

這是非常接近現實世界的情況,除了'let! a =成功1'會成爲'let! a = someFnReturningAResult With validationFn someValue' – robkuz

+0

它在我看來,你正試圖積累驗證錯誤,如果是的話,作家Monad可能是要走的路。 – Gustavo

回答

1

由於@tomasp是說一種方法是始終以使bind正常工作提供除失敗的值。這是我在處理這個問題時一直使用的方法。然後我會改變的Result的定義,例如,這樣的:

type BadCause = 
    | Exception of exn 
    | Message of string 

type BadTree = 
    | Empty 
    | Leaf of BadCause 
    | Fork of BadTree*BadTree 

type [<Struct>] Result<'T> = Result of 'T*BadTree 

這意味着Result總是有一個值是否是好還是壞。如果BadTree爲空,則該值很好。

我偏好樹而不是列表的原因是Bind將聚合兩個單獨的結果,可能有子故障導致列表連接。

的一些功能,讓我們創建好或壞值:

let rreturn  v  = Result (v, Empty) 
let rbad  bv bt = Result (bv, bt) 
let rfailwith bv msg = rbad bv (Message msg |> Leaf) 

因爲即使是不好的結果需要以使Bind工作,我們需要通過bv參數提供值進行的值。對於支持Zero類型,我們可以創造一個舒適的方法:

let inline rfailwithz msg = rfailwith LanguagePrimitives.GenericZero<_> msg 

Bind很容易實現:

let rbind (Result (tv, tbt)) uf = 
    let (Result (uv, ubt)) = uf tv 
    Result (uv, btjoin tbt ubt) 

也就是說,我們評估兩個結果並在需要時加入壞樹。

與計算表達式生成器下面的程序:

let r = 
    result { 
     let! a = rreturn 1 
     let! b = rfailwithz "Oh nose!" 
     let! c = rfailwithz "God damn it, uncle Bob!" 
     return a + b + c 
    } 

    printfn "%A" r 

輸出:

結果(1,叉(葉(消息 「哦鼻子」),葉(消息「該死它,伯伯叔叔!「)))

即,我們得到了一個不好的值1,其原因很糟糕是因爲兩個加入的錯誤葉子。

我在使用可組合組合器轉換和驗證樹結構時使用了這種方法。在我看來,重要的是讓所有驗證失敗,而不僅僅是第一次。這意味着需要評估Bind中的兩個分支,但爲了做到這一點,我們必須始終有一個值才能撥打ufBind t uf

如OP:自己的答案,我做了實驗Unchecked.defaultof<_>,但我放棄了例如由於字符串的默認值是null並調用uf時,通常會導致崩潰。我確實創建了一張地圖Type -> empty value,但在我的最終解決方案中,我構建一個糟糕的結果時需要一個不好的值。

希望這有助於

完整的示例:

type BadCause = 
    | Exception of exn 
    | Message of string 

type BadTree = 
    | Empty 
    | Leaf of BadCause 
    | Fork of BadTree*BadTree 

type [<Struct>] Result<'T> = Result of 'T*BadTree 

let (|Good|Bad|) (Result (v, bt)) = 
    let ra = ResizeArray 16 
    let rec loop bt = 
    match bt with 
    | Empty   ->() 
    | Leaf bc  -> ra.Add bc |> ignore 
    | Fork (l, r) -> loop l; loop r 
    loop bt 
    if ra.Count = 0 then 
    Good v 
    else 
    Bad (ra.ToArray()) 

module Result = 
    let btjoin  l r = 
    match l, r with 
    | Empty , _  -> r 
    | _  , Empty -> l 
    | _  , _  -> Fork (l, r) 

    let rreturn  v  = Result (v, Empty) 
    let rbad  bv bt = Result (bv, bt) 
    let rfailwith bv msg = rbad bv (Message msg |> Leaf) 

    let inline rfailwithz msg = rfailwith LanguagePrimitives.GenericZero<_> msg 

    let rbind (Result (tv, tbt)) uf = 
    let (Result (uv, ubt)) = uf tv 
    Result (uv, btjoin tbt ubt) 

    type ResultBuilder() = 
    member x.Bind   (t, uf) = rbind t uf 
    member x.Return  v  = rreturn v 
    member x.ReturnFrom r  = r : Result<_> 

let result = Result.ResultBuilder() 

open Result 

[<EntryPoint>] 
let main argv = 
    let r = 
    result { 
     let! a = rreturn 1 
     let! b = rfailwithz "Oh nose!" 
     let! c = rfailwithz "God damn it, uncle Bob!" 
     return a + b + c 
    } 

    match r with 
    | Good v -> printfn "Good: %A" v 
    | Bad es -> printfn "Bad: %A" es 

    0 
+0

我喜歡這種方法。唯一的事情就是最後清理結果。如果出現錯誤,您的解決方案將返回「有效」對象(+錯誤)。但是,您最好不要觸摸返回值 - 特別是如果它包含非原始對象。我可以執行以下'let res = result {...} |> sanatize',但是如果構建者可以在場景後面執行此操作,那麼將會很不錯 – robkuz

+0

使用Active Patterns更新了源代碼,以便對好/壞結果進行模式匹配。 – FuleSnabel

+0

看起來你的樹同構於一個標籤類型爲BadCause選項的非空葉標籤樹,但它對於樹本身是可選的並且每個葉都是非可選的更有意義(即,類型BadTree = BadCause的葉子| BadTree的叉子* BadTree和結果<'t> ='t *(BadTree選項)的結果',因爲你的連接操作明確地避免了放入空的子樹,但是你可以防止鍵入級別。 – kvb

4

我認爲你正在嘗試做什麼不能用單子表示。問題是Bind只能調用其餘的計算(這可能會產生更多的失敗),如果它可以得到函數參數的值。在您的例子:

let! a = Success 1 
let! b = Failure ["Oh nose!"] 
let! c = Failure ["God damn it, uncle Bob!"] 

結合不能調用開始b因爲Failure ["Oh nose!"]b不提供價值的延續。你可以使用默認值,並保持在旁邊的錯誤,但這種情況正在改變,你正在使用的結構:

Merge : F<'T1> * F<'T2> -> F<'T1 * 'T2> 
Map : ('T1 -> 'T2) -> M<'T1> -> M<'T2> 
Return : 'T -> M<'T> 

type Result<'T> = { Value : 'T; Errors : list<string> } 

,你需要有你可以寫這個使用適用函子抽象您可以通過Merge積累錯誤(如果兩個參數均表示失敗)的方式來實現所有這些方法,並且Map僅在沒有值的情況下應用計算。

在F#中編寫函數式函數有很多種方法,但沒有很好的語法,所以很可能最終會使用醜陋的自定義運算符。

1

最終以上面提到的@tomas的提示,我可以使用這種解決方案,它保留了數據類型,但創建了一個有狀態的構建器。

現在唯一的問題仍然是我的線程安全 - 我會承擔是的。也許有人可以證實?

type Result<'TSuccess> = 
    | Success of 'TSuccess 
    | Failure of List<string> 

type Foo = { 
    a: int 
    b: string 
    c: bool 
} 

type EitherBuilder (msg) = 
    let mutable errors = [msg] 
    member this.Bind(x, fn) = 
     match x with 
     | Success s -> fn s 
     | Failure f -> 
      errors <- List.concat [errors;f] 
      fn (Unchecked.defaultof<_>) 

    member this.Return x = 
     if List.length errors = 1 then 
      Success x 
     else 
      Failure errors 

let either msg = EitherBuilder (msg) 

let Ok = either("OK") { 
    let! a = Success 1 
    let! b = Success "foo" 
    let! c = Success true 
    return 
     { 
       a = a 
       b = b 
       c = c 
     } 
} 

let fail1 = either("Fail1") { 
    let! a = Success 1 
    let! b = Failure ["Oh nose!"] 
    let! c = Success true 
    return 
     { 
       a = a 
       b = b 
       c = c 
     } 
} //returns fail1 = Failure ["Fail1"; "Oh nose!"] 


let fail2 = either("Fail2") { 
    let! a = Success 1 
    let! b = Failure ["Oh nose!"] 
    let! c = Failure ["God damn it, uncle Bob!"] 
    return 
     { 
       a = a 
       b = b 
       c = c 
     } 
} //should return fail2 = Failure ["Fail2"; "Oh nose!"; "God damn it, uncle Bob!"] 
+0

'Unchecked.defaultof <_>'可能會導致崩潰,對於非值類型,默認值爲null,大多數F#代碼都不期望。 'LanguagePrimitives.GenericZero <_>'更好,但需要'inline',並且類型有一個靜態成員'Zero'。如果你在'Bind'結尾加入列表,你不需要全局狀態。 – FuleSnabel

+0

我不明白爲什麼'Unchecked.defaultof <_>'比'GenericZero'更危險。如果發生故障,我不應該訪問'return'value。如果我確實有一個非原始值,'GenericZero'也不會工作 – robkuz

+0

'GenericZero'適用於任何類型爲靜態成員Zero的類型,如下所示:'type X = X with int with static member Zero = X 0' then' LanguagePrimitives.GenericZero 'returns'X 0' where'Unchecked.defaultOf ''null' – FuleSnabel