Is it possible to enforce that a Record respects some invariants?

前端 未结 3 1259
南方客
南方客 2020-12-18 17:47

Suppose I wanted to create a Record type that represents acceptable min/max bounds:

type Bounds = { Min: float; Max: float }

Is there a way

相关标签:
3条回答
  • I think your best bet is a static member:

    type Bounds = { Min: float; Max: float }
        with
            static member Create(min: float, max:float) =
                if min >= max then
                    invalidArg "min" "min must be less than max"
    
                {Min=min; Max=max}
    

    and use it like

    > Bounds.Create(3.1, 2.1);;
    System.ArgumentException: min must be less than max
    Parameter name: min
       at FSI_0003.Bounds.Create(Double min, Double max) in C:\Users\Stephen\Documents\Visual Studio 2010\Projects\FsOverflow\FsOverflow\Script2.fsx:line 5
       at <StartupCode$FSI_0005>.$FSI_0005.main@()
    Stopped due to error
    > Bounds.Create(1.1, 2.1);;
    val it : Bounds = {Min = 1.1;
                       Max = 2.1;}
    

    However, as you point out, the big down-side of this approach is that there is nothing preventing the construction of an "invalid" record directly. If this is a major concern, consider using a class type for guaranteeing your invariants:

    type Bounds(min:float, max:float) = 
        do
            if min >= max then
                invalidArg "min" "min must be less than max"
    
        with
            member __.Min = min
            member __.Max = max
    

    together with an active pattern for convenience similar to what you get with records (specifically with regard to pattern matching):

    let (|Bounds|) (x:Bounds) =
        (x.Min, x.Max)
    

    all together:

    > let bounds = Bounds(2.3, 1.3);;
    System.ArgumentException: min must be less than max
    Parameter name: min
       at FSI_0002.Bounds..ctor(Double min, Double max) in C:\Users\Stephen\Documents\Visual Studio 2010\Projects\FsOverflow\FsOverflow\Script2.fsx:line 4
       at <StartupCode$FSI_0003>.$FSI_0003.main@()
    Stopped due to error
    > let bounds = Bounds(1.3, 2.3);;
    
    val bounds : Bounds
    
    > let isMatch = match bounds with Bounds(1.3, 2.3) -> "yes!" | _ -> "no";;
    
    val isMatch : string = "yes!"
    
    > let isMatch = match bounds with Bounds(0.3, 2.3) -> "yes!" | _ -> "no";;
    
    val isMatch : string = "no"
    
    0 讨论(0)
  • 2020-12-18 18:38

    Here's another solution based on protection levels:

    module MyModule =
        type Bounds = private { _min: float; _max: float } with
            // define accessors, a bit overhead
            member public this.Min = this._min
            member public this.Max = this._max
            static member public Make(min, max) =
                if min > max then raise (ArgumentException("bad values"))
                {_min=min; _max=max}
    
        // The following line compiles fine,
        // e.g. within your module you can do "unsafe" initialization
        let myBadBounds = {_min=10.0; _max=5.0}
    
    open MyModule
    let b1 = Bounds.Make(10.0, 20.0) // compiles fine
    let b1Min = b1.Min
    let b2 = Bounds.Make(10.0, 5.0) // throws an exception
    // The following line does not compile: the union cases of the type 'Bounds'
    // are not accessible from this code location
    let b3 = {_min=10.0; _max=20.0}
    // The following line takes the "bad" value from the module
    let b4 = MyModule.myBadBounds
    
    0 讨论(0)
  • 2020-12-18 18:40

    A dodgy solution for the string example - use a DU

    type cleverstring = |S of char * string
    

    This will force the string to have at least one charcter. Then you can just use cleverstring instead of string in your record, although you probably want to write some wrapper functions to make it look like a string.

    0 讨论(0)
提交回复
热议问题