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

本章中,我们定义用于模算术运算[1](也称为时钟运算)的F#数字类型。模算术用于当我们想保留一个通过循环计算在指定范围内的值。例如,时钟的最大值为12小时。当我们将11小时和3小时相加的时候,数值会溢出,结果就为2小时。且别说时钟,这种数字系统对密码学,音乐等也是必需的。

本文介绍了几种在F#中定义任何新的数字类型时必不可少的技术。更重要的是,你将学会如何:

::  用重载运算符定义一个数字类型

::  为构造我们的新数字类型而定义一个数字文本

::  使在F#链表和矩阵中用我们的类型进行计算成为可能

::  隐藏数字类型的实现细节

我们定义类型IntegerZ5实现模运算的模5,也就是说有效值的范围是从0到4,我们用操作符如加法和乘法操作来装备这个类型。当一个操作产生一个范围之外的值时,我们通过加上或者减去模数(在本例中是5)来调整它。下面是一些计算的例子,我们就可以写:

2 + 1 = 3 (mod 5)

4 * 2 = 3 (mod 5)

List.sum [ 0; 1; 2; 3 ] = 1 (mod 5)

在第一种情况中,我们可以执行操作,无需任何调整。而第二种情况中,我们用4乘以2得到结果8,这已经超出了给定的范围。要修正它,我们计算除以5以后的余数(在F#中写作8 % 5),就是我们所看到的3。最后,最后一个例子表明我们也希望能够在链表中使用我们的类型。如果我们将值0,1,2,3相加,我们得到6,调整后为1。

本文是覆盖一些F# 和F# PowerPack数值计算功能系列的一部分。在本系列的其他文章中讨论到了矩阵,自定义数值类型和便携泛型代码。有关其他部分的链接,请参阅F#与数学 - F# PowerPack概述

 

定义IntegerZ5类型

我们将创建一个包含我们的数字类型的F#库,以使它可以很容易地从多个应用程序使用。我们使用的方法将是:按照通常的F#的最佳实践来设计这种库,所以当创建任何其他的数字类型时你应该遵循类似的步骤。

首先,我们在VisualStudio中创建一个新的F# 库工程。一旦工程被创建了,删除现有的文件,并添加一个新的名为IntegerZ5.fs的文件。此文件将包含我们的类型的实现。作为开始我们添加类型IntegerZ5到命名空间FSharp.Numeric中:

1: namespace FSharp.Numerics

2:

3: type IntegerZ5=

4:    | Z5 of int

5:    member z.ToInt32()=

6:        let (Z5 n)=zin n

7:    override z.ToString()=

8:       sprintf"%d (mod 5)"(z.ToInt32())

9:

10:   staticmember Create(n)=

11:       let z5= n%5

12:       Z5(max ((z5+5)%5) z5)

这个类型被作为一个有单例标签的Z5的可区分联合而执行。这个单例有一个int类型的值,但是我们提供的用以创建并用IntegerZ5计算的操作会保证实际值将在规定的范围内(0到4,含)。事实上,我们使用单例的可区分联合仅仅是一个实现细节,这个执行细节允许我们以一种很方便的途径用类型实现操作,但稍后我们将看到如何隐藏执行细节。

前两个成员将一个IntegerZ5类型的值转换为其他类型。为了得到一个整数,我们简单的返回基本值。当将一个值转化为一个字符串时,我们在这个数后面追加“(mod 5)”字符串,这就是一种常见的表示这个数正在进行模数运算的方式。

最后那个静态成员提供构造IntegerZ5类型的值的方法。它需要一个任意整数作为参数,并调整它,以使它在指定的范围内。如果结果是负数,可以通过除以5 后的余数和再加5来计算(例如,-13 % 5得到-3,max -3 2 会得到结果2。到目前为止,我们创建了一个可以创建和打印的简单类型,但是这只是到目前为止我们所能做的。在下一节中,我们会给我们的类型配以用它做计算的基本操作符。我们会看到,那就是我们需要的使用有一些标准的F#库函数的类型。

 

用IntegerZ5创建并计算

接下来,让我们用IntegerZ5类型并添加用这种类型的值进行乘法,加法和减法的操作符。我们还将添加两个成员以提供对频繁使用的常量0和1的轻松访问,而且我们还会提供一个简单的函数以构造这种类型的值。下面列出添加几个静态成员到这个类型中:

1: type IntegerZ5=

2:   (Omitted)

3:   static member (+) (Z5 a, Z5 b)= IntegerZ5.Create(a+ b)

4:   static member (-) (Z5 a, Z5 b)= IntegerZ5.Create(a- b)

5:   static member (*) (Z5 a, Z5 b)= IntegerZ5.Create(a* b)

6:   static member Zero= Z5 0

7:   static member One =Z5 1

这三个重载的操作符被实现为有一个特殊名字的静态成员。他们有两个IntegerZ5类型的参数,我们用模式匹配(使用可区分联合条件标签Z5)以立即求出用于表示数量的实际整数。执行完整数的数字运算后,结果可能在指定的范围之外,因此我们用Create成员来构造结果,这就调整了这个整数。成员ZeroOne简单地用Z5联合条件构造数值,因为0和1总是有效的值(所以无需调用Create成员)。

通过调用一个与这个类型有着相同名字的转换函数,大部分标准的F#数字类型可以从其它的数字类型被创建。例如,float42返回一个浮点数42.0,byte 42.0把一个浮点数转换成一个byte型值42uy

数字类型转换函数以一种方式被写好,这种方式使得他们能对任何支持某种方式的转换的数字类型均工作。这就意味着,我们可以写byte 42,也可以写byte 42.0,但是不能这样:byte (new System.Random())。这种行为通过使用静态成员约束达到了,静态成员约束允许函数指定参数需要是提供一个特定的方法的任意类型。

为了写一个针对IntegerZ5类型的转换函数,我们可以显式的使用静态成员约束,也可以用一些已经存在的函数(例如int)且让F#类型推断自动的推断约束。下面所列的是用第二种方法:

1: [<AutoOpen>]

2: module IntegerZ5TopLevelOperations=

3:     let inline z5 a= IntegerZ5.Create(int a)

函数Z5需要被标记为inline, 它允许我们使用静态成员约束。在函数体内,我们先用int 函数把参数转换成整型,然后从返回的整型构造IntegerZ5。静态成员约束自动被传播,稍后我们会更细节的去看它们。

由于函数不能定义在一个命名空间里面,我们需要把我们的函数放在一个module里。无论何时我们的库用户打开FSharp.Numerics命名空间,我们应使它自动可用,这可以通过添加AutoOpen特性声明以实现。

接下章

 

 

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

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