FSharpPlus divRem - 它是如何工作的?
FSharpPlus divRem - how does it work?
看着 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 的效率最佳。然而还有一个问题:
为什么要引入"ambiguity"(o1)?
我们称这三个重载为 o1、o2、o3
如果我们注释掉 o1 并使用类型未实现 DivRem(例如 int 或 float)的数字参数调用 divRem,则由于成员约束,无法使用 o3。编译器可以选择 o2,但它没有,就像它所说的 "you have a perfect signature matching overload o3 (so I will ignore the less than perfect signature in o2) but the member constraint is not fulfilled"。因此,如果我取消注释 o1,我希望它显示 "you have two perfect signature overloads (so I will ignore the less than perfect signature in o2) but both of them have unfulfilled constraints"。相反,它似乎在说 "you have two perfect signature overloads but both of them have unfulfilled constraints, so I will take o2 that, even with a less than perfect signature, can do the job"。即使在第一个实例中,避免 o1 技巧并让编译器说 "your perfect signature overload o3 has an unfulfilled member constraint, so I take o2 which is less than perfect in signature but can do the job" 不是更正确吗?
你的实现很好,其实和第二个重载一样,对应默认实现
F#+是一个F#基础库,类似于F#核心,同样使用fallback机制。
F# 核心使用静态优化并以不安全的方式伪造某些类型约束,但这种技术在 F# 编译器项目之外是不可能的,因此 F#+ 通过对重载方法的特征调用实现相同的效果,而无需伪造静态约束。
因此,您的实现与 F#+ 中的实现之间的唯一区别是 F#+ 将首先(在编译时)查找 [=35= 中定义的 DivRem
静态成员] 正在使用的数字类型,具有标准的 .NET 签名(使用 return 值和引用,而不是元组),这是第三个重载。这个方法可以有一个优化的、特定的实现。我的意思是,假设如果存在这种方法,它在最坏的情况下与默认定义同样最优。
如果此方法不存在,它将回退到默认定义,正如我所说,这是第二次重载。
第一个重载永远不会匹配,它的存在只是为了在重载集中产生必要的歧义。
这项技术目前还没有很好的记录,因为微软的 example in the docs 有点不幸,因为它并没有真正起作用,(可能是因为它没有足够的歧义),但是@rmunn的回答有很详细的解释。
编辑
关于您的问题更新:这不是 F# 编译器的工作方式,至少现在是这样。重载决议后正在解决静态约束,当这些约束不满足时它不会回溯。
添加另一个带有约束的方法会使问题复杂化,迫使编译器在最终重载解析之前进行一些约束求解。
这些天我们some discussions质疑是否应该纠正这种行为,这似乎不是一件小事。
先来看看documentation on overloaded methods,没啥好说的:
Overloaded methods are methods that have identical names in a given type but that have different arguments. In F#, optional arguments are usually used instead of overloaded methods. However, overloaded methods are permitted in the language, provided that the arguments are in tuple form, not curried form.
(强调我的)。要求参数采用元组形式的原因是因为编译器必须能够知道,在调用函数的地方,正在调用哪个重载。例如,如果我们有:
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
函数,因为它不知道 此时代码 [=16] 的哪个版本=] 应该被调用。所以这段代码会产生歧义。
现在,查看 DivRem
class 中的三个重载静态方法,它们具有三种不同的类型签名:
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
约束意味着"the provided type must support the null literal"(不包括值类型),而struct
约束的意思是"the provided type must be a .NET value type"。所以实际上永远无法选择第一个重载;正如古斯塔沃在他的出色回答中指出的那样,它的存在只是为了让编译器能够处理这个 class。 (尝试省略第一个重载并调用 divRem 5m 3m
:你会发现它无法编译并出现错误:
The type 'decimal' does not support the operator 'DivRem'
所以第一个重载的存在只是为了欺骗 F# 编译器做正确的事情。然后我们将忽略它并继续进行第二个和第三个重载。
现在,第二个和第三个重载的区别在于第三个参数的类型。第二个重载的参数是基础 class (Default1
),第三个重载的参数是派生的 class (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
静态方法,并且 returns 一个 quotient, remainder
.
的元组
最后,如果提供的类型没有 DivRem
静态方法,则第二个重载(签名中带有 Default1
的重载)将最终被调用。该函数在提供的类型上查找重载的 *
、-
和 /
运算符,并使用它们计算商和余数。
换句话说,正如 Gustavo 的简短回答所解释的那样,此处的 DivRem class 将遵循以下逻辑(在编译器中):
- 如果正在使用的类型有一个静态
DivRem
方法,请调用它,因为假定它可能针对该类型进行了优化。
- 否则,计算商
q
为D / d
,然后计算余数为D - q * d
。
就是这样:剩下的复杂性只是强制 F# 编译器做正确的事情,并最终得到一个尽可能高效的漂亮 divRem
函数。
看着 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 的效率最佳。然而还有一个问题:
为什么要引入"ambiguity"(o1)?
我们称这三个重载为 o1、o2、o3
如果我们注释掉 o1 并使用类型未实现 DivRem(例如 int 或 float)的数字参数调用 divRem,则由于成员约束,无法使用 o3。编译器可以选择 o2,但它没有,就像它所说的 "you have a perfect signature matching overload o3 (so I will ignore the less than perfect signature in o2) but the member constraint is not fulfilled"。因此,如果我取消注释 o1,我希望它显示 "you have two perfect signature overloads (so I will ignore the less than perfect signature in o2) but both of them have unfulfilled constraints"。相反,它似乎在说 "you have two perfect signature overloads but both of them have unfulfilled constraints, so I will take o2 that, even with a less than perfect signature, can do the job"。即使在第一个实例中,避免 o1 技巧并让编译器说 "your perfect signature overload o3 has an unfulfilled member constraint, so I take o2 which is less than perfect in signature but can do the job" 不是更正确吗?
你的实现很好,其实和第二个重载一样,对应默认实现
F#+是一个F#基础库,类似于F#核心,同样使用fallback机制。 F# 核心使用静态优化并以不安全的方式伪造某些类型约束,但这种技术在 F# 编译器项目之外是不可能的,因此 F#+ 通过对重载方法的特征调用实现相同的效果,而无需伪造静态约束。
因此,您的实现与 F#+ 中的实现之间的唯一区别是 F#+ 将首先(在编译时)查找 [=35= 中定义的 DivRem
静态成员] 正在使用的数字类型,具有标准的 .NET 签名(使用 return 值和引用,而不是元组),这是第三个重载。这个方法可以有一个优化的、特定的实现。我的意思是,假设如果存在这种方法,它在最坏的情况下与默认定义同样最优。
如果此方法不存在,它将回退到默认定义,正如我所说,这是第二次重载。
第一个重载永远不会匹配,它的存在只是为了在重载集中产生必要的歧义。
这项技术目前还没有很好的记录,因为微软的 example in the docs 有点不幸,因为它并没有真正起作用,(可能是因为它没有足够的歧义),但是@rmunn的回答有很详细的解释。
编辑
关于您的问题更新:这不是 F# 编译器的工作方式,至少现在是这样。重载决议后正在解决静态约束,当这些约束不满足时它不会回溯。
添加另一个带有约束的方法会使问题复杂化,迫使编译器在最终重载解析之前进行一些约束求解。
这些天我们some discussions质疑是否应该纠正这种行为,这似乎不是一件小事。
先来看看documentation on overloaded methods,没啥好说的:
Overloaded methods are methods that have identical names in a given type but that have different arguments. In F#, optional arguments are usually used instead of overloaded methods. However, overloaded methods are permitted in the language, provided that the arguments are in tuple form, not curried form.
(强调我的)。要求参数采用元组形式的原因是因为编译器必须能够知道,在调用函数的地方,正在调用哪个重载。例如,如果我们有:
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
函数,因为它不知道 此时代码 [=16] 的哪个版本=] 应该被调用。所以这段代码会产生歧义。
现在,查看 DivRem
class 中的三个重载静态方法,它们具有三种不同的类型签名:
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
约束意味着"the provided type must support the null literal"(不包括值类型),而struct
约束的意思是"the provided type must be a .NET value type"。所以实际上永远无法选择第一个重载;正如古斯塔沃在他的出色回答中指出的那样,它的存在只是为了让编译器能够处理这个 class。 (尝试省略第一个重载并调用 divRem 5m 3m
:你会发现它无法编译并出现错误:
The type 'decimal' does not support the operator 'DivRem'
所以第一个重载的存在只是为了欺骗 F# 编译器做正确的事情。然后我们将忽略它并继续进行第二个和第三个重载。
现在,第二个和第三个重载的区别在于第三个参数的类型。第二个重载的参数是基础 class (Default1
),第三个重载的参数是派生的 class (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
静态方法,并且 returns 一个 quotient, remainder
.
最后,如果提供的类型没有 DivRem
静态方法,则第二个重载(签名中带有 Default1
的重载)将最终被调用。该函数在提供的类型上查找重载的 *
、-
和 /
运算符,并使用它们计算商和余数。
换句话说,正如 Gustavo 的简短回答所解释的那样,此处的 DivRem class 将遵循以下逻辑(在编译器中):
- 如果正在使用的类型有一个静态
DivRem
方法,请调用它,因为假定它可能针对该类型进行了优化。 - 否则,计算商
q
为D / d
,然后计算余数为D - q * d
。
就是这样:剩下的复杂性只是强制 F# 编译器做正确的事情,并最终得到一个尽可能高效的漂亮 divRem
函数。