F#中泛型类型的通用人工操作

时间:2015-03-01 14:21:41

标签: generics f# type-inference

我发现了一篇很好的文章: 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

错误消息是:

当(^ a或^?18259):(静态成员(/):^ a * ^?18259 - >;??18260)&#39; p>

此外 - doSomething的感染类型是: 的Vector2D&LT;&#39 a取代; - &GT;的Vector2D&LT;&#39 c取代; (需要成员(/)和成员(/)以及成员get_One)

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

1 个答案:

答案 0 :(得分:3)

它说要求/两次,因为类型不必相同,所以你的函数会比你期望的更通用。

在设计通用数学库时,必须做出一些决定。你不能只是在没有类型注释所有内容或限制数学运算符的情况下继续进行,类型推断将无法提出类型。

问题是F#算术运算符不受限制,它们有一个'开放'签名:'a->'b->'c所以要么限制它们,要么你必须完全键入注释所有函数,因为如果你没有F#将无法知道应该采取哪种超载。

如果你问我我将采用第一种方法(也就是Haskell方法),它在开始时有点复杂,但是一旦你完成所有设置,实现新的泛型类型及其操作的速度都很快。 p>

解释这将需要该博客中的另一篇文章(我将在某一天),但我会给你一个简短的例子:

// 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模块,这里有很多(不同的)答案。 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#+执行类似的操作但使用有理数而不是十进制来保持更高的精确度。