Protected fields: Allow read access, but prevent construction to support input validation
See original GitHub issueProblem
We often need to validate input when constructing a value of a particular type. For example, let’s say we want a type that represents positive floating-point numbers:
type PosFloat =
{
Value : float
}
module PosFloat =
let add x y =
{
Value = x.Value + y.Value
}
(Note that this could be represented by either a record or a discriminated union. I’ve chosen to use a record type here, but the same discussion applies equally to DUs.)
We could then use this type as follows:
let x = { Value = 1.0 }
let y = { Value = 2.0 }
PosFloat.add x y |> printfn "%A" // { Value = 3.0 }
We want to make invalid states unrepresentable, so we need to prevent code like this:
let bad = { Value = -1.0 }
To do this, we make the fields private and provide a create
function instead:
type PosFloat =
private {
Value : float
}
module PosFloat =
let create value =
if value <= 0.0 then failwith "Must be positive"
{
Value = value
}
let add x y =
create (x.Value + y.Value)
let x = PosFloat.create 1.0
let y = PosFloat.create 2.0
PosFloat.add x y |> printfn "%A" // { Value = 3.0 }
let bad = PosFloat.create -1.0 // System.Exception: Must be positive
But now users can’t access the internal value themselves:
module MyModule =
let mult x y =
PosFloat.create (x.Value * y.Value) // The union cases or fields of the type 'PosFloat' are not accessible from this code location
How can we fix this?
Workaround
One approach is to provide a separate member for accessing the internal value:
type PosFloat =
private {
_Value : float
}
with member this.Value = this._Value
module PosFloat =
let create value =
if value <= 0.0 then failwith "Must be positive"
{
_Value = value
}
let add x y =
create (x._Value + y._Value)
But this has some major disadvantages:
- We had to rename the private field to avoid conflicting with the name of the public member.
- Users lose automatic type inference, so have to explicitly annotate uses of the type, which is potentially a lot of extra typing:
module MyModule =
let badMult x y =
PosFloat.create (x.Value * y.Value) // ERROR: Lookup on object of indeterminate type based on information prior to this program point. A type annotation may be needed prior to this program point to constrain the type of the object.
let mult (x : PosFloat) (y : PosFloat) =
PosFloat.create (x.Value * y.Value)
Proposal
I think it would be useful to declare a type whose fields can be accessed, but which cannot be directly created by users. Something like this:
type PosFloat =
protected {
Value : float
}
I’ve used protected
here, but feel free to substitute another keyword of your choice. Users would then be able to access the Value
field but not able to use it to construct a value of the type:
let x = { Value = 1.0 } // prohibited
printfn "%A" y.Value // allowed
Issue Analytics
- State:
- Created 2 years ago
- Comments:6 (2 by maintainers)
Top GitHub Comments
Records and DU’s have benefits in F# that objects don’t. In particular, they have simpler syntax and support type inference and pattern matching. Personally, I try to use object types only when necessary (e.g. for compatibility with C#).
Let’s see what my example looks like as an object type:
The
do
andmember val
syntax required is pretty obscure for such a common use case, IMHO. Furthermore, functions takingPosFloat
s must now annotate them explicitly, which is a burden on users:So, yes, it’s doable with objects, but it’s not very ergonomic.
Done: https://github.com/fsharp/fslang-suggestions/issues/1122