Runner in the High

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

elm-typedパッケージを使えばもっと型で仕様を表現できるかも

elm-typedというパッケージを作った。詳しくは以下のGithubのREADMEに書いている。

github.com

パッケージ自体が根本的に実現したいことはPunie/elm-idとかjoneshf/elm-taggedあたりとほぼほぼ同じ。プリミティブ型をそのまま使わず型エイリアスでもなく、プリミティブ型をラップしたカスタム型を使うことで仕様をもっと型に表現したいというモチベーションから来ている。

実際に使う場合には、以下のようにelm-typedが提供している Typed 型に対して、タグとなる型(MemberIdTypeとかAgeTypeあたり)とその型が内部的に持つプリミティブ型、そして最後にパーミッション(後述)を指定する。

import Typed exposing (Typed, ReadOnly, ReadWrite)


type MemberIdType
    = MemberIdType

type alias MemberId
    = Typed MemberIdType String ReadOnly

type AgeType
    = AgeType

type alias Age
    = Typed AgeType Int ReadWrite

type alias Model =
    { memberId : MemberId
    , age : Age
    }

パーミッションはelm-idとelm-taggedを折衷するための機構で、ある Typed なデータに対して任意のパーミッションを指定することで、Elmアプリケーションにおけるそのデータの作成/更新を制限することができる*1

たとえば、上のサンプルでは MemberIdReadOnly に指定されているため、newやmapなど更新系関数の呼び出しはできない。例外的に ReadOnly であっても encode/decode はできるようになっているが、Elmアプリケーションに値が生まれた時点ではもう書き込み不可である。

newAge : Age
newAge =
    Typed.new 30 -- OK!

newMemberId : MemberId
newMemberId =
    Typed.new "1" -- ReadOnly指定なので新規にデータを作成できない(コンパイルエラーになる)

パーミッションの利点として、フロントエンド・アプリケーションにおいて参照のみで使うデータ(サーバーサイドから取得されるIDなど)をReadOnly指定することで、仕様上更新してはいけないデータを更新してしまうなどのバグを未然に防止できる。また、コードレビューやコードリーディングの際にもModelを読むだけでどのデータが更新される仕様なのかが把握しやすい。

実際に個人開発のElmアプリケーションでこのパッケージをテスト導入をしているが、それまでOpaque Typeなモジュールとして設計していた型が置き換えられていてイイ感じ。カスタム型による仕様の表現だけじゃなく、型の定義でデータのライフサイクルまで制御できるのはわりかし便利だ。

*1:パーミッションごとに呼び出せる関数を幽霊型で制限して定義している