具有受保护创建和 public 读取访问权限的可区分联合的功能设计模式是什么?

What's the functional design pattern for a discriminated union with protected creation, and public read access?

区分联合通常用作数据持有者并提供有关它们所持有内容的信息,但有时我发现自己需要防止创建区分联合,但仍然能够使用它进行模式匹配熟悉的语法。

为了争论,假设我们用字符串表示一个 URI,但我想创建一个具有保证验证 URI 的类型(即,它根据 RFC 有效),它也是一个字符串。仅使用 Some/None 在这里不起作用,因为我仍然想访问任何无效的字符串。此外,我喜欢对当前代码库进行温和的重构体验(在多行代码上用新的单案例联合替换现有的单案例联合比使用多案例联合要容易得多)。

我可以按如下方式解决这个问题,我认为这说明了我打算做什么(为简单起见,省略了错误案例):

[<AutoOpen>]
module VerifiedUriModule =
    module VerifiedUri =
        type VerifiedUri = 
            private 
            | VerifiedUri of string

        let create uri = VerifiedUri uri  // validation and error cases go here

        let tryCreate uri = Some <| VerifiedUri uri  // or here

        let get (VerifiedUri uri) = uri

    let (|VerifiedUri|) x =
        VerifiedUri.get x

AutoOpen 的额外级别只是为了允许使用活动识别器的不合格访问。

我可能最终会使用典型的 Result 类型,但我想知道这是否是典型的编码实践,或者每当我发现自己在做这样的事情时,我的脑海中是否应该听到一个声音说 "rollback, rollback!",因为我违反了 class 函数式编程原则(是吗?)。

我意识到这是一个信息隐藏的案例,它看起来很像用数据模仿 OO class 行为。典型的 F#ish 方法是什么(除了使用私有 ctor 创建 class)?


编辑 2019-12-10:此问题 is now being discussed for inclusion in F# 作为语言功能。如果你认为它应该在:).

在相当普遍的意义上,我认为您所描述的模式是抽象数据类型 - 这不是特定 F# 实现的名称,但它符合您的描述很好

引用 Barbara Liskov 和 Stephen Zilles 在 1974 年的 Programming with Abstract Data Types

An abstract data type defines a class of abstract objects which is completely characterized by the operations available on those objects. This means that an abstract data type can be defined by defining the characterizing operations for that type.

在您的示例中,您定义了一个抽象数据类型 VerifiedUrl,它由三个操作描述。操作 create(或 tryCreate)创建一个抽象数据类型的值,操作 get 允许您获取该值。创建值的操作还捕获了这样一个事实,即您只能从有效的 URL 字符串创建 VerifiedUrl

此模式可能更侧重于隐藏实现细节并仅公开某些用于操作它的操作这一事实 - 而在您的情况下,另一个重要的事实是抽象数据类型的值满足某些属性 - 但您可以将它们视为关于抽象数据类型的不变量。我想不出一个更好的概念来捕捉这个想法。

好吧,很快:你不会隐藏你的选择。您只需确保它们足够。并且您提供适当的签名函数来映射类型。

现在,更长的版本: 单一职责 (TM) 也适用于此。具体联合类型 必须 专用于具体问题。在你的情况下,有 type VerificationStamp = Verified ... | NotYetVerified | YOU_SHALL_NOT_PASS 选项似乎是合理的。而且您不会隐藏它们:没有充分的理由这样做。然后,您定义 verify 函数以及您希望提供给代码客户的其他函数。这是您保持正确的地方:通过将您的功能绑定到合理的类型;例如,verify 肯定会采用原始​​的 string 而不是包装在容器中的;但它会 return VerificationStamp "myUrl".