在 F# 中定义非零整数类型

Defining a non zero integer type in F#

如何在 F# 中定义一个非零整数,从而在分配零值时引发编译时错误?

我的问题来自 Scott Wlaschin 视频 https://www.youtube.com/watch?v=E8I19uA-wGY&t=960s 16:00

我在 SO 中找到了这个问题的另一个答案,但都是指动态检查(在创建时抛出异常),但这种方法没什么大不了的,可以用任何 OO 而不是 OO 语言来完成。我正在寻找的是:type NonZeroInteger = int except 0 或类似的东西。

在 F# 中,您想要执行的操作并没有真正的编译时契约。处理此问题的 F# 方法是使用具有私有构造函数的类型 NonZeroInteger 和 returns 和 Option<NonZeroInteger> 的函数。这将确保您永远不会 Some(0).

这样做基本上是迫使使用您的代码的开发人员考虑到在构造一个 NonZeroInteger 后,如果给定错误的整数值,他可能没有 NonZeroInteger 的可能性。

在您的代码中,您始终可以安全地假设 NonZeroIntegers 实际上是非零的。

open Option

module A =
    type NonZeroInteger =
        private | NonZeroInteger of int

        static member Create (v : int) : Option<NonZeroInteger> =
            if v = 0 then
                None
            else
                Some(NonZeroInteger(v))

        member this.Get : int =
            this |> fun (NonZeroInteger v) -> v

        member this.Print =
            this |> fun (NonZeroInteger v) -> printfn "%i" v


printfn "%A" (A.NonZeroInteger(0)) // error FS1093: The union cases or fields of the type 'NonZeroInteger' are not accessible from this code location

let wrong = A.NonZeroInteger.Create(0) // None
let right = A.NonZeroInteger.Create(-1) // Some

wrong |> Option.iter (fun x -> x.Print) // Doesn't print anything
right |> Option.iter (fun x -> x.Print) // Prints -1

private 构造函数可防止模块外的任何人在不通过您的 Create 函数的情况下构造 NonZeroInteger

这使得代码非常冗长和缓慢,但安全。所以这里肯定需要权衡。

你不能真的直接这样做,例如使用 Scala 的 require

这是一种有点 DDD 风格的方法:

type NonZeroInteger = private NonZeroInteger of int 
let createNZI i = 
  if i = 0 then
    Error "NonZeroInteger can't be Zero"
  else 
    Ok (NonZeroInteger i)

构造函数是私有的,因此您必须通过 createNZI 函数(如果更自然,您可以切换名称)。比处理 Result 类型。如果更简单,您可以使用 Some/None 代替。最后,如有必要,使用模式匹配或活动模式解包 NonzeroInteger。

其他答案对于 F# 来说非常惯用,但这里有一种方法可以不可能 构造零值(给调用者带来轻微的不便):

type Digit =
| One = 1
| Two = 2
| Three = 3
| Four = 4
| Five = 5
| Six = 6
| Seven = 7
| Eight = 8
| Nine = 9


type NonZero private(ones, tens, hundreds, thousands, ten_thousands) =    
    static let f (n : Digit) = int n

    member val num = f(ones) + 10 * tens + 100 * hundreds + 1000 * thousands + 10000 * ten_thousands

    new (ten_thousands, thousands, hundreds, tens, ones) = NonZero(ones, f tens, f hundreds, f thousands, f ten_thousands)
    new (thousands, hundreds, tens, ones) = NonZero(ones, f tens, f hundreds, f thousands, 0)
    new (hundreds, tens, ones) = NonZero(ones, f tens, f hundreds, 0, 0)
    new (tens, ones) = NonZero(ones, f tens, 0, 0, 0)
    new (ones) = NonZero(ones, 0, 0, 0, 0)

这里是你构造数字 123 的方法:

let k = new NonZero(Digit.One, Digit.Two, Digit.Three)

并检索值:

let l = k.num //l is 123 : int

因此,不可能将零值传递给类型为 NonZero -> 'a 的函数。