问题
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