F#中通用类型的通用算术操作

4
我找到了一篇不错的文章:http://nut-cracker.azurewebsites.net/blog/2011/08/09/operator-overloading/,然后决定使用这篇文章中的信息来尝试一下 F#。我创建了自己的类型 Vector2D<'a>,支持加法和常数乘法:
type Vector2D<'a> = Vector2D of 'a * 'a
    with
        member this.X = let (Vector2D (x,_)) = this in x
        member this.Y = let (Vector2D (_,y)) = this in y
        static member inline (+) (Vector2D (lhsx, lhsy), Vector2D (rhsx, rhsy)) =
            Vector2D(lhsx + rhsx, lhsy + rhsy)
        static member inline ( * ) (factor, Vector2D (x, y)) =
            Vector2D (factor * x, factor * y)
        static member inline ( * ) (Vector2D (x, y), factor) =
            Vector2D (factor * x, factor * y)

这很好,我在使用这些成员时没有观察到任何问题。 然后我决定加入对常数除法的支持(以便实现例如向量归一化):

    static member inline (/) (Vector2D (x, y), factor) =
        Vector2D (x / factor, y / factor)

事情是虽然它可以正常编译,但每次我尝试使用它时,都会出现错误:
let inline doSomething (v : Vector2D<_>) =
    let l = LanguagePrimitives.GenericOne
    v / l // error

错误信息为:
一个类型参数缺少约束条件'when ( ^a or ^?18259) : (static member ( / ) : ^a * ^?18259 -> ^?18260)'
此外,doSomething 的推断类型是: Vector2D<'a> -> Vector2D<'c>(需要成员 (/)、成员 (/) 和成员 get_One)
我的问题是:为什么可以使用成员 (*) 和 (+),但是使用 (/) 却不可能,即使它们以相同的方式定义? 另外,为什么推断类型说需要两个 (/) 成员?

1
你正在做某件事情,我觉得我以前见过。观察结果:当尝试这个时,IntelliSense进入了一个无限循环,甚至在此之前也没有预测到错误。删除向量上的(*)的第一个定义会导致相同的乘法错误。在doSomething中,l的类型是未定义的,然而,将其注释为v的组件类型并不能解决问题。我曾经遇到过类似的困惑,当我尝试使用通用向量来推断矩阵逻辑时。我放弃了,因为我再也无法理解IDE和编译器在做什么了。 - Vandroiy
1
小注释 - 你需要在 (*) 中间加上空格,否则它们可能会被解释为开始/结束注释。 - John Palmer
1个回答

3
它说需要两次斜杠,因为类型不一样,所以你的函数将比你期望的更通用。
设计通用数学库时必须做出一些决策。不能只是随便开始而不注释所有类型或限制数学运算符,类型推断将无法提出类型。
问题在于F#算术运算符没有被限制,它们有一个“开放”的签名:'a->'b->'c,所以你要么限制它们,要么就必须完全注释所有函数,因为如果你不这样做,F#将无法知道哪个重载应该使用。
如果你问我,我会采取第一种方法(也是Haskell的方法),这在开始时有点复杂,但一旦你设置好一切,实现新的通用类型及其操作就相当快了。
解释这需要另一篇博客文章(我总有一天会写),但是我会给你一个简短的例子:
// math operators restricted to 'a->'a->'a
let inline ( + ) (a:'a) (b:'a) :'a = a + b
let inline ( - ) (a:'a) (b:'a) :'a = a - b
let inline ( * ) (a:'a) (b:'a) :'a = a * b
let inline ( / ) (a:'a) (b:'a) :'a = a / b

type Vector2D<'a> = Vector2D of 'a * 'a
    with
        member this.X = let (Vector2D (x,_)) = this in x
        member this.Y = let (Vector2D (_,y)) = this in y
        static member inline (+) (Vector2D (lhsx, lhsy), Vector2D (rhsx, rhsy)) =
            Vector2D(lhsx + rhsx, lhsy + rhsy)
        static member inline (*) (Vector2D (x1, y1), Vector2D (x2, y2)) =
            Vector2D (x1 * x2, y1 * y2)
        static member inline (/) (Vector2D (x1, y1), Vector2D (x2, y2)) =
            Vector2D (x1 / x2, y1 / y2)
        static member inline get_One () :Vector2D<'N> =
            let one:'N = LanguagePrimitives.GenericOne
            Vector2D (one, one)

let inline doSomething1 (v : Vector2D<_>) = v * LanguagePrimitives.GenericOne
let inline doSomething2 (v : Vector2D<_>) = v / LanguagePrimitives.GenericOne

所以,这个想法不是定义涉及您的类型和数字类型的所有重载,而是定义从数字类型到您的类型的转换以及仅在您的类型之间进行操作。
现在,如果您想将您的向量乘以另一个向量或整数,则在两种情况下都使用相同的重载:
let inline multByTwo (vector:Vector2D<_>) =
    let one = LanguagePrimitives.GenericOne
    let two = one + one
    vector * two  // picks up the same overload as for vector * vector

当然,现在你遇到的问题是生成通用数字,这是另一个密切相关的主题。关于如何生成NumericLiteralG模块,在SO上有许多(不同的)答案。F#+是一个提供了这种模块实现的库,因此您可以编写 vector * 10G 你可能会想为什么F#运算符没有像Haskell等其他语言一样被限制。这是因为一些现有的.NET类型已经以这种方式定义,一个简单的例子是DateTime,它支持添加一个整数,该整数被任意决定表示一天,更多讨论请参见here 更新:
回答您的后续问题,如何乘以浮点数,同样是一个完整的主题:如何创建具有许多可能解决方案的通用数字,取决于您希望库有多通用和可扩展。无论如何,以下是一个简单的方法来获取一些功能:
let inline convert (x:'T) :'U = ((^T or ^U): (static member op_Explicit : ^T -> ^U) x)

let inline fromNumber x =  // you can add this function as a method of Vector2D
    let d = convert x
    Vector2D (d, d) 

let result1 = Vector2D (2.0f , 4.5f ) * fromNumber 1.5M
let result2 = Vector2D (2.0  , 4.5  ) * fromNumber 1.5M

F#+使用有理数而不是十进制数来执行类似操作,以保留更多的精度。


哇,看起来很不错!现在我明白我的错误了——我只是没有意识到这些函数太通用了。感谢您让我清楚地认识到了这一点。我理解操作(如乘法等)的工作方式。你能向我解释一下如何实现浮点数(例如1.5)的乘法吗?是否有更好的方法,而不是将其写成分数,然后乘以分子并除以分母呢? - LA.27
1
@AlojzyLeszcz 这是一个很大的话题,但请查看更新以获取简单的方法。 - Gus
太好了 - 我现在感觉聪明多了!:D。非常感谢你! - LA.27
@Gustavo,定义受限运算符在类型上而不是全局上是否更安全?后者至少在FSI中会造成混乱。我考虑采用传统的方法,例如 type Foo = Foo with static member inline (|+) (Foo, (a : 'a, b : 'a)) : 'a = a + b,并在调用站点使用 static member inline (+) (Vector2D (lhsx, lhsy), Vector2D (rhsx, rhsy)) = Vector2D(Foo |+ (lhsx, rhsx), Foo |+ (lhsy, rhsy)),这将编译为相同的约束和签名。 - kaefer
@kaefer 是的,我做了一个快速示例,但我同意,不认为将它们始终全局限制是一个好主意,因为您可能希望与可能具有不同定义的现有.NET代码进行交互。一个类似于您建议的选项,我觉得很有用的是在模块RestrictedOps中定义它们,然后您可以决定何时将它们引入作用域。正如您所说,在定义静态成员之前打开它们,那么它们就会自动受限,但您也可以随时在客户端代码中打开它们。另一个选择是使用不同的名称,就像您写的那个例子一样。 - Gus

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接