【问题标题】:How to do argument validation of F# records如何对 F# 记录进行参数验证
【发布时间】:2013-09-03 14:15:45
【问题描述】:

F# 可以轻松定义诸如

之类的类型
type coords = { X : float; Y : float }

但是如何在不涉及更冗长的类定义语法的情况下为构造函数定义约束/检查参数?例如。如果我希望坐标从 (0,0) 开始或抛出异常。

此外,如果我将定义更改为一个类,我需要实现 Equals() 等所有我不想要的样板代码(以及我在 C# 中试图摆脱的)。

【问题讨论】:

标签: constructor f# record


【解决方案1】:

您可以将实现设为私有。您仍然获得结构上的平等,但您失去了直接的字段访问和模式匹配。您可以使用主动模式恢复该能力。

//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

【讨论】:

  • 谢谢,我的直觉告诉我这是非常复杂的,不是一种非常典型的类型制作方法?我想创建一个类并实现 Equals 是更可取的.. 或者强制我的程序中的所有类型都是通过一些通用的 create 方法构造的?
  • 这很罕见,是的,但只有你做的那么复杂。我认为私有实现是一个很好的特性,并且对于一些公认的不常见的场景很有用。
【解决方案2】:

F# for fun and profit 上有一个名为 Designing with Types 的系列。在“强制使用构造函数”一节中,它建议使用构造函数——这是在实例化类型之前进行验证的地方。为了防止人们直接实例化类型,它建议使用命名约定或签名文件。

您可以通过谷歌搜索“领域驱动设计 f#”找到更多相关文章和示例。

请注意,我来自 C# / 还没有将 F# 应用到我们的域层(还 ;)我无法真正说出任何一种推荐的方法在更大的项目中如何发挥作用。在这个勇敢的新世界中,有些事情确实看起来……不同。

【讨论】:

    【解决方案3】:

    你必须使用类定义语法:

    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
    

    【讨论】:

    • 如果我将我的定义更改为一个类,我需要实现 Equals() 等所有我不想要的样板代码(以及我在 C# 中试图摆脱的代码)来自)。
    • 是的,你必须这样做。如果你想要面向对象的封装/数据隐藏,你需要面向对象的类似 C# 的东西,没有办法。 F# 的好处是你可以做到。
    • 那么 F# 方式是什么?在与记录相同的模块中定义一个类或定义一个create函数并希望人们使用create方法?
    • 不需要elif(因为invalidArg 抛出),只需要if,也不需要else
    【解决方案4】:

    Daniel 的回答似乎最接近“FP”方法,但一个缺点是我们失去了利用其他福利记录提供的能力,例如复制和更新。由于我们现在有匿名记录,看来我们可以使用它们以透明的方式处理封装的对象。

    更新:Abel 建议使用匿名记录有一些缺点(例如失去模式匹配的能力等),因此我将这种方法与私有单例 DU 和公共记录相结合来解决这个问题担心。

    // 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
            ()
    

    【讨论】:

    • 当然是一种有趣且有创意的方法,尽管使用 匿名记录 prevents using match expressions(与私有字段一样),并且它将模仿的匿名类型限制为一堆other restraints of anonymous records。只是说,在某些情况下它可能是一种有用的方法,但肯定不是全部。
    • 这是真的,关于匿名记录的局限性 - 感谢您指出这一点。如果匿名记录本身存在太多缺点,那么当然没有什么可以阻止人们使用匿名记录、活动模式或此处所述的其他一些方法的组合。就我而言,我想通过所有这些来验证记录的创建,结果却丢失了副本并且更新的代价太高了。
    • @Abel,在考虑了您的评论后,我决定将标准记录包装在标记为私有的单个案例 DU 中,这似乎解决了使用匿名记录的缺点(因为现在它是标准记录类型) 并且在将 Coords 提升到单个案例 DU 中时仍然允许进行相同类型的验证。例如type CoordsRecord = { X : float Y : float } type Coords = private Coords of CoordsRecord ...然后,模块中的 create 函数可以接受 CoordsRecord 作为参数,对其进行验证,如果通过则将其提升为 Coords DU
    • 这听起来很有趣,也许你想在你的答案中反映出来(你可以编辑它),如果你觉得这可以改善它。大多数人不会通读所有的 cmets ;)。
    • 感谢您的反馈,obv 我是新手。 :-)
    猜你喜欢
    • 1970-01-01
    • 2014-03-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-01-31
    • 2022-07-28
    • 2019-02-19
    相关资源
    最近更新 更多