question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Protected fields: Allow read access, but prevent construction to support input validation

See original GitHub issue

Problem

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:closed
  • Created 2 years ago
  • Comments:6 (2 by maintainers)

github_iconTop GitHub Comments

2reactions
brianbernscommented, Feb 21, 2022

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:

type PosFloat(value) =
    do if value <= 0.0 then failwith "Must be positive"
    member val Value = value

The do and member val syntax required is pretty obscure for such a common use case, IMHO. Furthermore, functions taking PosFloats must now annotate them explicitly, which is a burden on users:

let mult (x : PosFloat) (y : PosFloat) =
    PosFloat (x.Value * y.Value)

So, yes, it’s doable with objects, but it’s not very ergonomic.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Restrict data input by using validation rules
Validations rules help you check data as it is added to your Access desktop database which improves accuracy and consistency of data entry....
Read more >
SQL Injection Prevention Cheat Sheet
SQL Injection Prevention Cheat Sheet¶. Introduction¶. This article is focused on providing clear, simple, actionable guidance for preventing SQL Injection ...
Read more >
Client-side form validation - Learn web development | MDN
Using built-in form validation​​ We've seen many of these earlier in the course, but to recap: required : Specifies whether a form field...
Read more >
CWE-20: Improper Input Validation
Input validation is a frequently-used technique for checking potentially dangerous inputs in order to ensure that the inputs are safe for processing within ......
Read more >
Security tips
Don't leak permission-protected data. This occurs when your app exposes data over IPC that is available only because your app has permission to ......
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found