Runner in the High

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

Maybeに代えてカスタム型を使う

 ElmのMaybeはデータの有無を型で表現できるゆえ非常に便利なものであるが、文脈が失われるため無闇に使い過ぎるとワケがわからなくなる。ケースによっては、カスタム型を使うことによって型でデータの有無を Maybe に代えて表現するほうがよりメンテナブルにある可能性がある。

前提

 ユーザーログインの機能を持つTODOアプリを開発している。

 上記の仕様から、おそらくモデルに currentUser: User みたいなフィールドと TODO を表す todos: List Todo があるだろうと言うことは容易に想像できる。 だが、実際には「ログインしていない」と「ログインしている」のふたつのコンテキストを表現しなければいけないため、両方とも Maybe になる。ログインしていなければ両方共 Nothing になる。

type alias Model =
  {
    currentUser: Maybe User,
    todos: Maybe List Todo
  }

 正直ここの Maybe は苦しい。なぜならこの Maybe は文脈を持っていないからだ。

 例えば、ログインに失敗して Nothing なのか、純粋にまだ TODO を一個も持っていないから Nothing なのかは、ここでは分からない。 ユーザーがログインに失敗したから Nothing なのか、まだログインしていないから Nothing なのかも、判断が難しい。場当たり的な処置として Result 型を使う手もある。だが、結局文脈をもたない Maybe はコードを書く側が頑張ってその文脈をコード・リーディングしながら紐解いていくことになる。これはまず大変だし、バグを生む可能性がある。

 ではログインにまつわる文脈を持たせましょう、ということで LogInStatus 型のデータを与えると以下のようになる。

type alias Model =
  {
    currentUser: Maybe User,
    todos: Maybe List Todo,
    logInStatus: LogInStatus
  }

type LogInStatus
  = NotLoggedIn | LoggingIn | LogInSucceeded | LogInFailed

 これでモデルにログイン文脈のデータが与えられたが、それでもまだ苦しい。

 それぞれの Maybe の結果は LogInStatus と独立しているため、開発者のうちひとりが LogInSucceeded の状態であるにも関わらず currentUser を Nothing のままにしているなどのコードを書いてしまう可能性がある。ログインが成功しているのにユーザーがいない、というのはまず仕様としてもおかしい。

カスタム型で仕様を表現する

 Elmは型付言語なので、仕様を表現するような型制約を設けるのがよい

 たとえば、以下のようにデータが前提としてログインの状態に紐づくようにすることによって Maybe を使わずに currentUsertodos の存在有無を表現できる

type alias Model =
  {
    logInStatus: LogInStatus
  }

type alias UserData =
  {
    currentUser: User,
    todos: List Todo
  }

type LogInStatus
  = NotLoggedIn
    | LoggingIn
    | LogInSucceeded UserData
    | LogInFailed

 このコードであれば、まずログインが成功していない限りユーザーも、TODOも存在していないということが型レベルで表現されるため、もとのコードにあったような Maybe の結果とログイン状態が食い違ってしまうようなコードを書いてしまうのを防ぐことができる。