記錄是一種資料結構,把相關的資訊連同標籤歸類在一起。 它們類似 JavaScript 或 Java 裡的物件,或是 C 或 Rust 裡的 struct,但有一些關鍵的差異。
建立記錄時要使用大括號,而記錄中的元素以逗號分隔。
firefly =
{ name = "Firefly"
, creator = "Joss Whedon"
, episodes = 14
}
記錄的型別也用類似的語法定義,只是等號=換成了冒號:。
firefly : { name : String, creator : String, episodes : Int }
firefly =
{ name = "Firefly"
, creator = "Joss Whedon"
, episodes = 14
}
重複寫這些型別定義很麻煩,所以用型別別名來共用定義是常見的做法。
type alias TvSeries =
{ name : String
, creator : String
, episodes : Int
}
firefly : TvSeries
firefly =
{ name = "Firefly"
, creator = "Joss Whedon"
, episodes = 14
}
型別別名也會自動為該記錄提供一個建構函式。 這有時會讓人意外,因為 Elm 的函式必須以小寫字母開頭,而型別建構函式正是其中一個例外。
firefly : TvSeries
firefly = TvSeries "Firefly" "Joss Whedon" 14
存取記錄實例的欄位值的主要方式,是使用像firefly.creator這樣的點號記法。
Elm 編譯器也支援以點.開頭的特別存取函式,例如.creator,可用於任何具有同名欄位的記錄。
這是「函式必須以小寫字母開頭」這條規則的第二個例外。
事實上,使用者自訂的函式必須以小寫字母開頭,但我們剛剛看到的兩個例子是由編譯器產生的,而不是程式設計師寫的。
firefly : TvSeries
firefly =
{ name = "Firefly"
, creator = "Joss Whedon"
, episodes = 14
}
firefly.name
--> "Firefly"
.creator firefly
--> "Joss Whedon"
記錄也可以在綁定中被_解構_,這要用到記錄模式比對。 只要記錄具有這些名稱的欄位,就可以解構。
episodesCount : TvSeries -> Int
episodesCount { episodes } =
episodes
complicatedCopy : TvSeries -> TvSeries
complicatedCopy show =
let
{ name, creator } = show
in
{ name = name
, creator = creator
, episodes = episodesCount show
}
記錄和 Elm 裡的所有東西一樣都是不可變的,所以一旦定義了記錄,它的欄位值就永遠不會改變。
有一種特別的語法可以建立現有記錄的副本,同時改變其中一個或多個欄位。
這種語法使用直線符號|來區分原始記錄與要改變的欄位,例如{ oldRecord | field1 = newValue }。
firefly : TvSeries
firefly =
{ name = "Firefly"
, creator = "Joss Whedon"
, episodes = 14
}
updatedFirefly : TvSeries
updatedFirefly =
{ firefly | creator = "J Whedon", episodes = 15 }
Elm 使用[結構相等][equality],意思是同一個記錄的兩個實例若有相同的值,就相等。
firefly1 = TvSeries "Firefly" "Joss Whedon" 14
firefly2 = TvSeries "Firefly" "Joss Whedon" 14
firefly1 == firefly2
--> True
Elm 也支援結構型別,意思是如果某個函式需要一個具有 x 和 y 欄位的記錄,它就能用於任何具有這些欄位的記錄,例如 2D 點、3D 點、太空船等等。
這類型別在定義中有一個直線符號|,例如{ a | x : Float, y : Float },在 Elm 的術語中稱為「可擴充記錄」。
要注意型別定義中的直線符號|與實際記錄實例中的直線符號|之間的差異:前者是可擴充記錄的定義({ a | x : Int }),後者表示我們正在更新該記錄的某些欄位。
point2d = { x = 1, y = 2 }
point3d = { x = 3, y = 4, z = 7 }
.x point2d --> 1
.x point3d --> 3
length : { a | x : Float, y : Float } -> Float
length vector =
sqrt (vector.x * vector.x + vector.y * vector.y)
length point2d --> 2.236068
length point3d --> 5
translateX : { a | x : Float } -> { a | x : Float }
translateX vector =
{ vector | x = vector.x + 1 }
translateX point2d --> { x = 2, y = 2 }
translateX point3d --> { x = 4, y = 4, z = 7 }
在這個練習中,你是個超級運動迷,而且最近剛愛上 NBA 籃球。 由於才剛接觸 NBA 籃球,你正深入研究 NBA 的歷史,追蹤各支球隊、教練、他們的勝敗紀錄,並互相比較。
因為你還沒有支持的球隊,你也會開發一套演算法,判斷是否該為某支球隊加油。 你有七個任務,幫助你開發出專屬於你的「為球隊加油」演算法。
定義 Coach 型別別名,用來表示一筆記錄,包含以下兩個欄位,順序如下:
name:教練的名字,型別為String。formerPlayer:表示該教練是否曾是球員,型別為Bool。定義 Stats 型別別名,用來表示一筆記錄,包含以下兩個欄位,順序如下:
wins:勝場數,型別為Int。losses:敗場數,型別為Int。定義 Team 型別別名,用來表示一筆記錄,包含以下三個欄位,順序如下:
name:球隊的名字,型別為String。coach:球隊的教練,型別為Coach。stats:球隊的戰績,型別為Stats。你想與某些第三方程式碼介接,那些程式碼也有球隊的概念,但它們呼叫你的程式碼時,參數順序不同。請新增一個createTeam函式,型別簽章如下:String -> Stats -> Coach -> Team。
NBA 的老闆們沒什麼耐性,你發現球隊戰績不佳往往會導致教練被換掉。
實作replaceCoach函式,它接收新教練和球隊作為參數,並回傳換上新教練的球隊:
let
coach = { name = "Larry Bird", formerPlayer = True }
stats = { wins = 58, losses = 24 }
team = { name = "Indiana Pacers", coach = coach, stats = stats }
newCoach = { name = "Isiah Thomas", formerPlayer = True }
in
replaceCoach newCoach team
--> { name = "Indiana Pacers"
-- , coach = { name = "Isiah Thomas", formerPlayer = True }
-- , stats = { wins = 58, losses = 24 } }
未來你打算想出一套複雜的演算法來判斷是否該為某支球隊加油,但第一次嘗試時,你決定只要球隊的勝場數多於敗場數,你就該為它加油。
你希望這個函式能在其他情境中重複使用,所以在型別簽章中使用可擴充的記錄,以接受任何具有相關欄位的記錄。
為了讓函式盡可能簡潔,你想用模式比對從函式參數中取出stats。
實作rootForTeam函式,它接收一支球隊,如果你該為這支球隊加油就回傳True,否則回傳False。
let
spursCoach = { name = "Gregg Popovich", formerPlayer = False }
spursStats = { wins = 56, losses = 26 }
spursTeam = { name = "San Antonio Spurs", coach = spursCoach, stats = spursStats }
in
rootForTeam spursTeam
--> True