看着FSharpPlus,我正在思考如何创建一个通用函数以用于

let qr0  = divRem 7  3
let qr1  = divRem 7I 3I
let qr2  = divRem 7. 3.


并提出了可能的(可行的)解决方案

let inline divRem (D:^T) (d:^T): ^T * ^T = let q = D / d in q,  D - q * d


然后我查看了FSharpPlus如何实现它,然后发现:

open System.Runtime.InteropServices

type Default6 = class end
type Default5 = class inherit Default6 end
type Default4 = class inherit Default5 end
type Default3 = class inherit Default4 end
type Default2 = class inherit Default3 end
type Default1 = class inherit Default2 end

type DivRem =
    inherit Default1
    static member inline DivRem (x:^t when ^t: null and ^t: struct, y:^t, _thisClass:DivRem) = (x, y)
    static member inline DivRem (D:'T, d:'T, [<Optional>]_impl:Default1) = let q = D / d in q,  D - q * d
    static member inline DivRem (D:'T, d:'T, [<Optional>]_impl:DivRem  ) =
        let mutable r = Unchecked.defaultof<'T>
        (^T: (static member DivRem: _ * _ -> _ -> _) (D, d, &r)), r

    static member inline Invoke (D:'T) (d:'T) :'T*'T =
        let inline call_3 (a:^a, b:^b, c:^c) = ((^a or ^b or ^c) : (static member DivRem: _*_*_ -> _) b, c, a)
        let inline call (a:'a, b:'b, c:'c) = call_3 (a, b, c)
        call (Unchecked.defaultof<DivRem>, D, d)

let inline divRem (D:'T) (d:'T) :'T*'T = DivRem.Invoke D d


我确信有充分理由这样做。但是,我对为什么这样做会不感兴趣,但是:

这是如何运作的?

是否有任何文档可帮助您了解此语法的工作原理,尤其是三个DivRem静态方法重载?

编辑

因此,FSharp +实现的优点是,如果divRem调用中使用的数字类型实现了DivRem静态成员(例如,例如BigInteger),它将代替可能存在的算术运算符。假定DivRem比调用默认运算符更有效,这将使divRem的效率达到最佳。但是仍然存在一个问题:

为什么我们需要引入“歧义”(o1)?

我们称三个重载为o1,o2,o3

如果我们注释掉o1并使用其类型不实现DivRem的数字参数(例如int或float)调用divRem,则由于成员约束而不能使用o3。编译器可以选择o2,但不能这样做,就像它说的那样:“您有一个匹配重载o3的完美签名(因此我将忽略o2中小于完美的签名),但未满足成员约束”。因此,如果我取消对o1的注释,我会期望它说“您有两个完美的签名重载(因此我将忽略o2中不完美的签名),但它们都具有未实现的约束”。相反,它似乎说“您有两个完美的签名重载,但它们都具有未实现的约束,因此我认为即使签名不那么完美,O2仍能胜任”。避免使用o1技巧并让编译器说“您的完美签名重载o3有一个未实现的成员约束,这不是更正确的做法,所以我认为o2在签名方面不尽人意,但可以做到”实例?

最佳答案

首先,让我们看一下documentation on overloaded methods,它没有什么要说的:


重载方法是在给定类型中名称相同但参数不同的方法。在F#中,通常使用可选参数代替重载方法。但是,该语言允许重载方法,前提是参数采用元组形式而不是咖喱形式。


(强调我的)。之所以要求参数采用元组形式,是因为编译器必须能够在调用函数的那一刻知道正在调用的重载。例如,如果我们有:

let f (a : int) (b : string) = printf "%d %s" a b
let f (a : int) (b : int) = printf "%d %d" a b

let g = f 5


然后,编译器将无法编译g函数,因为此时代码中尚不知道应调用哪个版本的f。因此,这段代码是模棱两可的。

现在,查看DivRem类中的这三个重载静态方法,它们具有三个不同的类型签名:

static member inline DivRem (x:^t when ^t: null and ^t: struct, y:^t, _thisClass:DivRem)
static member inline DivRem (D:'T, d:'T, [<Optional>]_impl:Default1)
static member inline DivRem (D:'T, d:'T, [<Optional>]_impl:DivRem  )


此时,您可能会问自己,编译器将如何在这些静态重载之间进行选择:如果省略了第三个参数,并且给出了第三个参数,但第二个和第三个参数是DivRem的实例,则第二个和第三个似乎似乎无法区分。 ,那么它与第一个重载看起来就模棱两可了。此时,将代码粘贴到F#Interactive会话中可能会有所帮助,因为F#Interactive将生成更具体的类型签名,这可能会更好地解释它。这是将代码粘贴到F#Interactive中时得到的:

type DivRem =
  class
    inherit Default1
    static member
      DivRem : x: ^t * y: ^t * _thisClass:DivRem -> ^t *  ^t
                 when ^t : null and ^t : struct
    static member
      DivRem : D: ^T * d: ^T * _impl:Default1 -> ^a *  ^c
                 when ^T : (static member ( / ) : ^T * ^T -> ^a) and
                      ( ^T or  ^b) : (static member ( - ) : ^T * ^b -> ^c) and
                      ( ^a or  ^T) : (static member ( * ) : ^a * ^T -> ^b)
    static member
      DivRem : D: ^T * d: ^T * _impl:DivRem -> 'a * ^T
                 when ^T : (static member DivRem : ^T * ^T * byref< ^T> -> 'a)
    static member
      Invoke : D: ^T -> d: ^T -> ^T *  ^T
                 when (DivRem or ^T) : (static member DivRem : ^T * ^T * DivRem -> ^T * ^T)
  end


这里的第一个DivRem实现最容易理解。其类型签名与FSharpPlus源代码中定义的类型签名相同。从documentation on constraints来看,nullstruct约束是相反的:null约束表示“提供的类型必须支持空文字”(不包括值类型),而struct约束表示“提供的类型必须是.NET值类型”。因此,第一个重载实际上是不可能选择的。正如古斯塔沃(Gustavo)在其出色的回答中指出的那样,它的存在仅是为了使编译器能够处理此类。 (尝试省略第一个重载并调用divRem 5m 3m:您会发现它无法编译并显示以下错误:


“十进制”类型不支持运算符“ DivRem”


因此,第一个重载只是为了欺骗F#编译器做正确的事情。然后,我们将忽略它,并继续进行第二和第三次重载。

现在,第二个和第三个重载在第三个参数的类型上有所不同。第二个重载的参数是基类(Default1),第三个重载的参数是派生类(DivRem)。这些方法将始终以DivRem实例作为第三个参数来调用,那么为什么要选择第二个方法呢?答案在于为第三种方法自动生成的类型签名:

static member
  DivRem : D: ^T * d: ^T * _impl:DivRem -> 'a * ^T
             when ^T : (static member DivRem : ^T * ^T * byref< ^T> -> 'a)


static member DivRem参数约束是由以下行生成的:

(^T: (static member DivRem: _ * _ -> _ -> _) (D, d, &r)), r


发生这种情况是由于F#编译器如何处理具有out参数的函数调用。在C#中,正在寻找的DivRem静态方法是带有参数(a, b, out c)的方法。 F#编译器将该签名转换为签名(a, b) -> c。因此,此类型约束将查找像BigInteger.DivRem这样的静态方法,并使用参数(D, d, &r)对其进行调用,其中F#中的&r类似于C#中的out r。调用的结果是商,它将余数分配给方法的out参数。因此,此重载仅对提供的​​类型调用DivRem静态方法,并返回quotient, remainder的元组。

最后,如果所提供的类型没有DivRem静态方法,则第二个重载(签名中带有Default1的重载)将最终被调用。这将在提供的类型上查找重载的*-/运算符,并使用它们来计算商和余数。

换句话说,正如古斯塔沃的简短回答所解释的那样,此处的DivRem类将遵循以下逻辑(在编译器中):


如果正在使用的类型上有一个静态的DivRem方法,请调用它,因为它假定可以针对该类型进行优化。
否则,将商q计算为D / d,然后将余数计算为D - q * d


就是这样:其余的复杂性只是迫使F#编译器做正确的事,最后得到一个看起来效率最高的divRem函数。

07-25 23:21