如何为通用数字编写函数?

Posted

技术标签:

【中文标题】如何为通用数字编写函数?【英文标题】:How to write a function for generic numbers? 【发布时间】:2011-01-19 07:17:03 【问题描述】:

我对 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)) 读起来不太好。

没有通用的写法吗?

【问题讨论】:

我担心不会,至少在 .NET 方面不会,因为 .NET 没有所有数字类型的公共子类型(实际上它根本不能,因为所有值类型都必须直接派生自System.ValueType)。话虽如此,我不知道 F#,所以他们可能会以某种方式解决它 - 但不要抱太大希望。 【参考方案1】:

根据 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) 编译时泛型。需要编译时泛型来适应泛型数值运算和鸭子类型 (explicit member constraints) 之类的东西。这些功能是 F# 不可或缺的,但在 .NET 中不受支持,因此必须在编译时由 F# 处理。

插入符号 (^) 用于区分 statically resolved (compile-time) type parameters 和普通符号(使用撇号)。简而言之,'a 在运行时处理,^a 在编译时处理——这就是为什么必须将函数标记为 inline

我以前从未尝试过写这样的东西。结果比我预期的要笨拙。我看到在 F# 中编写通用数字代码的最大障碍是:创建一个非零或一的通用数字的实例。请参阅this answer 中FromInt32 的实现以了解我的意思。 GenericZeroGenericOne 是内置的,它们是使用用户代码中不可用的技术实现的。在这个函数中,因为我们只需要一个小数(10),所以我创建了一个由 10 个GenericOnes 组成的序列并将它们相加。

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

【讨论】:

注意这里crossfoot的疯狂推断类型签名使用数字文字技术而没有添加足够的类型注释。 是的,这就是类型推断大放异彩的时候。我很高兴我们不必明确指定这些约束。 嗯,这并不是我真正要理解的:推断的签名在这里过于笼统,我们实际上可以通过 1)添加一些类型注释来显着加强它,2)使用我开发的技术.见:***.com/questions/2840714/… 我不完全理解你链接的代码(或你的技术——但我没有太多时间看)。我很高兴您优化我的代码并发布改进的答案。 我喜欢 F#,但必须为通用数值运算编写这样的代码可能会让一些新手望而却步。【参考方案2】:

除了 kvb 使用 Numeric Literals 的技术(Brian 的链接)之外,我还使用了一种不同的技术取得了很大的成功,该技术可以产生更好的推断结构类型签名,也可以用于创建预先计算的特定于类型的函数,以便更好地性能以及对支持的数字类型的控制(例如,因为您通常希望支持所有整数类型,但不支持有理类型):F# Static Member Type Constraints。

Daniel 和我一直在讨论不同技术产生的推断类型签名,以下是概述:

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 

不添加任何类型注释的 Crossfoot:

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

Crossfoot,很好的推断签名不需要注释:

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)

Crossfoot,无注释,接受 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

我在实践中使用上述方法,从那时起我可以制作不受通用 LanguagePrimitives 性能成本影响的预计算类型特定版本:

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)

【讨论】:

哇,感谢您将这么多时间用于我的新手问题。我必须投入更多才能了解这一切。 我是否更正NumericLiteralG.FromInt32(int) 通过Peano axioms 从其输入构造一个通用数字?这不是很慢吗? Peano 公理在这里过于严格,但概念很接近。是的,它很慢。虽然@kvb 在这个答案的末尾有一个更快的实现:***.com/questions/4740857/…。 我们使用类似的技术为 F# for Numerics 库在数组、向量、矩阵等上编译快速 FFT 代码。【参考方案3】:

自从出现使用广义数字文字时如何使类型签名不那么繁琐的问题以来,我想我会投入两分钱。主要问题是 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 的良好可读定义中受益。

【讨论】:

这可能应该被标记为答案,因为它提供了简单的用法(函数按照您的预期读取)和最佳实现(类型推断是干净的)。 @Daniel,我不会走那么远 ;) 使用这种技术会显着影响性能,我认为这是不可接受的。 我很想看看这个与你的技术的基准。我已经很久没有写过一些亚秒级重要的东西了,以至于我通常更喜欢可读性而不是性能。 @Stephen - 请注意,如果您担心性能,定义一个更高效(但不太优雅)的NumericLiteralG.FromInt32 方法并不难。 @Stephen - 是的,使用 GenericZero(和 GenericOne)不应导致使用反射 - 对 bigint.Zero 的调用应直接内联。我不是 100% 确定为什么存在动态实现,但它确实允许其他语言调用函数(或从 F# 反射) - 但是,请注意,对于某些运算符(例如 (-)),没有动态版...【参考方案4】:

我在寻找解决方案并发布答案时偶然发现了这个主题,因为我找到了一种表达通用数字的方法,而无需手动构建数字的最佳实现。

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

【讨论】:

【参考方案5】:

crossfoot 正是您想要做的,还是只是将一个长数字的数字相加?

因为如果你只想对数字求和,那么:

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

...你就完成了。

无论如何, 你能把东西转换成字符串,去掉小数点,记住小数点在哪里,把它解释为一个 int,运行 crossfoot 吗?

这是我的解决方案。当您添加小数点时,我不确定您希望“crossfoot”如何工作。

例如,您想要: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 的工作方式,你可以将大数字分成小块(字符串)并将它们输入这个函数,然后将它们加在一起。

【讨论】:

是的,这也是一种可能。我只是在谈论整数交叉脚。但是使用字符串意味着它的类型安全性降低。我不想说crossfoot "no number"。我想我最喜欢inline 的成员约束。

以上是关于如何为通用数字编写函数?的主要内容,如果未能解决你的问题,请参考以下文章

如何为最小堆编写添加函数?

如何为具有泛型类型的箭头函数编写流类型

如何为 Presto 编写自定义窗口函数?

如何为包含特定函数数组的 JSON 对象编写接口

如何为导出函数的节点模块编写打字稿定义文件?

如何为解构对象中的嵌套函数编写类型?