如何在 F# 中通过 SRTP 添加与 int32 兼容且通用的基于闭包的数字运算支持

How to add an int32-compatible and versatile, closure-based numeric operation support through SRTP in F#

上下文:前几天(几周前,我正在玩一种算法,根据不同的类型提取给定数字的数字)。

注意:除了玩 F# SRTP 之外,没有其他实际目的。

type Numbers =
    static member Ten (_ : uint8)   = 10uy
    static member Ten (_ : uint16)  = 10us
    static member Ten (_ : uint32)  = 10ul
    static member Ten (_ : uint64)  = 10UL
    static member Ten (_ : int8)    = 10y
    static member Ten (_ : int16)   = 10s
    static member Ten (_ : int32)   = 10l
    static member Ten (_ : int64)   = 10L
    static member Ten (_ : single)  = 10f
    static member Ten (_ : double)  = 10.
    static member Ten (_ : decimal) = 10m
    // static member Ten (_ : bigint)  = 10I //bug?

let inline (/%) dividend divisor = let quotient = dividend / divisor in quotient, dividend - quotient * divisor

let inline divRemBy10 x =
    let inline call (_: ^T, x: ^I) = ((^T or ^I) : (static member Ten: _ -> _) x)
    x /% (call (Unchecked.defaultof<Numbers>, x))

[<RequireQualifiedAccess>]
module Digits =
    let inline (|Positive|Zero|Negative|) number =
        if number > LanguagePrimitives.GenericZero then Positive
        elif number = LanguagePrimitives.GenericZero then Zero
        else Negative

    let inline toDigits number =
        let generator oldQuotient =
            let newQuotient, newRemainder = divRemBy10 oldQuotient
            if newQuotient = LanguagePrimitives.GenericZero &&
               newRemainder = LanguagePrimitives.GenericZero
               then None
            else Some(newRemainder, newQuotient)
        let alternateNegative i digit =
            if i > LanguagePrimitives.GenericZero then digit * -LanguagePrimitives.GenericOne
            else digit
        match number with
        | Positive -> number |> Seq.unfold generator |> Seq.rev
        | Zero     -> Seq.singleton LanguagePrimitives.GenericZero
        | Negative -> number |> Seq.unfold generator |> Seq.rev |> Seq.mapi alternateNegative


[<EntryPoint>]
let main _ =
    [ -465l; 10l; 0l; -10l; 1l; 42l; Int32.MaxValue; Int32.MinValue ]
    |> List.map (fun number -> number, Digits.toDigits number |> String.join "")
    |> List.iter(fun (number, digitsString) -> printfn $"%d{number}=%s{digitsString}")

    [ 10y; 0y; -10y; 1y; 42y; SByte.MaxValue; SByte.MinValue ]
    |> List.map (fun number -> number, Digits.toDigits number |> String.join "")
    |> List.iter(fun (number, digitsString) -> printfn $"%d{number}=%s{digitsString}")

    [ 10s; 0s; -10s; 1s; 42s; Int16.MaxValue; Int16.MinValue ]
    |> List.map (fun number -> number, Digits.toDigits number |> String.join "")
    |> List.iter(fun (number, digitsString) -> printfn $"%d{number}=%s{digitsString}")

    [ 10L; 0L; -10L; 1L; 42L; Int64.MaxValue; Int64.MinValue ]
    |> List.map (fun number -> number, Digits.toDigits number |> String.join "")
    |> List.iter(fun (number, digitsString) -> printfn $"%d{number}=%s{digitsString}")
    0

到目前为止,上面的代码运行良好。

话虽这么说,但我想知道如何重构它,以便我可以从一些可以变成 int32 的东西中受益,例如10 相当于每个基础数字基元的 Ten 函数实现。

let inline divRemBy x (i: int32) = ...

在调用站点,它可能变成:

divRemBy oldQuotient 10

但我真的想不通,有什么想法吗?

此外,想知道我的目标是否真的可行?

由于您的除数现在是非泛型的,您不能再使用被除数类型的除法运算符。相反,我认为最好的办法是将所有内容都转换为“安全”类型,使用安全类型的除法运算符,然后再转换回股息类型。这在转换过程中引入了 rounding/truncation 错误的可能性,但幸运的是,看起来我们可以使用 int64 作为安全类型(包括 bigint 支持)来容纳您的所有测试用例).

首先,这里是安全除法函数:

let div (convert, dividend: int64, divisor: int64) =
    let quotient = dividend / divisor
    convert quotient, convert (dividend - quotient * divisor)

接下来我们更改 Numbers 以便它调用 div 并为每种股息类型使用正确的转换函数:

type Numbers =
    static member Div (x: uint8, y: int64) = div (uint8, int64 x, y)
    static member Div (x: uint16, y: int64) = div (uint16, int64 x, y)
    static member Div (x: uint32, y: int64) = div (uint32, int64 x, y)
    static member Div (x: int8, y: int64) = div (int8, int64 x, y)
    static member Div (x: int16, y: int64) = div (int16, int64 x, y)
    static member Div (x: int32, y: int64) = div (int32, int64 x, y)
    static member Div (x: int64, y: int64) = div (int64, int64 x, y)
    static member Div (x: single, y: int64) = div (single, int64 x, y)
    static member Div (x: double, y: int64) = div (double, int64 x, y)
    static member Div (x: decimal, y: int64) = div (decimal, int64 x, y)
    static member Div (x: bigint, y: int64) = div (bigint, int64 x, y)

我们现在可以按照您想要的方式实施 divRemBy

let inline divRemBy x (y: int) =
    let inline call (_: ^T, x: ^I) =
        ((^T or ^I) : (static member Div: _ * _ -> _) (x, int64 y))
    call (Unchecked.defaultof<Numbers>, x)

并且调用站点看起来也是您想要的方式:

let newQuotient, newRemainder = divRemBy oldQuotient 10

为了好玩,我将基数改为二进制(divRemBy oldQuotient 2),我认为结果仍然正确:

-465=-111010001
10=1010
0=0
-10=-1010
1=1
42=101010
2147483647=1111111111111111111111111111111
-2147483648=-10000000000000000000000000000000
10=1010
0=0
-10=-1010
1=1
42=101010
127=1111111
-128=-10000000
10=1010
0=0
-10=-1010
1=1
42=101010
32767=111111111111111
-32768=-1000000000000000
10=1010
0=0
-10=-1010
1=1
42=101010
9223372036854775807=111111111111111111111111111111111111111111111111111111111111111
-9223372036854775808=-1000000000000000000000000000000000000000000000000000000000000000