F#与数学(III) – 自定义数字类型(PartII)

交互地测试数字类型

下一步,我们将用F#交互来验证我们所写的代码到目前为止是按预期工作的。在这一点上,我们甚至没有生成库。我们仅仅写了一些F#的源代码,想不生成库来测试这些源代码。我们将使用在F#脚本文件中写入的交互式测试。一旦你建立了库,就可以很容易地用NUnit,XUnit或者其他测试框架把它们转变成相应的单元测试。

为了测试我们在源代码级别实现的类型,我们先添加一个新的名为Test.fsx的F#脚本文件(添加->新建项目->F#脚本文件)。在这个文件中,我们加载IntegerZ5.fs的内容并注册一个打印机以指定F#交互工具应该如何输出我们的类型的值:

1: #load "IntegerZ5.fs"

2: open FSharp.Numerics

3:

4: fsi.AddPrinter(fun (z:IntegerZ5)-> z.ToString())

第一条命令加载F#源代码,并在F#互动工具里求值。接下来,我们打开包含有我们的类型的FSharp.Numerics命名空间。最后,我们调用全局fsi值的AddPrinter成员。参数是一个Lambda函数,它有一个IntegerZ5类型的值,且返回一个通过简单调用一个我们提供的ToString成员而得到的字符串。请注意我们需要添加类型注释,因为AddPrinter是一个泛型成员。类型注释允许它为我们将指定的打印机决定一个类型。

现在,为了应用它们,我们可以创建IntegerZ5类型的值和操作符来测试我们的函数:

1: let a= z5 3.0

2: let b= z5 7

3: val a : IntegerZ5 ="3 (mod 5)"

4: val b : IntegerZ5 ="2 (mod 5)"

5:

6: a* b;

7: val it : IntegerZ5 ="1 (mod 5)"

8: a+ b

9: val it : IntegerZ5 ="0 (mod 5)"

正如我们所能看到的,当F#交互输出我们的类型的一个值时,它使用了我们指定的打印机。例如输出“3 (mod 5)”使我们很清晰地知道这个值做了模5运算。前两个let显示:函数Z5对任何数字类型都工作(包括浮点数和整数),它还自动计算模数。最后两个例子显示:我们提供的重载操作符也遵从模运算的规则。

到目前为止,我们已经直接地使用了操作符。我们写IntegerZ5类型的方式也允许我们通过F#核心库里面的一些函数来使用它。例如,如果我们创建一个IntegerZ5类型的链表,我们就可以像这样用我们提供的重载操作符为这个链表求和:

1: List.sum [ z5 0; z5 1; z5 2;z5 3; z5 4 ]

2: val it : IntegerZ5 ="0 (mod 5)"

List.sum函数怎么知道它需要使用IntegerZ5类型的重载操作符?这个函数用静态成员约束来执行(实现),且它需要元素的类型提供一个“+”操作符和一个Zero成员。编译器知道我们的类型提供了必需的成员,因此它允许我们的类型使用List.sum。我们将在下一系列的的文章中做眼于这样的函数实现。

在这一步中当创建IntegerZ5类型的值时,我们使用了Z5类型转换函数。我们还可以用IntegerZ5.Create成员,但是那会导致一个更重量级的注释。在下一步中,我们将看到怎样用数值字段以使语法更方便。

 

为IntegerZ5提供数字文本

内置数字类型如int64, bytefloat32的值可以用特殊的数字文本来创建,如:-30L127UL42.0f。F#使得为我们自己的数字类型提供一个类似的文本成为一种可能。当然我们可以定义什么种类的文本也是有许多限制的。简单地说,文本必须以F#语言保留做这种用途的字符中的一个作为结束。另外,文本必须仅仅由数值符号组成(因此如:12#3Z是不允许的)。

为了定义一个文本,我们需要用特殊的名字写一个module。在这个module中,我们实现了几个函数,无论何时一个文本被用到,它们在F#编译器中自动被调用:

1: module NumericLiteralZ=

2:   let FromZero ()= Z50

3:   let FromOne ()=Z51

4:   let FromInt32 a= IntegerZ5.Create(a%5)

5:   let FromInt64 a= IntegerZ5.Create(int(a%5L))

这个module的名字由一个特殊的名字NumericLiteral后跟一个我们将用来写我们自己的文本的符号Z组成。这就意味着我们将可以写这些文本比如:0Z,1Z,42Z。这个module可以提供一些函数以允许很多种长度的文本。

如果我们仅包括了前两个函数(FromZeroFromOne),我们仅仅可以写0Z1Z。后面的那两个函数使我们可以为调整成整数值的其它数字写文本。为了支持比int64大的数字文本,我们也可以提供FromString方法。请注意,这可能不允许写由非数值的字符组成的文本。这个方法的主要用途就是允许任意长度的整型数,因此我们在上面的例子中没有提供它。

以下所列显示了一些使用我们新定义的数字文本的例子:

1: 6Z

2: val it : IntegerZ5 ="1 (mod 5)"

3: 3Z* (4Z+2Z)

4: val it : IntegerZ5 ="3 (mod 5)"

正如我们所看到的,数值字段使处理自定义的数值数据类型更加简单。由于我们的数字类型基本上完成了,我们将在下一步里着眼于把它编译成一个可再发行的库。

添加F#签名文件

签名文件可以被添加进一个工程里以指定实现文件(如IntegerZ5.fs)里的哪个成员和应该公开的被编译库导出。一个签名文件列出了成员和函数以及他们的类型签名,这也使它成为一个关于我们的库的有用的信息来源。

文件名应该和实现文件有相同的名字,但是应该有fsi扩展名。在我们的例子中,我们将添加一个名为IntegerZ5.fsi的文件。这个文件需要在执行文件之前传给编译器。在Visual Studio中,这意味着在解决方案浏览器中你需要移动这个文件到实现文件之前。这可以通过右键点击这个文件选择“上移”命令来实现。

下面所列的是这个签名文件的源代码。你所能看到的这个代码清单中的大部分类型签名都能被F# 编译器自动推断出来。从源代码中提取第一个版本F#签名文件的一个简单方法就是全选这个执行文件的内容并用F# 交互来求值(?)。F#交互打印所有的推断出来的类型签名,这样我们就能轻松的把它们拷贝到签名文件中了。

不过,我们将需要显式的添加到产生的类型签名中。这个代码清单还显示了Z5函数的类型签名,这个非常有趣:

1: namespace FSharp.Numerics

2:

3: [<Sealed>]

4: type IntegerZ5=

5:   member ToInt32:unit-> int

6:   override ToString:unit-> string

7:   static member Create: int-> IntegerZ5

8:   static member One: IntegerZ5

9:   static member Zero: IntegerZ5

10: static member (+ ): IntegerZ5* IntegerZ5->IntegerZ5

11: static member (* ): IntegerZ5* IntegerZ5->IntegerZ5

12: static member (- ): IntegerZ5* IntegerZ5->IntegerZ5

13:

14: module NumericLiteralZ=

15:   val FromZero : unit->IntegerZ5

16:   val FromOne  : unit->IntegerZ5

17:   val FromInt32: int-> IntegerZ5

18:   val FromInt64: int64-> IntegerZ5

19:

20: module IntegerZ5TopLevelOperations=

21:   val inline z5:^a-> IntegerZ5when

22:         ^a: (static member op_Explicit:^a->int)

这个签名文件从一个命名空间声明开始,这和实现文件中的声明是一样的。接下来,它包含了一个类型签名和两个module签名。注意,IntegerZ5类型被标记了Sealed特性。这是必须地,因为签名文件需要透露我们在定义什么种类的文件。对于诸如记录和可区分联合的F#类型,我们使用Sealed意味着那个类型是不可扩展的。

我们的数值类型成员还有NumericLiteralZ module中的函数的类型签名都是是简单明了的。最后那个module中的函数Z5非常有趣。如前所述,这个函数是为能对任何可以被转换成整数的参数都工作而实现的。实际上,这就意味着参数的类型需要提供一个名为op_Explicit(带有类型参数,返回整型)的静态成员。

为确定是否一个参数类型有某种在.NET泛型中不能表达的方法,我们使用名为静态成员约束的特定功能。这仅适用于所谓的帽式类型(如^a)。帽式类型在编译期间被内联,因此这样的函数也需要被标记为inline

最后,我们也值得注意的是,如果我们添加了op_Explicit成员到我们的IntegerZ5类型中,那我们将可以把我们的类型用作int函数的参数(例如我们可以这样写:int 3Z

 

接下章

 

原文链接:http://tomasp.net/blog/fsharp-custom-numeric.aspx

posted @ 2012-04-27 16:29  tryfsharp  阅读(343)  评论(0编辑  收藏  举报