如何通过专用功能强制创建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类型设置为私有会破坏在创建函数的结果上执行模式匹配的能力。
答案 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值名称(示例中为Valid
和Invalid
)也是这些案例的构造函数。你不可能做你想要的,隐藏一个并暴露另一个。需要采用不同的方法。
一种方法可能是做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案例构造函数Bar
和Fred
无法从外部模块Foo
访问,并被作为验证钩子的函数替换。对于消费者,我们有主动识别器Bar
和Fred
。
let bar = Foo.mkBar 42
let fred = Foo.mkFred<int> "Fred"
[Foo.mkBar 42; Foo.mkFred "Fred"]
|> List.filter (function Foo.Bar _ -> true | _ -> false)