Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
Examples of creating constrained types in F#
module ConstrainedTypes =
open System
// 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 function to extract the
// data from the type.
//
// This is marginally annoying, but you can always use the C# approach if you like!
//
// OTOH, your opaque types really ARE opaque!
// ---------------------------------------------
// Constrained String50 (without using signature files)
type String50 = private String50 of string
let createString50 s =
if String.IsNullOrEmpty(s) then
None
elif String.length s > 50 then
None
else
Some (String50 s)
// function used to extract data since type is private
let string50Value (String50 s) = s
// ---------------------------------------------
// Constrained String50 (C# style)
type OOString50 private(s) =
member this.Value = s
static member Create s =
if String.IsNullOrEmpty(s) then
None
elif String.length s > 50 then
None
else
Some (OOString50(s))
// ---------------------------------------------
// Constrained AtLeastOne (FP style)
type AtLeastOne = private {A:int option; B: int option; C: int option}
// This might fail, so return option -- caller must test for None
let createAtLeastOne 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
// 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 atLeastOneValue atLeastOne =
let a = atLeastOne.A
let b = atLeastOne.B
let c = atLeastOne.C
(a,b,c)
// ---------------------------------------------
// Constrained AtLeastOne (C# style)
type OOAtLeastOne private (aOpt:int option,bOpt:int option,cOpt:int option) =
member this.A = aOpt
member this.B = bOpt
member this.C = cOpt
// This might fail, so return option -- caller must test for None
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)
// ---------------------------------------------
// Constrained DU (FP style)
type NumberClass =
private
| IsPositive of int // int must be > 0
| IsNegative of int // int must be < 0
| Zero
let createNumberClass 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 (without using signature files)
let s50Bad = String50 "abc" // The union cases or fields of the type 'String50' are not accessible from this code location
let s50opt = createString50 "abc"
s50opt
|> Option.map string50Value
|> Option.map (fun s -> s.ToUpper())
|> Option.iter (printfn "%s")
// ---------------------------------------------
// Constrained String50 (C# style)
let ooS50Bad = OOString50("abc") // 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} // The union cases or fields of the type 'AtLeastOne' are not accessible from this code location
let atLeastOne_BOnly = createAtLeastOne None (Some 2) None
match atLeastOne_BOnly with
| Some x -> x |> atLeastOneValue |> printfn "%A"
| None -> printfn "Not valid"
let atLeastOne_AOnly = createWhenAExists 1 None None
let atLeastOne_AB = createWhenAExists 1 (Some 2) None
atLeastOne_AB |> atLeastOneValue |> printfn "%A"
// ---------------------------------------------
// Constrained AtLeastOne (C# 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=%A; B=%A; C=%A" x.A x.B x.C
| None -> printfn "Not valid"
let ooAtLeastOne_AOnly = OOAtLeastOne.createWhenAExists(1,None,None)
let ooAtLeastOne_AB = OOAtLeastOne.createWhenAExists(1,Some 2,None)
ooAtLeastOne_AB.A |> printfn "A=%A"
// ---------------------------------------------
// Constrained DU (FP style)
let numberClassBad = IsPositive -1 // The union cases or fields of the type 'NumberClass' are not accessible from this code location
let numberClass = createNumberClass -1
match numberClass with
| IsPositive i -> printfn "%i is positive" i
| IsNegative i -> printfn "%i is negative" i
| Zero -> printfn "is zero"

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

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

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment