レコードは、関連する情報をラベル付きでまとめたデータ構造です。 JavaScriptやJavaのオブジェクト、CやRustの構造体に似ていますが、いくつか重要な違いがあります。
レコードは波括弧({})で作成し、要素はカンマで区切ります。
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の関数は小文字で始めなければならないので、これはときに意外に思えますが、型コンストラクターはその例外の1つです。
firefly : TvSeries
firefly = TvSeries "Firefly" "Joss Whedon" 14
レコードのインスタンスが持つフィールドの値にアクセスするには、firefly.creatorのようなドット記法を使うのが基本です。
Elmコンパイラーは、.で始まる特別なアクセサー関数もサポートしています。たとえば.creatorは、その名前のフィールドを持つどんなレコードにも使えます。
これが、関数は小文字で始めなければならないというルールの2つ目の例外です。
実際のところ、ユーザーが定義する関数は小文字で始めなければなりませんが、いま見た2つの例はプログラマーではなくコンパイラーが生成するものです。
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のすべての値がそうであるように、レコードは不変です。一度定義したレコードのフィールドの値を後から変えることはできません。
既存のレコードのコピーを作り、1つ以上のフィールドを変更する特別な構文があります。
この構文では、パイプ記号|を使って、元のレコードと変更するフィールドを区別します。たとえば{ oldRecord | field1 = newValue }のようになります。
firefly : TvSeries
firefly =
{ name = "Firefly"
, creator = "Joss Whedon"
, episodes = 14
}
updatedFirefly : TvSeries
updatedFirefly =
{ firefly | creator = "J Whedon", episodes = 15 }
Elmは[構造的等価性][equality]を採用しているので、同じレコードの2つのインスタンスは、値がまったく同じであれば等しいとみなされます。
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の歴史を深く掘り下げ、チームやコーチ、その勝敗の成績を追いかけながら、それらを互いに比較しています。
まだお気に入りのチームがないので、特定のチームを応援すべきかどうかを判断するアルゴリズムも開発します。 独自の_チーム応援_アルゴリズムを開発するための、7つのタスクがあります。
次の2つのフィールドをこの順序で持つレコードとして、Coach型エイリアスを定義します。
name: コーチの名前。型はString。formerPlayer: コーチが元選手かどうかを示します。型はBool。次の2つのフィールドをこの順序で持つレコードとして、Stats型エイリアスを定義します。
wins: 勝利数。型はInt。losses: 敗戦数。型はInt。次の3つのフィールドをこの順序で持つレコードとして、Team型エイリアスを定義します。
name: チームの名前。型はString。coach: チームのコーチ。型はCoach。stats: チームの成績。型はStats。チームという概念を持つサードパーティのコードと連携したいのですが、そのコードは入力を異なる順序で渡してきます。
次のシグネチャString -> Stats -> Coach -> Teamを持つcreateTeam関数を追加します。
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を取り出します。
チームを受け取り、そのチームを応援すべきならTrueを、そうでなければFalseを返すrootForTeam関数を実装します。
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