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

不问归期 提交于 2019-11-28 02:27:32

问题


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.


回答1:


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



回答2:


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"



回答3:


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.



来源:https://stackoverflow.com/questions/13925361/is-it-possible-to-enforce-that-a-record-respects-some-invariants

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!