2017-02-14 63 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)