13.4.2 用计量单位格式化数据
XML 数据中的许多指标,都只能转换为浮点数(float);这当然是对的,因为面积和森林覆盖率都是数字,但是,这种数据没有更多的意义。把从非类型化的 XML 数据转换成 F# 类型化数据结构,其目的是通过类型注解,更好地了解这些值含义。为了使类型更具体,我们可以使用计量单位(units of measure),这在第二章提到过。使用这个功能,面积按平方公里计,森林覆盖面积按占总地面积的百分比计。我们首先通过几个示例来了解一下计量单位。
13.4.2.1 使用计量单位
在 F# 中,使用计量单位很容易,因此,我们在这里一带而过。声明计量单位使用 type 关键字,加上专有的特性([])。严格说来,计量单位并不是类型,但我们可以把它当作为另一种类型使用。我们首先定义两个简单的计量单位,表示公里和小时:
[<Measure>] type km
[<Measure>] type h
可以发现,Measure 特性说明类型是计量单位。这是一个专有特性,F# 编译器能够理解。我们不一定要自己定义计量单位,因为 FSharp.PowerPack.dll 库中有标准集,但是现在,我们还是自己声明。有了单位 km 和 h 以后,就可以创建表示公里或小时的值了。清单 13.15 显示了创建带单位的值,和使用这些值参与计算的函数。
清单13.15 使用计量单位的计算 (F#)
> let length = 9.0<km>;; [1]
val length : float<km> = 9.0
> length * length;; [2]
val it : float<km^2> = 81.0
> let distanceInTwoHours(speed:float<km/h>) = [3]
speed * 2.0<h>;;
val distanceInTwoHours : float<km/h> -> float<km> [4]
> distanceInTwoHours(30.0<km/h>);;
val it : float<km> = 60.0
描述数值常量的单位,把单位括在尖括号角中,加在值的后面[1]。我们首先定义了一个表示在长度的值,以公里计。如果计算有单位的值,F# 能自动推断出结果的单位,因此,距离乘了两次,得出的面积以平方公里计[2]。表示单位,可用常规符号,即,用 ^ 表示乘方,用 / 表示除法,并列单位用乘法。
下一个示例中函数的参数包括单位信息,函数的参数为速度,返回两小时行走的路程[3],参数以公里每小时计,因此,需要添加有单位的类型注解,把单位放在尖括号中,与指定参数值类型类似,比如 list 类型。F# 编译器能够推断出返回类型[4],就像处理普通类型一样。通常,它能够帮助我们在阅读代码时,了解函数的功能;另外,这也是对函数重要的检查,可以避免犯低级错误:如果我们试图计算距离,但最后返回类型的单位是时间,那就肯定错了。
在世界银行的数据中,我们将使用 km^2 表示国家总面积的单位。到目前为止,一切顺利,但是,我们想要的第二个指标是以百分比的形式提供的,那么,应该如何指定百分比的单位呢?虽然,计量单位主要是用来表示物理量的,但也可以用它们来表示百分比:
[<Measure>] type percent
let coef = 33.0<percent>
这段代码创建表示百分比的单位,然后,定义了常量 coef,值为 33%。严格来讲,以百分比计的值没有单位,因为它仅是一个比例,但是,把它定义成单位很有用。为了演示,我们计算 50 公里距离的 33%。由于 coef 表示比例,可以简单地把两个值乘起来:
> 50.0<km> * coef;;
val it : float<km percent> = 1650.0
很明显这是错误的。我们期望的结果是以公里计,但是,从推导出的类型可以发现,结果是以公里乘以新的单位 percent。另外,从交互运行的代码还可以看到,数值太大,而计量单位的重要性就在于,可以在类型检查时就发现错误,而不必等到程序实际运行。那么,哪里出问题了呢?总是出在百分比值表示的比例已经乘以 100,要得出正确的结果,需要把值再除以 100 percent:
> 50.0<km> * coef / 100.0<percent>;;
val it : float<km> = 16.5
可以发现,现在好多了。我们把结果除以 100 percent,这样,在结果中就没有 percent单位了。F# 自动化简单位,知道 km percent/percent 等于 km。这个示例演示了使用计量单位的重要原因:与使用类型一样,能够有帮于尽早捕获大量的错误。
注意
计量单位还有很多其他重要的功能,在这个简介中我们并没有涉及到。例如,可以定义派生单位,比如 N(表示力,以牛顿计),这实际上就是 kg m/s^2;在函数或类型中,泛型参数也可以使用单位。有关计量单位的详细信息,请参阅 F# 联机文档,和架构师 Andrew Kennedy 关于该功能的博客(http://blogs.msdn.com/andrewkennedy)。
现在,还是回到我们的主示例,把下载的数据转换成带单位的类型。我们将使用原子单位 percent 表示地区森林覆盖率,用单位 km^2 表示面积。
13.4.2.2 格式化世界银行的数据
我们声明的 readValues 函数,从 XML 文档中读取值,最后一个参数是解析函数,用于将每个数据点转换到适当类型的值。我们下载的数组包含了三组面积,以平方公里计,和三组森林覆盖率,以百分比计。清单 13.16 演示了把原始文档转换成数据结构,从中可以方便提取重要信息。
清单 13.16 把原始数据转换成类型化的数据结构 (F#)
let areas =
Seq.concat(data.[0..2]) [1]
|> readValues (fun a -> float(a) * 1.0<km^2>) [2]
|> Map.ofSeq [3]
let forests =
Seq.concat(data.[3..5])
|> readValues (fun a -> float(a) * 1.0<percent>)
|> Map.ofSeq
在进行管道处理之前,先把表示第一个指标的所有页面中的数据连接起来[1],再把每个值从字符串转换成数值,以平方公里计[2],然后,用数据生成映射(Map)[3]。第二个命令处理森林覆盖率,与此类似。
数据处理的主要部分使用管道运算,这种新的功能我们还没介绍过,它从数据集中取前三个元素,这称为切片(slicing),语法为 data.[0..2]
, 生成索引从 0 到 2 的数组项的序列[1]。用 Seq.concat
把返回的序列连接成一个序列,包含所有年份的数据。管道运算的下一步是读这些值,转换成带对应计量单位的类型[2],这却是最简单的部分,就是简单的 lambda 表达式!要注意的是,世界银行使用点作为分隔符,所以,数字就如 1.0。内置的 float 函数始终使用固定的区域设置,因此,在任何系统上,它都能正确解析字符串。
我们使用 Map.ofSeq 函数,把数据生成 F# 映射类型[3]。这个函数参数为元组的序列,第一个元素是关键字,第二个元素是值。在清单 13.16 中,关键字的类型为int * string
,即年和地区名。第一个函数的类型为 float<km^2>
,第二个函数为 float。把数据转换成映射,可以方便地查看不同年份和地区的各项指标。