如何通过专用功能强制创建Discriminated Union值?

时间:2017-02-14 15:43:57

标签: f#

如何通过专用功能强制创建Discriminated Union值?

意图:

我想依靠Creational Patterns来生成只有有效数据的结构。

因此,我认为我需要通过将DU值设为只读来限制使用DU值。但是,对我来说,如何实现这一目标并不明显。

while semaphore.wait(timeout: DispatchTime.now() + Double(5000000000) / Double(NSEC_PER_SEC)) == DispatchTimeoutResult.success {//time out set to 5 seconds
    print("wait")
}

我尝试了以下内容:

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

但是,将DU类型设置为私有会破坏在创建函数的结果上执行模式匹配的能力。

4 个答案:

答案 0 :(得分:9)

这正是人们想到的。

您可以使用活动模式来确定要作为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 :(得分:2)

正如您所发现的,模式匹配中使用的DU值名称(示例中为ValidInvalid)也是这些案例的构造函数。你不可能做你想要的,隐藏一个并暴露另一个。需要采用不同的方法。

一种方法可能是做Anton 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)。我真的建议从系列的开头阅读,但我已经链接了最相关的一个。

但是 ...我建议采用不同的方法,即要求 为什么 强制使用这些构造函数。您是否正在编写一个通用程序库供一般用途的程序员使用,他们不能信任遵循指示并使用您的构造函数?你是在为自己写作,但是你不相信你自己遵循你自己的指示? OR ...您是否为合理能力的程序员编写了一个库,他们将阅读代码顶部的注释并实际使用您提供的构造函数?

如果是这样,那么就没有必要强制隐藏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

然后继续编写其余的代码。担心让你的模型正确首先,然后你可以担心其他程序员是否会错误地使用你的代码。

答案 2 :(得分: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)

答案 3 :(得分:0)

添加accepted answer所暗示的内容,以及其评论者试图反驳的内容,我的印象是通常不需要类型注释。如果您真的考虑按照F# Component Design Guidelines隐藏二进制兼容API 的歧视联合的表示,那么简约且通用但完整的复制可能如下所示:

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

Union案例构造函数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)