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#+ 做类似的事情,但使用有理数而不是小数以保持更高的精度。
我找到了一篇不错的文章: 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#+ 做类似的事情,但使用有理数而不是小数以保持更高的精度。