Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to do argument validation of F# records

F# makes it easy to define types such as

type coords = { X : float; Y : float }

but how do I define constraints/check arguments for the constructor without going into the more verbose class definition syntax? E.g. if I want coords to start from (0,0) or throw an exception.

Moreover, if I change my definition to a class I need to implement Equals() etc. all the boiler plate code I don't want (and which I have in C# that I'm trying to get away from).

like image 614
Carlo V. Dango Avatar asked Aug 30 '13 19:08

Carlo V. Dango


People also ask

How do you validate the number of function arguments in MATLAB?

nargin in Argument ValidationThe nargin function returns the number of function input arguments given in the call to the currently executing function. When using function argument validation, the value returned by nargin within a function is the number of positional arguments provided when the function is called.

Can a function be an argument in MATLAB?

MATLAB® creates a cell array that contains all the values passed in for that argument. Functions can include only one repeating arguments block. If the function includes both repeating and name-value arguments, declare name-value arguments in their own, separate arguments block after the repeating arguments block.

How do you validate inputs in MATLAB?

Use function argument validation in MATLAB® to declare specific restrictions on function input arguments. You can constrain the class, size, and other aspects of function input values without writing code in the body of the function to perform these tests.

What does Varargin mean in MATLAB?

varargin is an input variable in a function definition statement that enables the function to accept any number of input arguments. Specify varargin by using lowercase characters. After any explicitly declared inputs, include varargin as the last input argument .


4 Answers

You can make the implementation private. You still get structural equality but you lose direct field access and pattern matching. You can restore that ability using active patterns.

//file1.fs

type Coords = 
  private { 
    X: float
    Y: float 
  }

[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
module Coords =
  ///The ONLY way to create Coords
  let create x y =
    check x
    check y
    {X=x; Y=y}

  let (|Coords|) {X=x; Y=y} = (x, y)

//file2.fs

open Coords
let coords = create 1.0 1.0
let (Coords(x, y)) = coords
printfn "%f, %f" x y
like image 102
Daniel Avatar answered Oct 13 '22 04:10

Daniel


There's a series called Designing with Types on F# for fun and profit. In section "Forcing use of the constructor" it recommends the use of constructor functions - that's where the validations go before the type is instantiated. To keep people from directly instantiating types it recommends either naming conventions or signature files.

You can find several more relevant articles and examples by googling "domain driven design f#".

Note that I'm coming from C# / not having applied F# to our domain layer (yet ;) I cannot really tell how either of the recommended methods would work out in a bigger project. Some things sure seem.. different in this brave new world.

like image 23
stmax Avatar answered Oct 13 '22 04:10

stmax


You have to use the class definition syntax:

type coords(x: float, y: float) =
  do
    if x < 0.0 then
      invalidArg "x" "Cannot be negative"
    if y < 0.0 then
      invalidArg "y" "Cannot be negative"

  member this.X =
    x
  member this.Y =
    y
like image 4
MiMo Avatar answered Oct 13 '22 05:10

MiMo


Daniel's answer seems to be the closest to the "FP" approach, but one disadvantage is that we lose the ability to utilize other benefits records offer, such as copy and update. Since we now have anonymous records, it seems we could use those to work with the encapsulated object in a transparent way.

UPDATE: Abel suggested there are some downsides to using anonymous records (such as losing the ability to pattern match, etc.), so I used a combination of this approach with a private single case DU and a public record to address that concern.

// file1.fs

type Coords' =
    { X : float
      Y : float }


type Coords = private Coords of Coords'

module Coords =
    
    let private checkCoord (value : float) =
        if value < 0.0 || value > 32.0 then invalidOp "Invalid coordinate"

    let create (newcoord : Coords') =
        checkCoord newcoord.X
        checkCoord newcoord.Y
        newcoord |> Coords

    let value (Coords c) = c

// file2.fs
open File1

module Tests =

    [<Test>]
    let Test0 () =
        let firstcoord = Coords.create {X = 5.0; Y = 6.0}
        let secondcoord = Coords.create {(firstcoord |> Coords.value) with X = 10.0}
        let thirdcoord = Coords.value secondcoord

        Assert.IsTrue (thirdcoord.X = 10.0)
        Assert.IsTrue (thirdcoord.Y = 6.0)
        Assert.Pass ()

    [<Test>]
    let Test1 () =
        {X = 0.0; Y = 0.0} |> Coords   //Doesn't compile
        ()
like image 1
Vince G. Avatar answered Oct 13 '22 04:10

Vince G.