如何编写适用于通用数字的函数?

40

我对F#很新,发现类型推断真的很酷。但目前似乎它也可能导致代码重复,这并不是一件酷的事情。我想像这样对数字的位数求和:

let rec crossfoot n =
  if n = 0 then 0
  else n % 10 + crossfoot (n / 10)

crossfoot 123

这段代码正确输出了6。但是现在我的输入数字不适合int 32位,所以我必须对其进行转换。

let rec crossfoot n =
  if n = 0L then 0L
  else n % 10L + crossfoot (n / 10L)

crossfoot 123L

接着,一个 BigInteger 来到了我的面前,你猜怎么着...

当然,我只能使用 bigint 版本,并根据需要将输入参数强制转换为更大的数据类型并将输出参数降级。但首先,我认为使用 BigInteger 而不是 int 会有一些性能惩罚。其次,let cf = int (crossfoot (bigint 123)) 看起来就不太好。

难道没有一种通用的方法来编写这个吗?


1
我并不担心,至少不是从.NET方面来看,因为.NET没有适用于所有数字类型的通用子类型(实际上它根本无法有,因为所有值类型必须直接派生自System.ValueType)。话虽如此,我不了解F#,所以可能他们会以某种方式解决这个问题 - 但不要抱太高的希望。 - Joey
5个回答

26

在Brian和Stephen的回答基础上,这是一些完整的代码:

module NumericLiteralG = 
    let inline FromZero() = LanguagePrimitives.GenericZero
    let inline FromOne() = LanguagePrimitives.GenericOne
    let inline FromInt32 (n:int) =
        let one : ^a = FromOne()
        let zero : ^a = FromZero()
        let n_incr = if n > 0 then 1 else -1
        let g_incr = if n > 0 then one else (zero - one)
        let rec loop i g = 
            if i = n then g
            else loop (i + n_incr) (g + g_incr)
        loop 0 zero 

let inline crossfoot (n:^a) : ^a =
    let (zero:^a) = 0G
    let (ten:^a) = 10G
    let rec compute (n:^a) =
        if n = zero then zero
        else ((n % ten):^a) + compute (n / ten)
    compute n

crossfoot 123
crossfoot 123I
crossfoot 123L

更新:简单回答

这里是一个独立的实现,没有使用NumericLiteralG模块,并且推断类型稍微不那么严格:

let inline crossfoot (n:^a) : ^a =
    let zero:^a = LanguagePrimitives.GenericZero
    let ten:^a = (Seq.init 10 (fun _ -> LanguagePrimitives.GenericOne)) |> Seq.sum
    let rec compute (n:^a) =
        if n = zero then zero
        else ((n % ten):^a) + compute (n / ten)
    compute n

解释

F# 实际上有两种泛型类型:1)运行时多态,通过.NET接口/继承实现;2)编译时泛型。编译时泛型是必要的,以适应像泛型数学操作和类似鸭子类型(显式成员约束)之类的东西。这些功能对于 F# 来说至关重要,但在 .NET 中不受支持,因此必须由 F# 在编译时处理。

插入符号 (^) 用于区分 静态解析(编译时)类型参数与普通类型参数 (使用撇号)。简而言之,'a 在运行时处理,^a 在编译时处理-这就是为什么函数必须标记为 inline 的原因。

我以前从未尝试过这样的写作。结果比我预期的要笨拙得多。我认为在使用 F# 编写泛型数字代码时最大的障碍是:创建除零或一以外的泛型数字的实例。请参见此答案中的 FromInt32 实现,了解我的意思。 GenericZeroGenericOne是内置的,并且它们使用用户代码不可用的技术来实现。在这个函数中,由于我们只需要一个小数字(10),我创建了一个包含 10 个 GenericOne 的序列并将它们求和。

我无法很好地解释为什么需要所有类型注释,除了可以说每次编译器遇到泛型类型操作时,它似乎认为正在处理一个新类型。因此,它最终推断出一些具有重复限制的奇怪类型(例如,可能多次要求(+))。添加类型注释让它知道我们始终在处理相同的类型。代码没有注释也可以正常工作,但添加注释简化了推断的签名。


3
注意,使用数字字面量技巧推断出的crossfoot函数的类型签名非常混乱,没有添加足够的类型注释。 - Stephen Swensen
没错,这就是类型推断发挥作用的时候。我很高兴我们不必明确指定这些约束条件。 - Daniel
嗯,实际上我并不是这个意思:推断出的签名在这里过于笼统,我们可以通过1)添加一些类型注释,2)使用我开发的技术来显著缩紧它。请参见:https://dev59.com/FHE85IYBdhLWcg3wUBsZ#2840826 - Stephen Swensen
我并不完全理解你提供的代码(或者你的技术——但是我没有太多时间去看)。如果你能优化我的代码并且发布一个改进后的答案,我会非常高兴。 - Daniel
3
我喜欢 F#,但是对于新手来说,必须编写这样的泛型数值操作代码可能会让他们望而却步。 - Daniel
显示剩余4条评论

16

除了kvb使用数字文字的技巧(Brian的链接),我还采用了一种不同的技术,取得了很多成功,可以产生更好的推断结构类型签名,也可以用于创建预计算的类型特定函数,以实现更好的性能和对支持的数字类型的控制(例如您通常希望支持所有整数类型,但不是有理数类型):F#静态成员类型约束

在我们就不同技术产生的推断类型签名进行讨论的基础上,以下是概述:

NumericLiteralG技术

module NumericLiteralG = 
    let inline FromZero() = LanguagePrimitives.GenericZero
    let inline FromOne() = LanguagePrimitives.GenericOne
    let inline FromInt32 (n:int) =
        let one = FromOne()
        let zero = FromZero()
        let n_incr = if n > 0 then 1 else -1
        let g_incr = if n > 0 then one else (zero - one)
        let rec loop i g = 
            if i = n then g
            else loop (i + n_incr) (g + g_incr)
        loop 0 zero 

不添加任何类型注释的交叉求和:

let inline crossfoot1 n =
    let rec compute n =
        if n = 0G then 0G
        else n % 10G + compute (n / 10G)
    compute n

val inline crossfoot1 :
   ^a ->  ^e
    when ( ^a or  ^b) : (static member ( % ) :  ^a *  ^b ->  ^d) and
          ^a : (static member get_Zero : ->  ^a) and
         ( ^a or  ^f) : (static member ( / ) :  ^a *  ^f ->  ^a) and
          ^a : equality and  ^b : (static member get_Zero : ->  ^b) and
         ( ^b or  ^c) : (static member ( - ) :  ^b *  ^c ->  ^c) and
         ( ^b or  ^c) : (static member ( + ) :  ^b *  ^c ->  ^b) and
          ^c : (static member get_One : ->  ^c) and
         ( ^d or  ^e) : (static member ( + ) :  ^d *  ^e ->  ^e) and
          ^e : (static member get_Zero : ->  ^e) and
          ^f : (static member get_Zero : ->  ^f) and
         ( ^f or  ^g) : (static member ( - ) :  ^f *  ^g ->  ^g) and
         ( ^f or  ^g) : (static member ( + ) :  ^f *  ^g ->  ^f) and
          ^g : (static member get_One : ->  ^g)

添加了一些类型注释的Crossfoot:

let inline crossfoot2 (n:^a) : ^a =
    let (zero:^a) = 0G
    let (ten:^a) = 10G
    let rec compute (n:^a) =
        if n = zero then zero
        else ((n % ten):^a) + compute (n / ten)
    compute n

val inline crossfoot2 :
   ^a ->  ^a
    when  ^a : (static member get_Zero : ->  ^a) and
         ( ^a or  ^a0) : (static member ( - ) :  ^a *  ^a0 ->  ^a0) and
         ( ^a or  ^a0) : (static member ( + ) :  ^a *  ^a0 ->  ^a) and
          ^a : equality and  ^a : (static member ( + ) :  ^a *  ^a ->  ^a) and
          ^a : (static member ( % ) :  ^a *  ^a ->  ^a) and
          ^a : (static member ( / ) :  ^a *  ^a ->  ^a) and
          ^a0 : (static member get_One : ->  ^a0)

记录类型技术

module LP =
    let inline zero_of (target:'a) : 'a = LanguagePrimitives.GenericZero<'a>
    let inline one_of (target:'a) : 'a = LanguagePrimitives.GenericOne<'a>
    let inline two_of (target:'a) : 'a = one_of(target) + one_of(target)
    let inline three_of (target:'a) : 'a = two_of(target) + one_of(target)
    let inline negone_of (target:'a) : 'a = zero_of(target) - one_of(target)

    let inline any_of (target:'a) (x:int) : 'a =
        let one:'a = one_of target
        let zero:'a = zero_of target
        let xu = if x > 0 then 1 else -1
        let gu:'a = if x > 0 then one else zero-one

        let rec get i g = 
            if i = x then g
            else get (i+xu) (g+gu)
        get 0 zero 

    type G<'a> = {
        negone:'a
        zero:'a
        one:'a
        two:'a
        three:'a
        any: int -> 'a
    }    

    let inline G_of (target:'a) : (G<'a>) = {
        zero = zero_of target
        one = one_of target
        two = two_of target
        three = three_of target
        negone = negone_of target
        any = any_of target
    }

open LP
交叉脚注,不需要注释即可得到良好的推断签名:
let inline crossfoot3 n =
    let g = G_of n
    let ten = g.any 10
    let rec compute n =
        if n = g.zero then g.zero
        else n % ten + compute (n / ten)
    compute n

val inline crossfoot3 :
   ^a ->  ^a
    when  ^a : (static member ( % ) :  ^a *  ^a ->  ^b) and
         ( ^b or  ^a) : (static member ( + ) :  ^b *  ^a ->  ^a) and
          ^a : (static member get_Zero : ->  ^a) and
          ^a : (static member get_One : ->  ^a) and
          ^a : (static member ( + ) :  ^a *  ^a ->  ^a) and
          ^a : (static member ( - ) :  ^a *  ^a ->  ^a) and  ^a : equality and
          ^a : (static member ( / ) :  ^a *  ^a ->  ^a)

交叉脚注,不带注释,接受预先计算的G实例:

let inline crossfootG g ten n =
    let rec compute n =
        if n = g.zero then g.zero
        else n % ten + compute (n / ten)
    compute n

val inline crossfootG :
  G< ^a> ->  ^b ->  ^a ->  ^a
    when ( ^a or  ^b) : (static member ( % ) :  ^a *  ^b ->  ^c) and
         ( ^c or  ^a) : (static member ( + ) :  ^c *  ^a ->  ^a) and
         ( ^a or  ^b) : (static member ( / ) :  ^a *  ^b ->  ^a) and
          ^a : equality

我在实践中使用上述方法,因为我可以制作预计算的类型特定版本,这些版本不会遭受通用语言基元的性能损失:

let gn = G_of 1  //int32
let gL = G_of 1L //int64
let gI = G_of 1I //bigint

let gD = G_of 1.0  //double
let gS = G_of 1.0f //single
let gM = G_of 1.0m //decimal

let crossfootn = crossfootG gn (gn.any 10)
let crossfootL = crossfootG gL (gL.any 10)
let crossfootI = crossfootG gI (gI.any 10)
let crossfootD = crossfootG gD (gD.any 10)
let crossfootS = crossfootG gS (gS.any 10)
let crossfootM = crossfootG gM (gM.any 10)

哇,感谢您为了回答我的初学者问题而花费了这么多的时间。我需要更多的投入来理解这一切。 - primfaktor
我理解的是,NumericLiteralG.FromInt32(int) 是通过Peano公理构造一个通用数字。这不是很慢吗? - primfaktor
Peano公理在这里过于严格,但概念是接近的。是的,它很慢。虽然@kvb在此答案结尾处有更快的实现:https://dev59.com/NVPTa4cB1Zd3GeqPkIvJ#4741799。 - Stephen Swensen
我们使用类似的技术来编译快速FFT代码,适用于F# for Numerics库中的数组、向量、矩阵等。 - J D

14

既然涉及使用广义数字文字时如何使类型签名更简明,我想说一下我的建议。主要问题是F#的运算符可以是不对称的,所以您可以执行类似 System.DateTime.Now + System.TimeSpan.FromHours(1.0) 的操作,这意味着当执行算术运算时,F#的类型推断会添加中间类型变量。

在数值算法的情况下,这种潜在的不对称性通常并不实用,导致类型签名的爆炸相当丑陋(尽管通常不影响F#在给定具体参数时正确应用函数)。解决此问题的一个潜在方法是限制您关心的范围内算术运算符的类型。 例如,如果您定义了这个模块:

module SymmetricOps =
  let inline (+) (x:'a) (y:'a) : 'a = x + y
  let inline (-) (x:'a) (y:'a) : 'a = x - y
  let inline (*) (x:'a) (y:'a) : 'a = x * y
  let inline (/) (x:'a) (y:'a) : 'a = x / y
  let inline (%) (x:'a) (y:'a) : 'a = x % y
  ...

那么,您只需在需要运算符仅应用于两个相同类型的参数时打开SymmetricOps模块。因此,现在我们可以定义:

module NumericLiteralG = 
  open SymmetricOps
  let inline FromZero() = LanguagePrimitives.GenericZero
  let inline FromOne() = LanguagePrimitives.GenericOne
  let inline FromInt32 (n:int) =
      let one = FromOne()
      let zero = FromZero()
      let n_incr = if n > 0 then 1 else -1
      let g_incr = if n > 0 then one else (zero - one)
      let rec loop i g = 
          if i = n then g
          else loop (i + n_incr) (g + g_incr)
      loop 0 zero

并且

open SymmetricOps
let inline crossfoot x =
  let rec compute n =
      if n = 0G then 0G
      else n % 10G + compute (n / 10G)
  compute x

推断类型相对清晰

val inline crossfoot :
   ^a ->  ^a
    when  ^a : (static member ( - ) :  ^a *  ^a ->  ^a) and
          ^a : (static member get_One : ->  ^a) and
          ^a : (static member ( % ) :  ^a *  ^a ->  ^a) and
          ^a : (static member get_Zero : ->  ^a) and
          ^a : (static member ( + ) :  ^a *  ^a ->  ^a) and
          ^a : (static member ( / ) :  ^a *  ^a ->  ^a) and  ^a : equality

虽然我们仍然可以从一个漂亮易读的定义中获益,来自crossfoot


1
这个应该标记为答案,因为它提供了直接的用法(函数的读取方式符合预期),并且实现最佳(类型推断很干净)。 - Daniel
@Stephen - 注意,如果你担心性能问题,定义一个更高效(但不太优雅)的NumericLiteralG.FromInt32方法并不太难。 - kvb
@kvb,是的,这是我担心的一部分,我记得之前看到你在某个地方发布了一个更有效的版本,但它是否如此高效以至于可以真正消除明显的拖延?我还担心GenericZero/One(prim-types.fs第2304行)的实现,它们针对“原始”数字类型进行静态优化,但例如对于bigint和bignum使用反射。 - Stephen Swensen
@kvb - 如果您想增强FromInt32的代表性,请随意回答此问题:https://dev59.com/NVPTa4cB1Zd3GeqPkIvJ - Daniel
1
@Stephen - 是的,不应该使用GenericZero(和GenericOne)会导致使用反射-对bigint.Zero的调用应直接内联。我不完全确定为什么存在动态实现,但它确实允许其他语言(或从F#中反射地调用函数)调用这些函数-但是请注意,对于某些运算符(例如“(-)”),没有动态版本... - kvb
显示剩余4条评论

2
我在寻找解决方案时偶然发现了这个主题,我正在发布我的答案,因为我找到了一种表达通用数字的方法,而不是通过手动构建数字实现不太理想的方法。
open System.Numerics
// optional
open MathNet.Numerics

module NumericLiteralG = 
    type GenericNumber = GenericNumber with
        static member instance (GenericNumber, x:int32, _:int8) = fun () -> int8 x
        static member instance (GenericNumber, x:int32, _:uint8) = fun () -> uint8 x
        static member instance (GenericNumber, x:int32, _:int16) = fun () -> int16 x
        static member instance (GenericNumber, x:int32, _:uint16) = fun () -> uint16 x
        static member instance (GenericNumber, x:int32, _:int32) = fun () -> x
        static member instance (GenericNumber, x:int32, _:uint32) = fun () -> uint32 x
        static member instance (GenericNumber, x:int32, _:int64) = fun () -> int64 x
        static member instance (GenericNumber, x:int32, _:uint64) = fun () -> uint64 x
        static member instance (GenericNumber, x:int32, _:float32) = fun () -> float32 x
        static member instance (GenericNumber, x:int32, _:float) = fun () -> float x
        static member instance (GenericNumber, x:int32, _:bigint) = fun () -> bigint x
        static member instance (GenericNumber, x:int32, _:decimal) = fun () -> decimal x
        static member instance (GenericNumber, x:int32, _:Complex) = fun () -> Complex.op_Implicit x
        static member instance (GenericNumber, x:int64, _:int64) = fun () -> int64 x
        static member instance (GenericNumber, x:int64, _:uint64) = fun () -> uint64 x
        static member instance (GenericNumber, x:int64, _:float32) = fun () -> float32 x
        static member instance (GenericNumber, x:int64, _:float) = fun () -> float x
        static member instance (GenericNumber, x:int64, _:bigint) = fun () -> bigint x
        static member instance (GenericNumber, x:int64, _:decimal) = fun () -> decimal x
        static member instance (GenericNumber, x:int64, _:Complex) = fun () -> Complex.op_Implicit x
        static member instance (GenericNumber, x:string, _:float32) = fun () -> float32 x
        static member instance (GenericNumber, x:string, _:float) = fun () -> float x
        static member instance (GenericNumber, x:string, _:bigint) = fun () -> bigint.Parse x
        static member instance (GenericNumber, x:string, _:decimal) = fun () -> decimal x
        static member instance (GenericNumber, x:string, _:Complex) = fun () -> Complex(float x, 0.0)
        // MathNet.Numerics
        static member instance (GenericNumber, x:int32, _:Complex32) = fun () -> Complex32.op_Implicit x
        static member instance (GenericNumber, x:int32, _:bignum) = fun () -> bignum.FromInt x
        static member instance (GenericNumber, x:int64, _:Complex32) = fun () -> Complex32.op_Implicit x
        static member instance (GenericNumber, x:int64, _:bignum) = fun () -> bignum.FromBigInt (bigint x)
        static member instance (GenericNumber, x:string, _:Complex32) = fun () -> Complex32(float32 x, 0.0f)
        static member instance (GenericNumber, x:string, _:bignum) = fun () -> bignum.FromBigInt (bigint.Parse x)

    let inline genericNumber num = Inline.instance (GenericNumber, num) ()

    let inline FromZero () = LanguagePrimitives.GenericZero
    let inline FromOne () = LanguagePrimitives.GenericOne
    let inline FromInt32 n = genericNumber n
    let inline FromInt64 n = genericNumber n
    let inline FromString n = genericNumber n

这个实现过程在转换期间没有复杂的迭代。它使用FsControl作为Instance模块。

http://www.fssnip.net/mv


1

您是想要进行十字足或者只是对长数字的各位数求和呢?

如果您只是想要对各位数求和,那么:

let crossfoot (x:'a) = x.ToString().ToCharArray()
                       |> (Array.fold(fun acc x' -> if x' <> '.' 
                                                    then acc + (int x')
                                                    else acc) 0)

...然后你就完成了。

无论如何,你能把东西转换成字符串,去掉小数点,记住小数点的位置,将其解释为整数,运行交叉脚吗?

这是我的解决方案。我不确定当你添加小数点时,“交叉脚”应该如何工作。

例如,你想要:crossfoot(123.1) = 7还是crossfoot(123.1) = 6.1?(我假设你想要后者)

无论如何,这段代码允许你使用数字作为通用类型。

let crossfoot (n:'a) = // Completely generic input

    let rec crossfoot' (a:int) =       // Standard integer crossfoot
        if a = 0 then 0 
        else a%10 + crossfoot' (a / 10)

    let nstr = n.ToString()

    let nn   = nstr.Split([|'.'|])    // Assuming your main constraint is float/int

    let n',n_ = if nn.Length > 1 then nn.[0],nn.[1] 
                else nn.[0],"0"

    let n'',n_' = crossfoot'(int n'),crossfoot'(int n_)

    match n_' with
    | 0 -> string n''
    | _ -> (string n'')+"."+(string n_')

如果您需要输入大整数或int64类型的数据,可以使用交叉相加的方法,将大数字拆分成小块(字符串),并将它们馈送到此函数中进行相加。

是的,那也是一种可能性。我只是在谈论整数交叉脚。但使用字符串意味着它变得不太类型安全。我不想能够说“crossfoot”no number"”。我认为我最喜欢使用inline的成员约束。 - primfaktor

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接