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)