Дискриминационный союз - разрешить сопоставление с образцом, но ограничить конструкцию - PullRequest
0 голосов
/ 17 января 2019

У меня есть дискриминированный союз 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

Есть ли лучший способ обеспечить выполнение логики конструктора, которую я хотел вначале, без увеличения нагрузки где-то еще в будущем?

Ответы [ 2 ]

0 голосов
/ 17 января 2019

Вы можете иметь частные конструкторы дел, но выставлять публичные активные шаблоны с такими же именами. Вот как бы вы их определили и использовали (для краткости функции создания опущены):

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 ✔
0 голосов
/ 17 января 2019

Если нет особой причины, по которой требуется дискриминационное объединение, с учетом предоставленного вами конкретного варианта использования звучит так, как будто вы на самом деле не хотите дискриминационного объединения вообще, поскольку активный шаблон будет более полезным. Например:

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 -> // ...
| _ -> // ...
...