Is it possible to enforce that a Record respects s

2019-01-25 03:39发布

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

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

Is there a way to enforce that Min < Max? It is easy to write a validateBounds function, I was just wondering if there was a better way to do this.

Edit: I realized that for this specific example I could probably get away with exposing two properties and re-order the arguments, so let's say we were trying to do

type Person = { Name: string }

and Name needs to have at least one character.

3条回答
Ridiculous、
2楼-- · 2019-01-25 03:48

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
查看更多
趁早两清
3楼-- · 2019-01-25 03:51

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.

查看更多
爷的心禁止访问
4楼-- · 2019-01-25 04:06

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"
查看更多
登录 后发表回答