Elmを始めると躓く可能性がとても高い**カスタム型 (Custom type)**を噛み砕いて説明していこうと思います。なんとなく良さだったり、出くわしたときにウッてならずに実用に読んだり使えたりなるのが目的です。そのため代数的データ構造・直和・直積のようなキーワードは出さずに、多少厳密さを欠いて説明するのでご容赦ください。
限定して並べる
カスタム型は他の型と同様に自分で定義出来る型になります。とても文法的に似たものに型の別名(タイプエイリアス)がありますが、それとは違い、全く本当に新しい型を定義出来ます。カスタム型ではない既存の型を思い浮かべてみましょう。例えば、Int型です。Int型の値を並べてみてよ!って言われた場合には無限に数字を並べることができます(安全な範囲はコンパイラ等で決まります)。String型も、"", " ", "a", "b", "ab",...
挙げだしたらキリがないですね? Elmで作る仮に「段級位制」を導入したとします。段位に応じて支払われるお金を決める関数を定義すると以下のようになるでしょう。
prizeMoney : Int -> Int
prizeMoney grade =
case grade of
1 ->
100
2 ->
200
3 ->
300
4 ->
400
5 ->
1000
_ ->
-1 -- 例外とかnull投げたい
Elmは厳密な言語なのでif式を書いたらelseも必須。case式もすべてのケースを満たさなければコンパイラが通してくれません。仕方なく-1を返していますが例外やnullを投げたいのがよくある言語の特徴だと思います。すべてのケースとはなんでしょうか?先程言った「並べてみてよ!」な値です。それではどうするでしょうか?自分で可能性を並べてみれば良いのです!
type Grade
= Ichi
| Ni
| San
| Yon
| Go
prizeMoney : Grade -> Int
prizeMoney grade =
case grade of
Ichi ->
100
Ni ->
200
San ->
300
Yon ->
400
Go ->
1000
-- > prizeMoney Yon
-- 400 : number
これで例外やnullを投げる必要が無くなりシンプルでイージーにプログラミングをすることができます。さらに、Intのバージョンであったケースの抜け漏れのコンパイルエラーが望ましい形で叱ってくれるようになります。とても親切なエラーを出してくれます。
構造を分解して取り出す
自分で定義した型に値を持たせたい場合があります。よくあるのはユーザを表す型です。
type alias Id =
Int
type alias Name =
String
type User
= User Id Name -- User Int String と等価
getName : User -> Name
getName user =
case user of
User _ name ->
name
-- getName (User _ name) = -- 「パターンが一つの場合」こう書くことも出来ます。
-- name
-- > john = User 1 "John"
-- > getName john
-- "John": String
caseはとても強力で中の構造を分解して取り出せます。ただ、このようなケースの場合、レコードを使ったほうが楽です。
type alias Id =
Int
type alias Name =
String
type alias User =
{ id : Id, name : Name }
-- > user = User 1 "John"
-- > user.name
-- "John": String
-- > { user | id = 2 }
-- { id = 2, name = "John" }
-- : { id : Id, name : Name }
何故なら漏れなく、コンストラクタ(呼び出し方はカスタム型と同じです。)・Getter・Setterが付いてくるからです。 そのためカスタム型とレコード型の使い分けに困ってしまう方が多いです。それでは、先程説明した限定して並べると構造を分解して取り出すの能力をミックスするとどうなるでしょうか?
限定して並べて、分解する
さっそく例を見てみましょう。
type alias Id =
Int
type alias Name =
String
type User
= User Id Name
| Guest
getName : User -> Name
getName user =
case user of
User _ name ->
name
Guest ->
"Guest"
-- > john = User 1 "John"
-- > getName john
-- "John": String
-- > getName Guest
-- "Guest": String
まとめると、可能性を限定して安全なプログラミングが行えて、データ構造を分解して扱える、と言う一度で2度美味しい仕組みを手に入れることができました。
Elmでの使われ方
言ってしまえばカスタム型は言語が持つ組み込みの型(IntやStringなど)とレコード型以外はすべてカスタム型で表現されています。とてもわかりやすくカスタム型の特徴を活かしているのがMaybe型です。この型は値があるかないかを2つのパターンとして「限定して並べて」いる型です。今までの説明に加えて、一つ違うポイントは型変数aを持っているポイントです。値があるかないかと言うのは汎用的な仕組みなためどんな型でも受けれるようにしているということですね。(以下の例はMaybe Intの場合) それではMaybe型から値を取り出すときのwithDefault
の実装をパターンマッチ(case式の並べて分解する手法)で想像してみましょう。
type Maybe a
= Just a
| Nothing
-- > Maybe.withDefault 100 (Just 42)
-- > 42
-- > Maybe.withDefault 100 Nothing
-- > 100
withDefault : a -> Maybe a -> a
withDefault defaultValue aMaybe =
case aMaybe of
Just a ->
a
Nothing ->
defaultValue
これで色んな型が出てきても「限定して並べて」「構造を分解して取り出す」の呼吸で扱うことが出来るはずです!それでは素敵なElmライフを!