F#与数学(Iv) - 泛型数字代码
泛型数字代码是能够被用来处理多种不同数字类型包括像int 型, decimal型 和float型或者甚至是我们自己的数字类型的一些计算(比如此系列的前篇文章中的时钟运算)。泛型数字代码区别于普通的F#泛型代码像'a 链表类型 或者List.map函数, 因为数字代码使用数字操作符, 比如为每个数字类型而区别定义的+或者 >=。
当编写有一些参数类型 'T的简单泛型代码的时候, 对于参数的类型我们什么也不知道并且也没有办法将其确定为某一数字类型,来提供所有我们需要在我们的代码里面使用的操作符。这就是.NET运行时的局限性,F#提供2种方式来克服这个问题。
:: 静态成员约束 能被用来在编译时间具体数字操作符被决定的地方写泛型代码(一个泛型函数专门为所有的必须数字类型)。这个方法使最终的代码非常高效并且当写一个函数比如List.sum的时候一般也很容易使用。
:: 全局数字联合(在F# PowerPack中可用)给我们了一个在运行时动态地获得实现必需数字操作符的接口的方式。这种方法有一些运行时开销,但是能用于复杂的数字类型(比如Matrix<'T>)
:: 两种结合的技术能被用来去实现包含数字值泛型并且仅有最小的运行时开销的复杂类型。
静态成员约束是F#唯一有的特性 , 这在其他.NET语言中是不可用的, 所以如果你有兴趣为.NET写数字代码,这可能是一个好的选择F#的原因。在 C#或者VB中,第二项你是受限制的(它能用C#实现)。在动态语言中(像IronPython),所有的都是动态的,所以数字计算能够处理任何数字类型,但是明显的低效。在接下来的文章中,我们看看上面总结的三项。
本文是覆盖一些F# 和F# PowerPack数值计算功能系列的一部分。 此系列其他的文章讨论矩阵,自定义数字类型和写泛型代码. 对于其他部分的链接, 请看F#Math - Overview of F# PowerPack
使用静态成员约束
在这段中,我们将写一个带有静态解析的类型参数的函数。这种类型的参数不同于在编译时间决定的(用具体的参数类型代替)普通泛型类型参数。
当使用静态解析的类型参数的时候,我们使用成员约束来指定具体的参数类型必须用某些类型提供方法。然后这些方法能从我们的泛型方法内部被调用。仅当泛型函数标记为inline的时候,静态解析的类型参数才能被使用。这意味着对这样的函数的调用将在编译期间被函数体替换,所以这种技术仅仅对相对简单的函数有用。
在官方MSDN F#文档里面, 你能找多更多关于静态解析的类型参数[1]和inline函数 [2]的信息,虽然我也将在这篇文章解释所用重要东西。
使用内嵌数字操作符
虽然可以手动地写一个静态成员约束,但是我们可以只通过依赖于F#类型推断来轻松地开始。当写一个inline函数,而它又调用另外一个带有成员约束的inline函数的的时候,约束是自动传播的。这意味着我们所有需要做的就是标记我们的函数为inline并且仅使用通过使用静态成员约束被他们自己写的操作符。这包括LanguagePrimitives模块里的函数也有标准的F#操作符比如*和+:
1: let inline halfSquare num=
2: let res= LanguagePrimitives.DivideByInt num 2
3: res*res
4: val inline halfSquare : ^a -> ^b
5: when ^a: (static member DivideByInt : ^a *int -> ^a)
6: and ^a: (static member ( * ) : ^a * ^a ->^b)
这个函数带有一个数字参数,除以2然后计算这个结果的平方。这里的除法不能简单的写成
Num/2, 因为这将使用标准整数除法而且编译器也将推断num为int型。相反,我们使用DivideByInt函数。这个函数被定义在核心库里面以供所有的标准数字类型。对于非标准类型,它使用一个静态成员约束并且需要这个类型有一个DivideByInt成员。
在打印的函数签名里,我们能够看见推断的静态成员约束。类型参数的名字以脱字符号(^)作为开头,以指示它是个静态解析的类型参数。When字句指定了成员约束。如我们看到的,类型需要提供DivideByInt方法(它取整数为第二个参数)和一个乘法操作符。
下面的两个例子出示了函数能被用于多个不同的数字类型:
1: halfSquare20.0f
2: val it : float32 =100.0f
3: halfSquare11M
4: val it : decimal =30.25M
在第一个例子里,我们使用了float32类型(它对应于C#里面的System.Single 或float),而第二个例子使用了decimal。值得注意的是DivideByInt成员不提供整数,因为它表现了精确的浮点除法。另一种写对整数也起作用的上述函数的方式就是用标准的操作符除法和加法(/和+)和来自于LanguagePrimitives模块的GenericOne值。
写字定义约束
在先前的例子中,基于我们在inline函数里使用的操作,我们依靠F#类型推断,让编译器自动地指出了静态成员约束。这工作的非常好,因为LanguagePrimitives模块的操作符像/或数字函数生成了约束。
然而,LanguagePrimitives模块仅包含了有限的函数集,如果你想能够用指定的成员写一个适用于任何类型的函数,你需要显示地写一个静态成员约束。例如,float32 (System.Single) andfloat (System.Double)俩个都有几个成员处理IEEE浮点算术。
下面的函数当浮点数无穷大的时候使用一个静态方法IsInfinity来返回一个选项值None否则返回Some:
1: let inline check<^Twhen^T:
2: (static member IsInfinity:^T->bool)> (num:^T):option<^T>=
3: if(^T: (static member IsInfinity:^T->bool) (num)) then None
4: else Some num
声明很冗长,但是显示写约束通常一般不需要 - 一旦你写一个做基本操作的函数,类型推断是很容易地使用。约束被写为泛型参数声明的一部分。语法< ^T, when ...>指定了函数有一个(静态决定的)参数^T,它必须有该指定类型专门的静态成员(IsInfinity)。
为了在函数体的内部调用该方法,我们需要重复成员约束的签名并且我们把num给它 作为参数。现在我们能使用不同的参数来调用函数。
1: check 42.0
2: val it : floatoption = Some(42)
3: check (1.0f/0.0f)
4: val it : float32option = null
5: check (1/2)
6: error FS0001: Thetype 'int' does not support
7: any operators named 'IsInfinity'
前面两个调用是正确的,因为float和float32两个都提供了必需的(静态)成员。在第三个例子里,我们得到了一个编译错误,因为F#编译器检测到类型int没有实现必需的IsInfinity方法。值得注意的是check函数不是限于这2种类型的 - 如果你定义一个有必需的静态方法的自定义数字类型,check函数也将工作。然而,增加IsInfinity方法到已经存在的类型是不可能的。即使你实现它为一个扩展方法,F#编译器将忽略它 (至少 ,在2.0版本里)。
如我们在介绍中讨论的,静态成员约束仅用于写一个inline函数的时候。在下一段里,我们将探索在F#中写泛型数字代码的第二种技术。
使用全局数字联合
使用全局数字联合,在运行时我们可以获得一个为特定数字类型提供基本操作实现的接口。这通过使用一个被F#PowerPack库[3]维护的全局联合的表来做。表自动地包含了所有标准F#数字类型的数字联合且当你定义一个新的类型的时候,你能够使用表来注册它(例如,看先前的文章)。最终用数字联合实现的任何数字代码将很好的和新定义的类型工作。
在这段中,我们将实现一个类型Quadruple<'T>。此类型存储了四个(可能)'T类型的不同值和提供了逐点加法和乘法操作。你能够把它想成是很简单的向量类型。那儿有两种方式来实现此类型的操作 :
::一个静态方法定义的类型操作符能被标记为inline,所以他们能使用静态成员约束。
然而,这个方法对实例成员不起作用。
::当实现一个更加复杂的的类型时,我们可以继承为基本数字类型提供操作的接口。
本章中实现的quadruple 类型是很简单的,我们能使用第一个方法。然而,我想示范一下第二个替代方案(它将为复杂的类型所需,像矩阵)。在下一段中,我们就回到quadruple使用静态成员约束。
正如刚才所提到的,全局联合被F# PowerPack 库提供。所以我们首先需要增加对 Fsharp.PowerPack.dll的引用。Quadruple<'T>类型的核心部分是简单明了。构造函数取'T类型的四个参数,接下来,我们把它们公开为成员。更有趣的是,构造函数也取一个为'T类型提供数字操作的INumberic<'T>类型的值:
1: type Quadruple<'T>(a:'T,b:'T, c:'T, d:'T, ops:INumeric<'T>)=
2: member x.Item1=a
3: member x.Item2=b
4: member x.Item3=c
5: member x.Item4=d
6:
7: /// Expose implementation of operations for 'T
8: member x.Operations=ops
9:
10: /// Create quadruple and retrievenumeric operations dynamically
11: staticmember CreateRuntime(a, b, c, d)=
12: letops= GlobalAssociations.GetNumericAssociation<'T>()
13: newQuadruple<_>(a, b, c, d, ops)
14:
15: /// Format quadruple as a string
16: override x.ToString()=
17: sprintf"%A" (a, b, c, d)
我们不期望我们类型的用户自己去获得数字联合,所以通常该类型将通过一个createRuntime来创建。此方法通过使用GetNumericAssociation来动态的获得数字联合。该函数取一个单独的参数类型,它应该是数字类型然后返回一个INumberic<'T>接口的实现。在上面的例子中,我们还没为任何东西使用接口。然而,我们通过一个公有属性来显示它,为了我们能使用它来实现重载操作。
下面的例子出示了我们能增加到我们的Quadruple<'T>类型的*和+操作符代码。它们两个都被实现为逐点操作,这意味着它们用第二个quadruple的第一个元素乘以第一个quadruple的第一个元素等等:
1: static member (+) (q1:Quadruple<_>, q2:Quadruple<_>)=
2: let inline (+? ) a b= q1.Operations.Add(a, b)
3: Quadruple<_>(q1.Item1+?q2.Item1, q1.Item2+? q2.Item2,
4: q1.Item3 +?q2.Item3, q1.Item4 +? q2.Item4,
5: q1.Operations)
6:
7: static member(*) (q1:Quadruple<_>, q2:Quadruple<_>)=
8: let inline (*? ) a b= q1.Operations.Multiply(a, b)
9: Quadruple<_>(q1.Item1*?q2.Item1, q1.Item2*? q2.Item2,
10: q1.Item3*?q2.Item3, q1.Item4*? q2.Item4,
11: q1.Operations)
这些操作符实际上有同样的结构。它们两个都取 quadruples作为参数。下一步,我们定义一个简单的的局部操作符(使语法更加便利),它通过使用第一个 quadruple里存储的数字联合add 或者Multiply方法来增加或者乘以两个数(类型'T)。既然这两个quadruples有同样的类型,那么我们也能使用来自于第二个的数字联合,因为它们是一样的。最后,我们使用局部帮助操作符来实现逐点操作并构建一个新的 quadruple作为结果。注意我们传递了作为参数的数字联合到新构建的 quadruple,为了它不需要又访问联合表。
下面列出的示范了quadruple怎样和整型和浮点型值工作:
1: let quad a b c d=Quadruple<_>.CreateRuntime(a, b, c, d)
2: val quad : 'a -> 'a -> 'a -> 'a-> Quadruple<'a>
3:
4: (quad1234)+ (quad4321)
5: val it : Quadruple<int> = (5, 5, 5, 5)
6:
7: let q1=quad1.02.03.04.0
8: let q2=quad0.40.30.20.1
9: (q1+ q2)* q1
10: val it :Quadruple<float> = (1.4, 4.6, 9.6, 16.4)
第一句定义了一个创建了一个quadruple的帮助函数。该类型不被任何成员约束限制。然而,该实现仅仅对数字类型起作用,这意味着当我们用非数字参数调用 quad的时候,它将抛出一个异常。
值得注意的是仅仅当创建quadruples的时候,GetNumericAssociation函数才被调用。两个操作都提取来参数中某一个的数字联合。所以一旦我们构建了quadruples,余下的计算就能高效。这解释了例如为什么这个技术能被用作 F# Matrix<'T>,在哪里我们需要避免不必要的开销。
合并成员约束和INumberic
使用GetNumericAssociations方法去获得一个被F#PowerPack维护的INumeric<'T>接口的实现有一些运行时开销,它需要数字类型被注册在联合表里面。然而,合并基于静态成员约束的方法和使用INumberic<'T>接口来得到操作的主意是可能的。
在这一段中,我们将用一个静态成员Create来扩展 Quadruple<'T>类型,此静态成员Create构造此类型的值比我们先前实现的CreateRuntime更有效。如果你想通过反射动态地创建quadruples,那么CreateTime方法可能仍然有用。在寻找更好的方式来实现Create方法之前,我们通过一个简单的F#交互命令来测量一下CreateRunTime的性能:
1: #time
2:
3: // Add quadruples in a simple imperativeloop
4: letmutable sum= quad0000
5: fori in0..10000000do
6: sum<- sum+ (quad i (i/2) (i/3) (i/4))
7: Real: 00:00:02.536,CPU: 00:00:02.511, (...)
8: val mutable sum :Quadruple<int> =
9: (-2004260032, -1004630016, -2103075776,1642668640)
这个片段创建了一千万个quadruples并且用一个循环命令增加它们。在F#交互中,当作为一个非优化F#代码执行的时候,操作花2.5秒。
为了增加 Create方法,我们只可以修改现有的类型。这个方法被标记为inline并且它有一个名为^TStatic的静态决定地参数类型(帽式类型)。在内部,我们使用对象表达式来实现
INumeric<^TStatic>接口,然后我们使用它来构造一个 quadruple:
1: /// Initialize a quadruple and capture operations for working with the
2: /// type ^TStatic using static member constraints
3: staticmember inline Create
4: (a:^TStatic, b:^TStatic, c :^TStatic, d:^TStatic)=
5: let ops=
6: // Implement INumeric interface using generic operators
7: {new INumeric<^TStatic>with
8: member x.Add(a, b)=a+ b
9: member x.Abs(a)=abs a
10: member x.Compare(a, b) = compare a b
11: member x.Equals(a, b) = a= b
12: member x.Multiply(a, b) = a* b
13: member x.Negate(a)=-a
14: member x.Subtract(a, b) = a- b
15: member x.Sign(a)= sign a
16: member x.Zero= LanguagePrimitives.GenericZero
17: member x.One= LanguagePrimitives.GenericOne
18: member x.ToString(_, _, _) = failwith"not implemented"
19: member x.Parse(_, _, _) = failwith"not implemented" }
20:
21: // Create a quadruple and pass it anINumeric implementation
22: newQuadruple<_>(a, b, c, d, ops)
当创建一个inline静态方法的时候,inline关键子需要被放在member关键子之后。我们不需要显示地定义静态决定参数类型 - 如果我们仅 在类型签名里面使用它,对我们来说,编译器使这个方法泛型化。
这里我们使用此诀窍的关键理念是当一些人调用 Create的时候,实现 INumeric<'TStatic>的对象表达式也将是内联的。这意味着将专门执行具体的数值类型且被用来实现它(+,*,函数像compare等等)的所有操作将用具体数值类型的优化代码替换。Create 方法(将鼠标悬停在其名称查看工具提示)的类型签名出示了^Static参数类型的所有需求。
Create方法的唯一开销是它创建了一个实现了INumeric<'T>接口的简单对象的一个实例。另一个方面,CreateRunTime方法,基于表现System.Type(它必须基于泛型参数被更新)的一些标记,执行了字典查找。咱们使用一个新的quad函数来测量我们先前例子的性能:
1: letinline quad a b c d= Quadruple<_>.Create(a,b, c, d)
2:
3: // Imperative loop using staticallyresolved 'quad' function
4: letmutable sum= quad0000
5: fori in0..10000000do
6: sum<- sum+ (quad i (i/2) (i/3) (i/4))
7: Real: 00:00:00.758,CPU: 00:00:00.748, (...)
8: val mutable sum :Quadruple<int> =
9: (-2004260032, -1004630016, -2103075776,1642668640)
此段通过声明一个新版本的quad帮助函数开始。这次,它被实现为调用新inline方法Create的inline函数。当我们使用我们新的实现运行循环的时候,运行时间从2.5秒减少到0.7秒,这意味着新版本是三四倍快!此外,内联代码仅仅创建了一个具体类的实例,所以生成的二进制不比原始的版本大。
小结
在.NET里面,没有说泛型参数必须是支持基本数字操作符和函数的数字类型这样的说法。这使得写执行一些计算的数字代码很困难,但是对多个不同的类型还是起作用。理想情况下,我们希望能写一个计算(或者一个泛型)一次,然后如果计算不需要浮点算术,可以用float32(System.Single)和float(System.Double),也可以用decimal和int使用它。试图在C#[5,6,7]里写泛型数字代码不是有运行时开销就是让简单的代码看起来很复杂。
在这篇文章里,我们已经看见了如何在F#中解决这个问题。幸亏static member constraint和inline关键字,写使用一般数字类型的简单数字函数就很容易了。F#库自己就使用这个技术来实现函数像List.sum(对一个链表的元素求和)。这样 的函数不但处理所有的标准类型,
也适合自定义的数字类型(如我们在先前的章节里面讨论的)。
对于更多的复杂泛型,我们需要在一个对象里面存储数字操作。在F#里面,这能够通过使用F# PowerPack里面可用的 INumeric<'T>来容易地完成.如果我们很少需要获得接口的实现,我们能使用提供运行时查找的GlobalAssociations模块。然而,我们也看见了通过合并INumeric<'T>接口和静态成员约束使这个更加高效是可能的。据我所知,从简洁和效率来讲,这可能是对.NET程序员来说最好的解决方案了。
引用与链接
:: [1]Statically Resolved Type Parameters(F#) - MSDN文档
:: [2]Inline Functions(F#) - MSDN文档
:: [3]F# PowerPack(原代码与二进制包)- CodePlex
:: [4]数字计算 – MSDN上的现实中的函数式编程
:: [5]使用泛型计算 - Rüdiger Klaehn at CodeProject
:: [6]泛型操作 - Marc Gravell, Jon Skeet
::[7]在 C# 4.0 中的动态模拟 INumeric - Luca Bolognese