Elmは静的型付言語なので、型のチカラを活かすことでコンパイラに「あっては行けない状態や組み合わせ」をチェックさせることができる。 高度な例としては前回書いた以下の幽霊型(Phantom Type)の記事があるが、もう少し簡単な例を紹介しようと思う。
自作Error型の例
以下は自作のError型を定義した例だ。Error型はカスタムタイプになっていて、エラーのパターンを表現している。
それぞれのエラーはエラーのメッセージを持っていて、それらをtoString
関数で取り出す事ができる
module Error exposing (Error, handle, toString) type Error = Internal String | Connection String | Unknown String type HandledError = HandledError String handle : Error -> HandledError handle error = case error of Internal msg -> HandledError ("Internal: " ++ msg) Connection msg -> HandledError ("Connection: " ++ msg) Unknown msg -> HandledError ("Unknown: " ++ msg) toString : HandledError -> String toString (Error msg) = "Error: " ++ msg
このモジュールにはルールがあり、メッセージをStringとして取り出す前には絶対にhandle
関数が呼び出される必要がある。
したがって、Errorモジュールを呼び出すにあたっては、以下のようなコードはコンパイルエラーになる
import Error -- ... msg = somethingReturningError -- Error型を返す関数 |> Error.toString -- Error型を文字列に変換したい(が、コンパイルエラーになる)
なぜなら、Errorモジュールの実装によればtoString
関数は必ずHandleError型しか受け取れないからだ。
HandleError型はhandle
関数を呼ばなければ受け取ることができない。
なので、以下のようにhandle
関数の呼び出しを挟まなければtoString
関数は呼び出せない。
import Error -- ... msg = somethingReturningError -- Error型を返す関数 |> Error.handle -- Error型をハンドリング(←これが必須) |> Error.toString -- Error型を文字列に変換したい
何がうれしいのか
上の例で伝えたい点は「型によってモジュールの使い方にルールを与えることができる」という点だ。例えば、Error型とは別にHandledError型がモジュールに用意されていることで、モジュール内部でHandledError型をとる関数というのは、必ずhandle
関数を経由した上でしか呼ばれないということが自明になる。
関数シグニチャを見ることで、その関数がどのようなタイミングで呼ばれるかが分かるようになるし、コンパイラによるチェックも効く。そして、この仕組みをより高度に使った例が幽霊型(ファントムタイプ)だ。
実際のプロダクトにおいてどこまで型による制約を設けるかは堅牢性と複雑性のトレードオフになるが、型を自分たちのレールとして使うことの重要性は知っておいて損はない。