13.4.2 用計量單位格式化數據

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。把數據轉換成映射,可以方便地查看不同年份和地區的各項指標。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章