Skip to content

Language Validation/Constraint Support For Types #939

@wilbennett

Description

@wilbennett

Disclaimer

I'm not an expert F# developer so please forgive any syntactic mistakes in the following as I'm not writing this in an IDE.

Overview

One of the big draws to F# for me is the promise of domain modeling in the type system. An area where this seems to fall short, IMHO, is supporting validation on types. Again, I'm not an expert F# developer so please don't hesitate to correct any of my misconceptions.

My understanding is, looking at declared types, you should be able to easily reason about how they are handled and the compiler should prevent invalid usage.

Issue

Let's examine a simple case:

I want to create a string subtype that only allows strings of length 3 to 5:

The existing way of approaching this problem in F# is ...

type String3To5 = private String3To5 of string

module String3To5 =
  let create str =
      if String.length str < 3 then raise "str should be at least 3 characters"
      if String.length str > 5 then raise "str should be no more than 5 characters"
      String3To5 str

// OR

  let create str =
      if String.length str < 3 then None
      elif String.length str > 5 then None
      else Some (String3To5 str)

  let value (String3To5 str) = str

There are several things I don't like about this solution:

  • Why are both construction and reading private?
    • The data is immutable. Reading should not be restricted.
    • I'm forced to use the "value" helper everywhere (or active pattern)
  • The validation is separate from the declaration
  • I can't reason about the type by looking at it
    • The name may give an indication but names lie

I propose we use one of the following options or something similar...

type String3To5 =
  | InvalidString3To5 of InvalidValue: string * Errors: string list
  | ValidString3To5 of Value: string
  with Validations
    Value: String.length Value < 3, "Value should be at least 3 characters"
    Value: String.length Value > 5, "Value should be no more than 5 characters"
  onfail errors: InvalidString3To5 (Value, errors) // Return type is String3To5
type String3To5 = String3To5 of Value: string
  with Validations
    Value: String.length Value < 3, "Value should be at least 3 characters"
    Value: String.length Value > 5, "Value should be no more than 5 characters"
  // One of:
  onfail errors: raise errors // Return type is String3To5
  onfail errors: None         // Return type is Option<String3To5>
  onfail errors: Error errors // Return type is Result<String3To5, string list>

I omitted some cases and took some corners for the sake of brevity but hopefully the intent is clear.

Pros and Cons

The advantages of making this adjustment to F# are ...

  • It is clear by looking at the declaration what the constraints on a type are
  • No need to create a module just for handling validation
  • Values are publicly accessible

The disadvantages of making this adjustment to F# are ...

  • Potential confusion, e.g. in the case where "new" returns Option instead of the type

Extra information

Estimated cost (XS, S, M, L, XL, XXL):

Related suggestions:

Affidavit (please submit!)

Please tick this by placing a cross in the box:

  • This is not a question (e.g. like one you might ask on stackoverflow) and I have searched stackoverflow for discussions of this issue
  • I have searched both open and closed suggestions on this site and believe this is not a duplicate
  • This is not something which has obviously "already been decided" in previous versions of F#. If you're questioning a fundamental design decision that has obviously already been taken (e.g. "Make F# untyped") then please don't submit it.

Please tick all that apply:

  • This is not a breaking change to the F# language design
  • I or my company would be willing to help implement and/or test this

For Readers

If you would like to see this issue implemented, please click the 👍 emoji on this issue. These counts are used to generally order the suggestions by engagement.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions