2017-02-14 61 views
7

如何通過專用功能強制創建歧視聯盟價值?如何通過專用功能強制創建歧視聯盟價值?

意圖:

我想依靠創建模式,以產生具有唯一有效的數據結構。

因此,我相信我需要通過使其成爲只讀來限制DU值的使用。然而,我不明白如何實現這一點。

module File1 = 

    type EmailAddress = 
     | Valid of string 
     | Invalid of string 

    let createEmailAddress (address:System.String) = 
     if address.Length > 0 
     then Valid address 
     else Invalid address 

module File2 = 

    open File1 

    let validEmail = Valid "" // Shouldn't be allowed 

    let isValid = createEmailAddress "" 

    let result = match isValid with 
       | Valid x -> true 
       | _  -> false 

我嘗試以下:

type EmailAddress = 
    private 
    | Valid of string 
    | Invalid of string 

然而,設置的杜類型作爲私人場所執行圖案上創建函數的結果相匹配的能力。

+0

FWIW時,[F#組件設計準則(http://fsharp.org/specs/component-design-guidelines/#do-hide-the-representations-of-record-and-union-types如果這些類型的設計是可能進化的)同樣建議不要暴露DU類型,如果它們必然發展並且僅代表代碼的內部狀態。 –

+0

密切相關,可能重複:http://stackoverflow.com/questions/18539870/how-to-do-argument-validation-of-f-records – Daniel

回答

8

這就是立即想到的。

您可以使用活動模式來確定您想要公開爲外部世界的API的情況,然後使DU的內部表示完全保密。

這將迫使你使用公開暴露的API來創建識別聯合,但仍允許模式匹配反對的結果 - 是這樣的:

module File1 = 

    type EmailAddress = 
     private 
     | Valid of string 
     | Invalid of string 

    let createEmailAddress (address:System.String) = 
     if address.Length > 0 
     then Valid address 
     else Invalid address 

    // Exposed patterns go here 
    let (|Valid|Invalid|) (input : EmailAddress) : Choice<string, string> = 
     match input with 
     | Valid str -> Valid str 
     | Invalid str -> Invalid str 

module File2 = 

    open File1 

    let validEmail = Valid "" // Compiler error 

    let isValid = createEmailAddress "" // works 

    let result = // also works 
     match isValid with 
     | Valid x -> true 
     | _  -> false 

請注意,如果您使用相同的模式名,你可能需要添加上面顯示的相當令人討厭的類型註釋 - 如果File2模塊不存在,則需要這些來防止編譯器錯誤 - 如果您在庫中公開API但不使用它,這可能是相關的。如果你使用不同的模式名稱,那顯然不是問題。

+1

哇!第一次我看到使用活動模式;-) – robkuz

+1

什麼是'選擇<字符串,字符串>的? –

+0

@FyodorSoikin活動模式的結果是由每個案例中包含的類型參數化的「選擇」。由於「有效」和「無效」名稱的重用,我必須在類型註釋中明確這一點。 – TheInnerLight

2

正如您發現的,在模式匹配中使用的DU值名稱(在您的示例中爲ValidInvalid)也是這些相應情況的構造函數。不可能做你要求的東西,隱藏一個而暴露另一個。需要一種不同的方法。

一種做法是做什麼安東Schwaighofer建議,並嵌入專用的模塊內您的電子郵件地址的所有可能的操作:

module EmailAddress = 

    type EmailAddress = 
     private 
     | Valid of string 
     | Invalid of string 

    let createEmailAddress (address:System.String) = 
     if address.Length > 0 
     then Valid address 
     else Invalid address 

    let isValid emailAddress = 
     match emailAddress with 
     | Valid _ -> true 
     | Invalid _ -> false 

    // Deliberately incomplete match in this function 
    let extractEmailOrThrow (Valid address) = address 

    let tryExtractEmail emailAddress = 
     match emailAddress with 
     | Valid s -> Some s 
     | Invalid _ -> None 

參見Scott Wlaschin的「同類型的設計」系列,尤其是http://fsharpforfunandprofit.com/posts/designing-with-types-more-semantic-types/(以及他在末尾引用的gist)。我真的很推薦從這個系列的開頭開始閱讀,但我已經把最相關的一個聯繫起來了。

...我會建議不同的方法,這就是要求爲什麼要強制使用這些構造函數。你是否正在編寫一個通用程序庫的程序員,他們不能被信任遵循方向並使用構造函數?你是專門爲自己寫作的,但你不相信自己遵循你自己的方向? ...你是否正在爲合理能力的程序員編寫一個庫,他們將閱讀代碼頂部的註釋,並實際使用您提供的構造函數?

如果是這樣,那麼沒有特別需要強制隱藏DU名稱。就像這樣記錄DU:

module EmailAddress = 

    /// Do not create these directly; use the `createEmailAddress` function 
    type EmailAddress = 
     | Valid of string 
     | Invalid of string 

    let createEmailAddress (address:System.String) = 
     if address.Length > 0 
     then Valid address 
     else Invalid address 

然後繼續寫下你的其他代碼。擔心讓你的模型第一個,那麼你可以擔心其他程序員是否會錯誤地使用你的代碼。

1

這真的取決於你想要做什麼。一種方法是將狀態作爲成員函數公開,並對其執行操作。這適用於你的情況,但可能會因3個或更多值構造函數而變得繁瑣。

type EmailAddress = 
    private 
    | Valid of string 
    | Invalid of string 
with 
    member this.IsValid() = 
     match this with 
      | Valid _ -> true 
      | _ -> false 
    member this.IsInvalid() = not <| this.IsValid() 

或者你添加一個特殊map功能

member this.Map (success, error) = 
     match this with 
      | Valid x -> Valid (success x) 
      | Invalid x -> Invalid (error x) 
0

添加到什麼accepted answer所暗示的,以及它批評家試圖反駁,我的印象是,通常是有沒有必要類型的註釋。如果你真的考慮隱藏歧視工會表示二進制兼容的API按照F# Component Design Guidelines,簡約和通用而完整的再現看起來是這樣的:

module Foo = 
    type 'a Foo = 
     private | Bar of 'a 
     | Fred of string 
    let mkBar a = Bar a 
    let mkFred<'a> s : 'a Foo = Fred s 
    let (|Bar|Fred|) = function 
    | Bar a -> Bar a 
    | Fred s -> Fred s 

聯盟的情況下構造BarFred無法訪問外部模塊Foo,並由功能加倍作爲驗證鉤子。對於消費者,我們有主動識別器BarFred

let bar = Foo.mkBar 42 
let fred = Foo.mkFred<int> "Fred" 

[Foo.mkBar 42; Foo.mkFred "Fred"] 
|> List.filter (function Foo.Bar _ -> true | _ -> false)