Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Discriminated Union - Allow Pattern Matching but Restrict Construction

I have an F# Discriminated Union, where I want to apply some "constructor logic" to any values used in constructing the union cases. Let's say the union looks like this:

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

Now, I want to apply some logic to the values that are actually passed-in to ensure that they are valid. In order to make sure I don't end up dealing with ValidValue instances that aren't really valid (haven't been constructed using the validation logic), I make the constructors private and expose a public function that enforces my logic to construct them.

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"

This works, allowing me to enforce the validation logic and make sure every instance of ValidValue really is valid. However, the problem is that no one outside of this module can pattern-match on ValidValue to inspect the result, limiting the usefulness of the Discriminated Union.

I would like to allow outside users to still pattern-match and work with the ValidValue like any other DU, but that's not possible if it has a private constructor. The only solution I can think of would be to wrap each value inside the DU in a single-case union type with a private constructor, and leave the actual ValidValue constructors public. This would expose the cases to the outside, allowing them to be matched against, but still mostly-prevent the outside caller from constructing them, because the values required to instantiate each case would have private constructors:

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"

Now the caller can match against the cases of ValidValue, but they can't read the actual integer and string values inside the union cases, because they're wrapped in types that have private constructors. This can be fixed with value functions for each type:

module VInt =
    let value (VInt i) = i

module VString =
    let value (VString s) = s

Unfortunately, now the burden on the caller is increased:

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

Is there a better way to enforce the execution of the constructor logic I wanted at the beginning, without increasing the burden somewhere else down the line?

like image 428
Aaron M. Eshbach Avatar asked Jan 17 '19 15:01

Aaron M. Eshbach


2 Answers

You can have private case constructors but expose public active patterns with the same names. Here's how you would define and use them (creation functions omitted for brevity):

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 ✔
like image 177
TheQuickBrownFox Avatar answered Sep 20 '22 17:09

TheQuickBrownFox


Unless there's a particular reason that a discriminated union is required, given the particular use case you've provided it sounds like you don't actually want a discriminated union at all since an active pattern would be more useful. For example:

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

At that point, callers can match and be assured that the logic has been applied.

match someValue with
| ValidInt x -> // ...
| _ -> // ...
like image 23
Chris Hannon Avatar answered Sep 23 '22 17:09

Chris Hannon