看着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来看,null
和struct
约束是相反的: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
函数。