Elm 的型別支援泛型,這通常能為介面增添彈性。舉例來說,Maybe a 型別可以存放任何型別的值。
type Maybe a = Nothing | Just a
注意在上面這個例子中,型別參數 a 出現在等號 = 的兩側。我們會說 a 綁定到型別定義內部的某個資料。
不過在某些情況下,型別參數只會出現在等式的左側。
type Distance unit = Distance Float
在上面 Distance 的定義中,unit 是一個自由型別參數,並沒有綁定到型別裡的任何資料。我們也把這種型別稱為幻影型別。
當我們想在編譯期強制執行某些限制時,它出乎意料地好用。舉例來說,我們想確保只把相同單位的距離相加。
-- Distance is an opaque type, since the module does not expose its variants.
-- This means that users may only use the functions meter, foot and add to manipulate distances.
module Distance exposing (Distance, Meter, Foot, meter, foot, add)
-- The Distance type has a phantom type 'unit'.
type Distance unit = Distance Float
-- We define two types that will be used in place
-- of the phantom type 'unit' in our constructor functions.
type Meter = Meter
type Foot = Foot
-- Constructor for Meter
meter : Distance Meter
meter = Distance 1.0
-- Constructor for Foot
foot : Distance Foot
foot = Distance 0.3048
-- The add function cannot take two parameters of different types.
-- So we cannot add meters and feet by mistake.
add : Distance unit -> Distance unit -> Distance unit
add (Distance d1) (Distance d2) = Distance (d1 + d2)
這個模組以外的程式碼無法存取 Distance 型別的內部。使用者可以把兩個 Distance Meter 相加,卻不能把 Distance Meter 和 Distance Foot 相加。
import Distance exposing (Distance)
-- Compiles
twoMeters = Distance.add Distance.meter Distance.meter
-- Does not compile
errDist = Distance.add Distance.meter Distance.foot
任何型別都可以當作幻影型別,沒有限制,記錄型別也一樣可以。記錄當作幻影型別使用時,能夠表達會透過函式轉換的複雜限制。
我們在 Distance 模組裡加入一個新的距離單位,叫做 LegoBlock。實體積木是受到規範的物件,永遠具備兩個屬性:它們的距離必須是非分數且非負。
type LegoBlock = LegoBlock
fourStuds : Distance { properties | unit: LegoBlock, nonFractional : (), nonNegative: () }
fourStuds = Distance 4.0
nonFractional 和 nonNegative 是某個記錄的欄位,但這個記錄只存在於型別引數中,所以很適合把它們的型別設為 (),也就是 單元型別;它除了表示自己在這裡之外,不帶任何資訊。
當然,要取得任意的 LegoBlock 距離也是可以的,例如在計算距離的差值或比值之後。
negativeStud : Distance { properties | unit: LegoBlock, nonFractional : () }
negativeStud = Distance -1.0
threeFiddyStud : Distance { properties | unit: LegoBlock, nonNegative : () }
threeFiddyStud = Distance 3.50
crazyStud : Distance { properties | unit: LegoBlock }
crazyStud = Distance -13.37
上面這些值都合法,也都能通過編譯;不過我們特別在意像 fourStuds 這樣同時具有 nonFractional 和 nonNegative 兩個屬性的值,因為它們代表的是可以互相結合的實體積木:
combineLegoBlocks
: Distance { properties | unit: LegoBlock, nonFractional : (), nonNegative: () }
-> Distance { properties | unit: LegoBlock, nonFractional : (), nonNegative: () }
-> Distance { properties | unit: LegoBlock, nonFractional : (), nonNegative: () }
combineLegoBlocks = add
我們再加入一些函式,讓使用者可以建立和調整 LegoBlock 距離吧。
newLegoBlock : Float -> Distance { properties | unit: LegoBlock }
newLegoBlock dist = Distance dist
floorDistance : Distance properties -> Distance { properties | nonFractional : () }
floorDistance (Distance dist) = Distance (toFloat (floor dist))
ceilingDistance : Distance properties -> Distance { properties | nonFractional : () }
ceilingDistance (Distance dist) = Distance (toFloat (ceiling dist))
absDistance : Distance properties -> Distance { properties | nonNegative : () }
absDistance (Distance dist) = Distance (abs dist)
注意 floorDistance、ceilingDistance 和 absDistance 也能處理 LegoBlock 以外的單位,而且一般來說不會對輸入的屬性做任何假設,它們只保證輸出的值會具有某個特定屬性,也就是 nonFractional 或 nonNegative。
我們來看看一些結果。
import Distance exposing (Distance)
-- Compiles
distance1 = combineLegoBlocks fourStuds fourStuds
-- Does not compile
distance2 = combineLegoBlocks fourStuds threeFiddyStud
-- Compiles
distance3 = combineLegoBlocks fourStuds (floorDistance threeFiddyStud)
-- Does not compile
distance4 = combineLegoBlocks fourStuds negativeStud
-- Compiles
distance5 = combineLegoBlocks fourStuds (absDistance negativeStud)
-- Does not compile
distance6 = combineLegoBlocks fourStuds crazyStud
-- Compiles
distance7 = combineLegoBlocks fourStuds (floorDistance (absDistance crazyStud))
-- Compiles
distance8 = combineLegoBlocks fourStuds (ceilingDistance (absDistance crazyStud))
一般來說,floorDistance 和 ceilingDistance 會產生不同的結果,但提供相同的保證。這正是幻影型別技巧的威力:在維持強大保證的同時,給使用者有彈性的選擇。
你,邪惡大師,對遍布世界各地邪惡地城中每個 TreasureChest 的品質深感自豪。
要維持這份品質可不容易,你的地城管理員老是搞砸寶藏製作,於是你決定提供一個 Elm API,讓他們乖乖聽你的話。
有兩個條件是你絕不退讓的:
TreasureChest 必須由至少 8 個字元的安全密碼保護TreasureChest 都應該裝著獨一無二的寶藏你的地城管理員會提出一份密碼/寶藏建議清單,只有合適的 TreasureChest 才會被建立出來。
根據這些條件,要從一份建議清單建立安全的寶箱有兩種可能的方式:
這兩種做法可能不會得到相同的結果(例如 [("strong_password", GoldStatue), ("1234", GoldStatue)]),但你兩種都不介意,所以你想讓地城管理員自己決定。
一個能給使用者一些選擇,又能保證最終結果具備某些性質的 API? 聽起來正好適合幻影型別技巧!
TreasureChest 型別和它搭配的 getTreasure 已經提供好了,但你需要為寶箱建議構思一個型別。
實作 Chest 型別、實作 makeChest,並修正 secureChest 和 uniqueTreasures 的型別簽章。
makeChest 和 makeTreasureChest 的型別簽章已經在需求中提供,請不要修改它們。
Chest 應該裝著和 TreasureChest 相同的資料,並且有兩個型別引數,一個是 treasure,另一個是給 conditions 用的記錄幻影型別。
注意,因為 Chest 使用了幻影型別,所以應該是不透明的,以防止它在 TreasureFactory 模組之外被使用。
在這個情況下,它甚至完全沒有被公開。
修改 secureChest 和 uniqueTreasures 的型別簽章,用可擴充記錄作為幻影型別來加上限制。
secureChest 應該接受一個沒有特定條件的 Chest,並回傳 Maybe Chest,其幻影記錄中帶有額外的條件 securePassword : ()。
uniqueTreasures 應該接受一個沒有特定條件的 List Chest,並回傳 List Chest,其幻影記錄中加上額外的條件 uniqueTreasure : ()。
Elm 測試可以存取已公開的函式,但無法存取型別簽章,因此測試不可能驗證你用的簽章是否正確。 當然,要確認簽章是否正確,最有力的證明就是讓模組和測試能順利編譯並執行,不過我們寫了一條分析器規則,在你提交解法後會檢查你的型別簽章。
寶箱準備好之後,你需要挑出安全的那些。
實作 secureChest,讓它只對密碼長度為 8 個字元以上的寶箱回傳 Just。
符合該條件的 Chest 應該在其幻影型別中加上額外的 securePassword : () 條件。
地城裡只該允許最稀有的寶藏,就算只多一份也會讓寶藏顯得廉價。
實作 uniqueTreasures,它接受一個 Chest 的陣列,並回傳輸入陣列中擁有獨一無二寶藏的 Chest 陣列。
如果某個寶藏在輸入中出現兩次,它就不應該出現在輸出中。
輸入的 Chest 不應該有特定條件,輸出的則應該加上額外的條件 uniqueTreasure : ()。
好好享受最棒的 TreasureChest 吧,它們肯定會像蜜糖吸引蒼蠅一樣引來冒險者喔。
實作 makeTreasureChest,它接受一個既安全又獨一無二的 Chest,並建立一個 TreasureChest。
由於 TreasureChest 是不透明型別,這會是建立它的唯一方式,就連糟糕的地城管理員也無法搞砸。