F# 编程/度量单位
F#:度量单位 |
度量单位允许程序员用静态类型化的单位元数据来注释浮点数和整数。当编写处理表示特定度量单位的浮点数和整数的程序时,这会很方便,例如千克、磅、米、牛顿、帕斯卡等。F# 将验证单位是否在程序员想要的位置使用。例如,如果 float<m/s>
用于需要 float<kg>
的地方,F# 编译器将抛出错误。
度量单位对于从事科学研究的程序员来说非常宝贵,它们增加了一层额外的保护,以防止与转换相关的错误。为了引用一个著名的案例研究,美国宇航局价值 1.25 亿美元的 火星气候探测器 项目最终以失败告终,因为探测器比最初计划的距离火星更近了 90 公里,导致它在火星大气层中壮观地撕裂并解体。事后分析将问题的主要原因缩小到探测器推进系统中使用的转换错误,该系统用于将航天器降入轨道:美国宇航局以公制单位将数据传递给系统,但软件预期数据以英制单位。尽管有很多导致任务失败的项目管理错误,但这个软件错误尤其可以避免,如果软件工程师使用了一个足够强大的类型系统来检测与单位相关的错误。
在 Joel Spolsky 的文章 使代码看起来错误 中,他描述了一种场景,在 Microsoft Word 和 Excel 的设计过程中,微软的程序员需要使用两个不可互换的坐标系来跟踪页面上对象的 位置
- 在所见即所得的文字处理中,您有可滚动的窗口,因此每个坐标都必须解释为相对于窗口或相对于页面,这有很大的区别,并且保持它们一致非常重要。[...]
- 如果您将其中一个分配给另一个,编译器不会帮助您,而 Intellisense 不会告诉您任何信息。但它们在语义上是不同的;它们需要以不同的方式解释和处理,并且如果您将其中一个分配给另一个,则需要调用某种转换函数,否则您将得到一个运行时错误。如果您幸运的话。[...]
- 在 Excel 的源代码中,您会看到很多
rw
和col
,当您看到它们时,您就知道它们指的是行和列。是的,它们都是整数,但将它们分配给彼此永远没有意义。在 Word 中,我听说过,您会看到很多xl
和xw
,其中xl
表示“相对于布局的水平坐标”,而xw
表示“相对于窗口的水平坐标”。都是整数。不可互换。在这两个应用程序中,您都会看到很多cb
,意思是“字节数”。是的,它又是整数,但仅仅通过查看变量名,您就了解了更多关于它的信息。它是字节数:缓冲区大小。如果您看到xl = cb
,那么吹响错误代码警报,这显然是错误的代码,因为即使xl
和cb
都是整数,将像素中的水平偏移设置为字节数也完全疯狂。
简而言之,微软依赖于编码约定来对变量的上下文数据进行编码,并且他们依赖于代码审查来根据上下文强制执行对变量的正确使用。这在实践中有效,但仍然有可能在没有检测到错误数月的情况下,将错误代码引入产品。
如果微软使用带有度量单位的语言,他们本可以定义自己的 rw
、col
、xw
、xl
和 cb
度量单位,这样形式为 int<xl> = int<cb>
的赋值不仅在视觉检查中失败,而且根本无法编译。
使用 Measure
属性定义新的度量单位
[<Measure>]
type m (* meter *)
[<Measure>]
type s (* second *)
此外,我们可以定义从现有度量单位派生的类型度量单位
[<Measure>] type m (* meter *)
[<Measure>] type s (* second *)
[<Measure>] type kg (* kilogram *)
[<Measure>] type N = (kg * m)/(s^2) (* Newtons *)
[<Measure>] type Pa = N/(m^2) (* Pascals *)
- 重要提示:度量单位看起来像数据类型,但它们不是。.NET 的类型系统不支持度量单位所具有的行为,例如能够对数据类型进行平方、除法或幂运算。此功能由 F# 静态类型检查器在编译时提供,但单位从编译后的代码中删除。因此,无法在运行时确定值的单位。
我们可以使用与泛型相同的表示法创建表示这些单位的浮点型和整型数据实例
> let distance = 100.0<m>
let time = 5.0<s>
let speed = distance / time;;
val distance : float<m> = 100.0
val time : float<s> = 5.0
val speed : float<m/s> = 20.0
请注意,F# 会自动为 speed
值推导出一个新的单位 m/s
。度量单位将根据使用方式进行乘法、除法和抵消。使用这些属性,在两个单位之间转换非常容易
[<Measure>] type C
[<Measure>] type F
let to_fahrenheit (x : float<C>) = x * (9.0<F>/5.0<C>) + 32.0<F>
let to_celsius (x : float<F>) = (x - 32.0<F>) * (5.0<C>/9.0<F>)
度量单位在编译时静态检查是否正确使用。例如,如果我们在不期望的地方使用度量单位,我们会收到编译错误
> [<Measure>] type m
[<Measure>] type s
let speed (x : float<m>) (y : float<s>) = x / y;;
[<Measure>]
type m
[<Measure>]
type s
val speed : float<m> -> float<s> -> float<m/s>
> speed 20.0<m> 4.0<s>;; (* should get a speed *)
val it : float<m/s> = 5.0
> speed 20.0<m> 4.0<m>;; (* boom! *)
speed 20.0<m> 4.0<m>;;
--------------^^^^^^
stdin(39,15): error FS0001: Type mismatch. Expecting a
float<s>
but given a
float<m>.
The unit of measure 's' does not match the unit of measure 'm'
单位也可以为整型类型定义
> [<Measure>] type col
[<Measure>] type row
let colOffset (a : int<col>) (b : int<col>) = a - b
let rowOffset (a : int<row>) (b : int<row>) = a - b;;
[<Measure>]
type col
[<Measure>]
type row
val colOffset : int<col> -> int<col> -> int<col>
val rowOffset : int<row> -> int<row> -> int<row>
没有单位的值是无量纲的。无量纲值通过隐式地写出它们而不带单位(即 7.0
、-14
、200.5
)来表示,或者可以使用 <1>
类型显式表示它们(即 7.0<1>
、-14<1>
、200.5<1>
)。
我们可以通过乘以 1<targetMeasure>
将无量纲单位转换为特定度量单位。我们可以通过将度量单位传递给内置的 float
或 int
方法将度量单位转换回无量纲单位
[<Measure>] type m
(* val to_meters : (x : float<'u>) -> float<'u m> *)
let to_meters x = x * 1<m>
(* val of_meters : (x : float<m>) -> float *)
let of_meters (x : float<m>) = float x
或者,通常更容易(更安全)地除掉不需要的单位
let of_meters (x : float<m>) = x / 1.0<m>
由于度量单位和无量纲值是(或看起来是)泛型类型,我们可以编写透明地对两者进行操作的函数
> [<Measure>] type m
[<Measure>] type kg
let vanillaFloats = [10.0; 15.5; 17.0]
let lengths = [ for a in [2.0; 7.0; 14.0; 5.0] -> a * 1.0<m> ]
let masses = [ for a in [155.54; 179.01; 135.90] -> a * 1.0<kg> ]
let densities = [ for a in [0.54; 1.0; 1.1; 0.25; 0.7] -> a * 1.0<kg/m^3> ]
let average (l : float<'u> list) =
let sum, count = l |> List.fold (fun (sum, count) x -> sum + x, count + 1.0<_>) (0.0<_>, 0.0<_>)
sum / count;;
[<Measure>]
type m
[<Measure>]
type kg
val vanillaFloats : float list = [10.0; 15.5; 17.0]
val lengths : float<m> list = [2.0; 7.0; 14.0; 5.0]
val masses : float<kg> list = [155.54; 179.01; 135.9]
val densities : float<kg/m ^ 3> list = [0.54; 1.0; 1.1; 0.25; 0.7]
val average : float<'u> list -> float<'u>
> average vanillaFloats, average lengths, average masses, average densities;;
val it : float * float<m> * float<kg> * float<kg/m ^ 3> =
(14.16666667, 7.0, 156.8166667, 0.718)
由于单位从编译后的代码中删除,因此它们不被视为真正的数据类型,因此不能直接用作泛型函数和类中的类型参数。例如,以下代码将无法编译
> type triple<'a> = { a : float<'a>; b : float<'a>; c : float<'a>};;
type triple<'a> = { a : float<'a>; b : float<'a>; c : float<'a>};;
------------------------------^^
stdin(40,31): error FS0191: Expected unit-of-measure parameter, not type parameter.
Explicit unit-of-measure parameters must be marked with the [<Measure>] attribute
F# 不会推断 'a
是上面的度量单位,可能是因为以下代码看起来正确,但它可以在无意义的方式中使用
type quad<'a> = { a : float<'a>; b : float<'a>; c : float<'a>; d : 'a}
类型 'a
可以是度量单位或数据类型,但不能同时是两者。F# 的类型检查器假设 'a
是一个类型参数,除非另有说明。我们可以使用 [<Measure>]
属性将 'a
更改为度量单位
> type triple<[<Measure>] 'a> = { a : float<'a>; b : float<'a>; c : float<'a>};;
type triple<[<Measure>] 'a> =
{a: float<'a>;
b: float<'a>;
c: float<'a>;}
> { a = 7.0<kg>; b = -10.5<_>; c = 0.5<_> };;
val it : triple<kg> = {a = 7.0;
b = -10.5;
c = 0.5;}
F# PowerPack (FSharp.PowerPack.dll) 包含许多用于科学应用的预定义度量单位。这些单位在以下模块中提供
- Microsoft.FSharp.Math.SI - 国际单位制 (SI) 中各种预定义度量单位。
- Microsoft.FSharp.Math.PhysicalConstants - 带有度量单位的基本物理常数。
- Andrew Kennedy 关于度量单位的 4 部分教程
- F# 度量单位 (MSDN)