首页 > 解决方案 > 在 F# 的类型成员中定义允许的值范围

问题描述

我正在玩F#中的测量单位,我目前正在尝试创建长度和质量的复合测量单位,以反映英制中的口语,例如“我 5 英尺 10”或“她重 8石头和 11 磅”在美国和英国。

我已经为标准(非复合)单元定义了一个模块,如下所示:

module Units
    // Mass
    [<Measure>] type kg // Kilogram
    [<Measure>] type g  // Gram
    [<Measure>] type lb // Pound (mass)
    [<Measure>] type st // Stone (mass)
    
    // Conversions
    ...

    // Length
    [<Measure>] type m      // Metre
    [<Measure>] type cm     // Centimetre
    [<Measure>] type inch   // Inch
    [<Measure>] type ft     // Foot

    // Conversions
    ...

我在不同的模块中定义了复合单元:

module CompoundUnits
    open Units
    
    // Mass
    type StonesAndPounds = {
        Stones: float<st>
        Pounds: float<lb>
    }

    // Length
    type FeetAndInches = {
        Feet: float<ft>
        Inches: float<inch>
    }

但是,按照我目前编写复合质量和长度类型的方式,存在非法状态(例如负值)和技术上正确但不是首选的状态的空间:

// 39 lbs = 2 st 11 lbs
let eightStoneEleven: StonesAndPounds = { Stones = 6.0<st>; Pounds = 39.0<lb> }
// 22" = 1' 10"
let fiveFootTen: FeetAndInches = { Feet = 4.0<ft>; Inches = 22.0<inch> }

Scott Wlaschin 在他的“Domain Modeling made Functional”一书中谈到了使非法状态无法表示,所以我想知道是否有办法对我的复合类型实施某种限制,以便0<ft> <= Feet,0<inch> <= Inches <= 12<inch>0<st> <= Stones, 0<lb> <= Pounds <= 14<lb>

标签: f#domain-driven-design

解决方案


一种常见的模式是为该类型创建一个模块,其中包含其定义以及create函数和其他验证逻辑。

Scott 在他的网站上有一些示例,作为他的“使用类型设计”系列的一部分。

https://fsharpforfunandprofit.com/posts/designing-with-types-non-strings/

您不能对测量单位本身实施限制,但您可以创建专用类型来表示您的复合测量,就像 Scott 使用SafeDate等一样NonNegativeInt

这些仍然可以对其组件属性使用“标准”度量单位。

引用文章:

“度量单位确实可以用来避免混淆不同类型的数值,并且比我们一直使用的单一案例联合强大得多。

另一方面,计量单位没有封装,也没有约束。任何人都可以使用度量单位创建一个 int,并且没有最小值或最大值。”


推荐阅读