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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With