可辨联合——允许模式匹配但限制构造
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 ✔
我有一个 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 ✔