How to do argument validation of F# records

前端 未结 4 576
执念已碎
执念已碎 2021-01-07 22:24

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 con

相关标签:
4条回答
  • 2021-01-07 22:43

    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
            ()
    
    0 讨论(0)
  • 2021-01-07 22:49

    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
    
    0 讨论(0)
  • 2021-01-07 22:55

    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.

    0 讨论(0)
  • 2021-01-07 23:07

    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
    
    0 讨论(0)
提交回复
热议问题