gpt4 book ai didi

generics - FSharpPlus divRem - 它是如何工作的?

转载 作者:行者123 更新时间:2023-12-02 15:35:36 25 4
gpt4 key购买 nike

看着 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 ,没什么好说的:

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函数,因为此时它不知道代码中的版本 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 session 会有所帮助,因为 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 值类型”。因此,实际上永远无法选择第一个重载;正如古斯塔沃在他出色的回答中指出的那样,它的存在只是为了让编译器能够处理这个类。 (尝试省略第一个重载并调用 divRem 5m 3m :您会发现它无法编译并显示错误:

The type 'decimal' does not support the operator '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) 调用它哪里 &r在 F# 中就像 out r在 C# 中。该调用的结果是商,并将余数分配给 out给方法的参数。所以这个重载只是调用 DivRem所提供类型的静态方法,并返回一个元组 quotient, remainder .

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

换句话说,正如古斯塔沃的简短回答所解释的那样,这里的 DivRem 类将遵循以下逻辑(在编译器中):
  • 如果有静态 DivRem正在使用的类型上的方法,调用它,因为它假定它可以针对该类型进行优化。
  • 否则,计算商 qD / d ,然后计算余数为 D - q * d .

  • 就是这样:其余的复杂性只是强制 F# 编译器做正确的事情,并最终得到一个漂亮的 divRem尽可能高效的功能。

    关于generics - FSharpPlus divRem - 它是如何工作的?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/51129078/

    25 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com