Runner in the High

技術のことをかくこころみ

Elmにおけるビルダパターンには後方互換性というメリットがある

Elmにおけるビルダパターンというのは以下のようなインターフェイスを指す。

-- setXxxのような関数をパイプでつないでいく雰囲気のやつ

Button.new
    |> setPadding 10
    |> setBackground "blue"
    |> setLabel "next"
    |> view

このインターフェイスの優れた点は新機能追加の際に後方互換性の維持が容易な点にある。つまり、今後 setXxx 系の関数が増えたとしても、その変更は既存でそのモジュールに依存している箇所に影響を与えないため、変更による影響範囲を最小にできる。

メリットを分かりやすくするために、ビルダパターンを採用せずに上記のインターフェイスをレコード型で表現してみるとしよう。

シンプルに考えると以下のようになる。

Button.view
    { padding = Nothing
    , background = Nothing
    , label = Just "next"
    }

可読性という観点では、上記のようにパラメタの名前を分かりやすくするためにビルダパターンを用いずRecord型を使うことはおかしくはない。しかし、ElmにおいてRecord型ではoptionalな性質のフィールドをMaybe型でしか表現することができない。これが意味するのは、新しい設定値のフィールドを追加するたびに既存で変更を加えたモジュールを利用している箇所すべてを修正しなければいけなくなってしまうということだ。

一方で、逆に考えてみるとレコード型や単純な引数でパラメタを与える実装はインターフェイスでそのパラメタが"必須である"ということを表明できる。

たとえば、上記のButtonモジュールにおいてlabelを必須のパラメタにしたいとする。その場合にはlabelの設定はビルダのインターフェイスになっていることは適切ではないため、次のようなインターフェイスにするほうが望ましい。

Button.view
    { label = "next" }
    |> setPadding 10
    |> setBackground "blue"
    |> view

このようなインターフェイスであれば、labelには必ずStringでなんらかのデータを与えないといけないということが明示できる。もちろん空文字列を与えてしまうことはできるが、その時点で違和感があるということに気付けるだけでもインターフェイス設計としては意味がある。

少し抽象化して考えるならば、あえてモジュールの変更を影響範囲として明確にさせたい(コンパイラにエラーを起こさせたい)ようなケースでは、ビルダパターンが適しているとは言えないということになる。

また、常ににビルダパターンを適用しようとすると対象のモジュールをほぼ必ずOpaque Typeの形に設計する必要があり、ここにも若干の手間がある。しかし、対象のモジュールが今後少なくない箇所から依存される未来が見えているのであれば後方互換性を維持する優先度が高いであろうし、ビルダパターンの適用はその時点で支払うべきコストだとも言える。

このように、状況に応じた観点でElmにおけるビルダパターンの使い分けを考慮してみると、モジュールのインターフェイス設計にはっきりとした意図を込められるだろう。

余談

なお、さらに応用的な実装例としてファントムビルダパターンというものがある。

medium.com

これは幽霊型を利用して「ある関数Aが呼ばれた場合のみ、関数Bも呼び出せるようにする」という制約を与え、ビルダのインターフェイスに依存関係を作りだすテクニックである。

内部的にやっていることは自分がelm-firestoreのインターフェイス設計で話したことと同じであるので、詳しくはそちらの記事を参照のこと。

izumisy.work