Create a gist now

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Examples of creating constrained types in F#
// General hints on defining types with constraints or invariants
//
// Just as in C#, use a private constructor
// and expose "factory" methods that enforce the constraints
//
// In F#, only classes can have private constructors with public members.
//
// If you want to use the record and DU types, the whole type becomes
// private, which means that you also need to provide:
// * a constructor function ("create").
// * a function to extract the internal data ("value").
//
// This is marginally annoying, but you can always use the C# approach if you like!
//
// OTOH, your opaque types really ARE opaque!
//
// The other alternative is to use signature files (which are not discussed here)
module ConstrainedTypes =
open System
// ---------------------------------------------
// Constrained String50 (FP-style)
// ---------------------------------------------
/// Type with constraint that value must be non-null
/// and <= 50 chars.
type String50 = private String50 of string
/// Module containing functions related to String50 type
module String50 =
// NOTE: these functions can access the internals of the
// type because they are in the same scope (namespace/module)
/// constructor
let create str =
if String.IsNullOrEmpty(str) then
None
elif String.length str > 50 then
None
else
Some (String50 str)
// function used to extract data since type is private
let value (String50 str) = str
// ---------------------------------------------
// Constrained String50 (object-oriented style)
// ---------------------------------------------
/// Class with constraint that value must be non-null
/// and <= 50 chars.
type OOString50 private(str) =
/// constructor
static member Create str =
if String.IsNullOrEmpty(str) then
None
elif String.length str > 50 then
None
else
Some (OOString50(str))
/// extractor
member this.Value = str
// ---------------------------------------------
// Constrained AtLeastOne (FP style)
// ---------------------------------------------
/// Type with constraint that at least one of the fields
/// must be set.
type AtLeastOne = private {
A : int option
B : int option
C : int option
}
/// Module containing functions related to AtLeastOne type
module AtLeastOne =
/// constructor
let create aOpt bOpt cOpt =
match aOpt,bOpt,cOpt with
| (Some a,_,_) -> Some <| {A = aOpt; B=bOpt; C=cOpt}
| (_,Some b,_) -> Some <| {A = aOpt; B=bOpt; C=cOpt}
| (_,_,Some c) -> Some <| {A = aOpt; B=bOpt; C=cOpt}
| _ -> None
// This might fail, so return option -- caller must test for None
// These three always succeed, no need to test for None
let createWhenAExists a bOpt cOpt =
{A = Some a; B=bOpt; C=cOpt}
let createWhenBExists aOpt b cOpt =
{A = aOpt; B=Some b; C=cOpt}
let createWhenCExists aOpt bOpt c =
{A = aOpt; B=bOpt; C=Some c}
// function used to extract data since type is private
let value atLeastOne =
let a = atLeastOne.A
let b = atLeastOne.B
let c = atLeastOne.C
(a,b,c)
// ---------------------------------------------
// Constrained AtLeastOne (object-oriented style)
// ---------------------------------------------
/// Class with constraint that at least one of the fields
/// must be set.
type OOAtLeastOne private (aOpt:int option,bOpt:int option,cOpt:int option) =
/// constructor
static member create(aOpt,bOpt,cOpt) =
match aOpt,bOpt,cOpt with
| (Some a,_,_) -> Some <| OOAtLeastOne(aOpt,bOpt,cOpt )
| (_,Some b,_) -> Some <| OOAtLeastOne(aOpt,bOpt,cOpt )
| (_,_,Some c) -> Some <| OOAtLeastOne(aOpt,bOpt,cOpt )
| _ -> None
// These three always succeed, no need to test for None
static member createWhenAExists(a,bOpt,cOpt) =
OOAtLeastOne(Some a,bOpt,cOpt)
static member createWhenBExists(aOpt,b,cOpt) =
OOAtLeastOne(aOpt,Some b,cOpt)
static member createWhenCExists(aOpt,bOpt,c) =
OOAtLeastOne(aOpt,bOpt,Some c)
member this.Value =
(aOpt,bOpt,cOpt)
// ---------------------------------------------
// Constrained DU (FP style)
// ---------------------------------------------
/// DU with constraint that classification must be done correctly
type NumberClass =
private
| IsPositive of int // int must be > 0
| IsNegative of int // int must be < 0
| Zero
/// Module containing functions related to NumberClass type
module NumberClass =
let create i =
if i > 0 then IsPositive i
elif i < 0 then IsNegative i
else Zero
// active pattern used to extract data since type is private
let (|IsPositive|IsNegative|Zero|) numberClass =
match numberClass with
| IsPositive i -> IsPositive i
| IsNegative i -> IsNegative i
| Zero -> Zero
// ======================================================
// This client attempts to use the types defined above
// ======================================================
module Client =
open ConstrainedTypes
// ---------------------------------------------
// Constrained String50 (FP-style)
// ---------------------------------------------
let s50Bad = String50 "abc"
// ERROR: The union cases or fields of the type 'String50' are not accessible from this code location
let s50opt = String50.create "abc"
s50opt
|> Option.map String50.value
|> Option.map (fun s -> s.ToUpper())
|> Option.iter (printfn "%s")
// ---------------------------------------------
// Constrained String50 (object-oriented style)
// ---------------------------------------------
let ooS50Bad = OOString50("abc")
// ERROR: This type has no accessible object constructors
let ooS50opt = OOString50.Create "abc"
ooS50opt
|> Option.map (fun s -> s.Value)
|> Option.map (fun s -> s.ToUpper())
|> Option.iter (printfn "%s")
// ---------------------------------------------
// Constrained AtLeastOne (FP style)
// ---------------------------------------------
let atLeastOneBad = {A=None; B=None; C=None}
// ERROR: The union cases or fields of the type 'AtLeastOne' are not accessible from this code location
let atLeastOne_BOnly = AtLeastOne.create None (Some 2) None
match atLeastOne_BOnly with
| Some x -> x |> AtLeastOne.value |> printfn "%A"
| None -> printfn "Not valid"
let atLeastOne_AOnly = AtLeastOne.createWhenAExists 1 None None
let atLeastOne_AB = AtLeastOne.createWhenAExists 1 (Some 2) None
atLeastOne_AB |> AtLeastOne.value |> printfn "%A"
// ---------------------------------------------
// Constrained AtLeastOne (OO style)
// ---------------------------------------------
let ooAtLeastOneBad = OOAtLeastOne(None, None, None) // This type has no accessible object constructors
let atLeastOne_BOnly = OOAtLeastOne.create(None,Some 2,None)
match atLeastOne_BOnly with
| Some x -> printfn "%A" x.Value
| None -> printfn "Not valid"
let ooAtLeastOne_AOnly = OOAtLeastOne.createWhenAExists(1,None,None)
let ooAtLeastOne_AB = OOAtLeastOne.createWhenAExists(1,Some 2,None)
ooAtLeastOne_AB.Value |> printfn "A=%A"
// ---------------------------------------------
// Constrained DU (FP style)
// ---------------------------------------------
// attempt to create a bad value
let numberClassBad = IsPositive -1
// ERROR: The union cases or fields of the type 'NumberClass' are not accessible from this code location
let numberClass = NumberClass.create -1
// this fails because the DU cases are not accessible
match numberClass with
| IsPositive i -> printfn "%i is positive" i
| IsNegative i -> printfn "%i is negative" i
| Zero -> printfn "is zero"
open NumberClass // bring active pattern into scope
// this works because the active pattern is being used.
match numberClass with
| IsPositive i -> printfn "%i is positive" i
| IsNegative i -> printfn "%i is negative" i
| Zero -> printfn "is zero"
@vasily-kirichenko

This comment has been minimized.

Show comment
Hide comment
@vasily-kirichenko

vasily-kirichenko May 11, 2015

Why is the pipe here Some <| {A = aOpt; B=bOpt; C=cOpt}?

vasily-kirichenko commented May 11, 2015

Why is the pipe here Some <| {A = aOpt; B=bOpt; C=cOpt}?

@ashtonkj

This comment has been minimized.

Show comment
Hide comment
@ashtonkj

ashtonkj May 11, 2015

Or you can go for a combination of the two styles:

type EmailAddress = 
    private
    | EmailAddress of string
    static member private isValid (value: string) =
        // Validation code goes here
        true
    static member Create(address: string) =
        if EmailAddress.isValid address then
            Some(EmailAddress(address))
        else
            None
    member this.Value =
        match this with 
        | EmailAddress value -> value

ashtonkj commented May 11, 2015

Or you can go for a combination of the two styles:

type EmailAddress = 
    private
    | EmailAddress of string
    static member private isValid (value: string) =
        // Validation code goes here
        true
    static member Create(address: string) =
        if EmailAddress.isValid address then
            Some(EmailAddress(address))
        else
            None
    member this.Value =
        match this with 
        | EmailAddress value -> value
@kylone

This comment has been minimized.

Show comment
Hide comment
@kylone

kylone May 11, 2015

@vasily-kirichenko Some <| {A = aOpt; B=bOpt; C=cOpt} is the exact same as Some ({A = aOpt; B=bOpt; C=cOpt}). The 'pipe-backward' <| operator makes the evaluation of the expression go right to left at that point, rather than the normal left to right. I use them in situations like this. It's worth noting that using both <| and |> on the same statement can get confusing real quick.

kylone commented May 11, 2015

@vasily-kirichenko Some <| {A = aOpt; B=bOpt; C=cOpt} is the exact same as Some ({A = aOpt; B=bOpt; C=cOpt}). The 'pipe-backward' <| operator makes the evaluation of the expression go right to left at that point, rather than the normal left to right. I use them in situations like this. It's worth noting that using both <| and |> on the same statement can get confusing real quick.

@realparadyne

This comment has been minimized.

Show comment
Hide comment
@realparadyne

realparadyne May 20, 2015

@kylone You can just write Some {A =.... }. The left pipe is redundant in this case, and so would be () around the {}.

realparadyne commented May 20, 2015

@kylone You can just write Some {A =.... }. The left pipe is redundant in this case, and so would be () around the {}.

@davidglassborow

This comment has been minimized.

Show comment
Hide comment
@davidglassborow

davidglassborow Jan 3, 2018

@swlaschin how about just rebinding the constructor, pattern matching still works as well.
e.g.

type String50 = String50 of string
let String50 str = 
    if String.IsNullOrEmpty(str) then
        failwith "No empty strings"
    elif String.length str > 50 then
        failwith "String must be less than 50 characters"
    else
        String50 str

davidglassborow commented Jan 3, 2018

@swlaschin how about just rebinding the constructor, pattern matching still works as well.
e.g.

type String50 = String50 of string
let String50 str = 
    if String.IsNullOrEmpty(str) then
        failwith "No empty strings"
    elif String.length str > 50 then
        failwith "String must be less than 50 characters"
    else
        String50 str
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment