-
-
Save caiorss/6f541c27a17de564a4f20889010af1f4 to your computer and use it in GitHub Desktop.
Examples of creating constrained types in F#
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment