У меня есть дискриминированный союз F #, где я хочу применить некоторую «логику конструктора» к любым значениям, используемым при построении случаев объединения. Допустим, объединение выглядит так:
type ValidValue =
| ValidInt of int
| ValidString of string
// other cases, etc.
Теперь я хочу применить некоторую логику к значениям, которые фактически передаются, чтобы убедиться, что они действительны. Чтобы убедиться, что я не буду иметь дело с ValidValue
экземплярами, которые не являются действительно допустимыми (не были созданы с использованием логики валидации), я делаю конструкторы частными и открываю открытую функцию, которая применяет мою логику к построить их.
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
для проверки результата, что ограничивает полезность Дискриминационного союза.
Я бы хотел разрешить внешним пользователям сопоставлять с шаблоном и работать с ValidValue
, как и с любым другим DU, но это невозможно, если у него есть частный конструктор. Единственное решение, которое я могу придумать, - это обернуть каждое значение внутри DU в единый тип объединения с закрытым конструктором и оставить фактические конструкторы ValidValue
открытыми. Это позволило бы раскрыть случаи снаружи, что позволило бы их сопоставить, но все же, в основном, предотвратило бы их создание внешним вызывающим объектом, поскольку значения, необходимые для создания экземпляра каждого случая, имели бы частные конструкторы:
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
Есть ли лучший способ обеспечить выполнение логики конструктора, которую я хотел вначале, без увеличения нагрузки где-то еще в будущем?