F# 中泛型类型的泛型算术运算

Generic artihmetical operations on generic types in F#

我找到了一篇不错的文章: 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)

我的问题是:为什么可以使用成员 (*) 和 (+),而不能使用 (/),即使它们的定义方式相同? 另外,为什么推断类型说它需要 (/) 成员两次?

它说需要 / 两次,因为类型不一定相同,因此您的函数将比您预期的更通用。

在设计通用数学库时,必须做出一些决定。如果不对所有内容进行类型注释或限制数学运算符,你就不能继续前进,类型推断将无法得出类型。

问题是 F# 算术运算符不受限制,它们有一个 'open' 签名:'a->'b->'c 所以要么限制它们,要么必须对所有函数进行完整类型注释,因为如果你不要 F# 将无法知道应该采用哪个重载。

如果你问我,我会采用第一种方法(也是 Haskell 方法),开始时有点复杂,但是一旦你设置好所有内容,实现新的泛型类型就会非常快及其运作。

解释这需要在该博客中再 post(我总有一天会的),但我会给你一个简短的例子:

// 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

当然,现在您会遇到生成通用数字的问题,这是另一个密切相关的话题。 SO 上有很多(不同的)答案关于如何生成 NumericLiteralG module. 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#+ 做类似的事情,但使用有理数而不是小数以保持更高的精度。