Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I enforce the creation of a Discriminated Union value through a dedicated function?

Tags:

f#

How can I enforce the creation of a Discriminated Union value through a dedicated function?

Intent:

I want to rely on Creational Patterns to produce structures having valid data only.

Therefore, I believe that I will need to restrict the use of a DU value by making it read-only. However, it's not obvious to me how to accomplish that.

module File1 =

    type EmailAddress = 
        | Valid   of string 
        | Invalid of string

    let createEmailAddress (address:System.String) =
        if address.Length > 0
        then Valid    address 
        else Invalid  address

module File2 =

    open File1

    let validEmail = Valid "" // Shouldn't be allowed

    let isValid = createEmailAddress ""

    let result = match isValid with
                 | Valid x -> true
                 | _       -> false

I tried the following:

type EmailAddress =
    private
    | Valid   of string 
    | Invalid of string

However, setting the DU type as private breaks the ability to perform pattern matching on the result of the creation function.

like image 263
Scott Nimrod Avatar asked Feb 14 '17 15:02

Scott Nimrod


2 Answers

This is just what springs to mind immediately.

You could use an active pattern to determine the cases you want to expose as an API to the outside world and then keep the internal representation of the DU completely private.

This would force you to use the publically exposed API to create the discriminated union but still allow pattern matching against the result - something like this:

module File1 =

    type EmailAddress = 
        private
        | Valid   of string 
        | Invalid of string

    let createEmailAddress (address:System.String) =
        if address.Length > 0
        then Valid    address 
        else Invalid  address

    // Exposed patterns go here
    let (|Valid|Invalid|) (input : EmailAddress) : Choice<string, string>  = 
        match input with
        | Valid str -> Valid str
        | Invalid str -> Invalid str

module File2 =

    open File1

    let validEmail = Valid "" // Compiler error

    let isValid = createEmailAddress "" // works

    let result = // also works
        match isValid with
        | Valid x -> true
        | _       -> false

Note that if you use the same pattern names, you may have to add the rather nasty type annotations shown above - these would be required to prevent a compiler error if the File2 module were not present - this could be relevant if you are exposing an API in a library but not making use of it. If you use different pattern names, that's obviously not an issue.

like image 117
TheInnerLight Avatar answered Oct 16 '22 05:10

TheInnerLight


As you've discovered, the DU value names (Valid and Invalid in your example), used in pattern matches, are also the constructors of those respective cases. It is not possible to do what you're asking for, to hide one and expose the other. A different approach is needed.

One approach might be to do what Anton Schwaighofer suggests, and embed all the possible operations on your email addresses inside a dedicated module:

module EmailAddress =

    type EmailAddress =
        private
        | Valid   of string 
        | Invalid of string

    let createEmailAddress (address:System.String) =
        if address.Length > 0
        then Valid    address 
        else Invalid  address

    let isValid emailAddress =
        match emailAddress with
        | Valid _ -> true
        | Invalid _ -> false

    // Deliberately incomplete match in this function
    let extractEmailOrThrow (Valid address) = address

    let tryExtractEmail emailAddress =
        match emailAddress with
        | Valid s -> Some s
        | Invalid _ -> None

See Scott Wlaschin's "Designing with types" series, and in particular http://fsharpforfunandprofit.com/posts/designing-with-types-more-semantic-types/ (and the gist he references at the end of that). I'd really recommend reading from the beginning of the series, but I've linked the most relevant one.

BUT... I would suggest a different approach, which is to ask why you want to enforce the use of those constructor functions. Are you writing a library for general-purpose use by beginning programmers, who can't be trusted to follow the directions and use your constructor function? Are you writing just for yourself, but you don't trust yourself to follow your own directions? OR... are you writing a library for reasonably-competent programmers who will read the comment at the top of the code and actually use the constructor functions you've provided?

If so, then there's no particular need to enforce hiding the DU names. Just document the DU like so:

module EmailAddress =

    /// Do not create these directly; use the `createEmailAddress` function
    type EmailAddress =
        | Valid   of string 
        | Invalid of string

    let createEmailAddress (address:System.String) =
        if address.Length > 0
        then Valid    address 
        else Invalid  address

Then go ahead and write the rest of your code. Worry about getting your model right first, then you can worry about whether other programmers will use your code wrong.

like image 36
rmunn Avatar answered Oct 16 '22 04:10

rmunn