首页 文章

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

提问于
浏览
3

我发现了一篇很好的文章: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或^?18259):(静态成员(/):^ a * ^?18259 - > ^?18260)'

此外 - doSomething的感染类型是:Vector2D <'a> - > Vector2D <'c>(需要成员(/)和成员(/)以及成员get_One)

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

1 回答

  • 3

    它说两次需要 / 因为类型不一定相同,所以你的函数会比你期望的更通用 .

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

    问题是F#算术运算符不受限制,它们有一个'open'签名: '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模块,这里有很多(不同的)答案 . F#+是一个提供此类模块实现的库,因此您可以编写 vector * 10G

    您可能想知道为什么F#运算符不像Haskell等其他语言那样受限制 . 这是因为一些现有的.NET类型已经以这种方式定义,一个简单的例子是DateTime,它支持添加一个任意决定代表一天的整数,请参阅更多讨论here .

    UPDATE

    要回答有关如何乘以浮点数的后续问题,这又是一个完整主题:如何使用许多可能的解决方案创建通用数字,具体取决于您希望的库的通用性和可扩展性 . 无论如何,这是获得一些功能的简单方法:

    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#做了类似的事情但是使用有理数而不是十进制来保持更高的精度 .

相关问题