Skip to content

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.

Copy link

commented May 11, 2015

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

@ashtonkj

This comment has been minimized.

Copy link

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.

Copy link

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.

Copy link

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.

Copy link

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
@arthurborisow

This comment has been minimized.

Copy link

commented Mar 25, 2019

@david I guess throwing exceptions is not a FP-style. Besides in this case you only have possibility to validate one input (which might come to your ASP.net controller for example) and leave unvalidated the rest of inputs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.