可辨联合——允许模式匹配但限制构造

Discriminated Union - Allow Pattern Matching but Restrict Construction

我有一个 F# 区分联合,我想在其中应用一些 "constructor logic" 到用于构造联合案例的任何值。假设联合看起来像这样:

type ValidValue =
| ValidInt of int
| ValidString of string
// other cases, etc.

现在,我想对实际传入的值应用一些逻辑以确保它们有效。为了确保我不会最终处理不是真正有效的 ValidValue 实例(尚未使用验证逻辑构建),我将构造函数设为私有并公开一个 public强制执行我的逻辑来构造它们的函数。

type ValidValue = 
    private
    | ValidInt of int
    | ValidString of string

module ValidValue =
    let createInt value =
        if value > 0 // Here's some validation logic
        then Ok <| ValidInt value
        else Error "Integer values must be positive"

    let createString value =
        if value |> String.length > 0 // More validation logic
        then Ok <| ValidString value
        else Error "String values must not be empty"

这有效,允许我执行验证逻辑并确保 ValidValue 的每个实例确实有效。然而,问题在于此模块之外的任何人都无法在 ValidValue 上进行模式匹配以检查结果,从而限制了可区分联合的实用性。

我想允许外部用户仍然像任何其他 DU 一样进行模式匹配和使用 ValidValue,但如果它有一个私有构造函数,那是不可能的。我能想到的唯一解决方案是将 DU 中的每个值包装在一个带有私有构造函数的单例联合类型中,并保留实际的 ValidValue 个构造函数 public。这会将案例暴露给外部,允许它们进行匹配,但仍然主要阻止外部调用者构造它们,因为实例化每个案例所需的值将具有私有构造函数:

type VInt = private VInt of int
type VString = private VString of string

type ValidValue = 
| ValidInt of VInt
| ValidString of VString

module ValidValue =
    let createInt value =
        if value > 0 // Here's some validation logic
        then Ok <| ValidInt (VInt value)
        else Error "Integer values must be positive"

    let createString value =
        if value |> String.length > 0 // More validation logic
        then Ok <| ValidString (VString value)
        else Error "String values must not be empty"

现在调用者可以匹配 ValidValue 的情况,但它们无法读取联合情况中的实际整数和字符串值,因为它们被包装在具有私有构造函数的类型中。这可以通过每种类型的 value 函数来解决:

module VInt =
    let value (VInt i) = i

module VString =
    let value (VString s) = s

不幸的是,现在调用者的负担增加了:

// Example Caller
let result = ValidValue.createInt 3

match result with
| Ok validValue ->
    match validValue with
    | ValidInt vi ->
        let i = vi |> VInt.value // Caller always needs this extra line
        printfn "Int: %d" i
    | ValidString vs ->
        let s = vs |> VString.value // Can't use the value directly
        printfn "String: %s" s
| Error error ->
    printfn "Invalid: %s" error

有没有更好的方法来强制执行我一开始想要的构造函数逻辑,而不增加其他地方的负担?

除非有特殊原因需要区分联合,否则考虑到您提供的特定用例,听起来您实际上根本不需要区分联合,因为活动模式会更有用。例如:

let (|ValidInt|ValidString|Invalid|) (value:obj) = 
    match value with
    | :? int as x -> if x > 0 then ValidInt x else Invalid
    | :? string as x -> if x.Length > 0 then ValidString x else Invalid
    | _ -> Invalid

届时,调用方可以匹配并确保已应用逻辑。

match someValue with
| ValidInt x -> // ...
| _ -> // ...

您可以拥有私有案例构造函数,但公开 public 具有相同名称的活动模式。以下是您将如何定义和使用它们(为简洁起见省略了创建函数):

module Helpers =
    type ValidValue = 
        private
        | ValidInt of int
        | ValidString of string

    let (|ValidInt|ValidString|) = function
        | ValidValue.ValidInt i -> ValidInt i
        | ValidValue.ValidString s -> ValidString s

module Usage =
    open Helpers

    let validValueToString = function
        | ValidInt i -> string i
        | ValidString s -> s
    //  Easy to use ✔

    // Let's try to make our own ValidInt 
    ValidInt -1
    // error FS1093: The union cases or fields of the type
    // 'ValidValue' are not accessible from this code location
    //  Blocked by the compiler ✔