来世から頑張る!!

技術ブログを目指して

IndexedContを使ってみよう: 前提知識編

この記事は?

IndexedCont便利なのでみんな使ってほしいとの思いの元、前提となる知識から解説する記事です。 なお、次の次の回ぐらいまでIndexedCont出てきません

本編

この記事ではエラーハンドリングのためにIndexedContを使えるようになることを目標とします。

Monad Transformerには触れませんので、別記事を探してください。

Step 1: Optionによるエラーハンドリング

理解しやすいエラーハンドリングの基本として、Optionを使用したエラーハンドリングの例からスタートします。

対象コード

必要十分な最低限の例として、下記のようなオブジェクトを取り扱います。

case class AID(value: Int)
case class BID(value: Int)
case class CID(value: Int)

case class A(id: AID, bID: BID)
case class B(id: BID, cID: CID)
case class C(id: CID)

case clas Found(a: A, b: B, c: C)

// 取得用のインターフェース(traitなどに定義): 見つからない場合はNoneを返す
def getA(id: AID): Option[A]
def getB(id: BID): Option[B]
def getC(id: CID): Option[C]

def find(aID: AID): Option[Found]

findを実装するのが目的となります。

実装

取得用インターフェースはわかりやすい例として下記のように実装してみます。

def getA(id: AID): Option[A] =
  if (id.value % 2 == 0) Some(A(id, BID(id.value * 2)))
  else None

def getB(id: BID): Option[B] =
  if (id.value > 12) Some(B(id, CID(id.value - 8)))
  else None

def getC(id: CID): Option[C] =
  if (id.value < 10) Some(C(id))
  else None

これに対して、findの実装は以下のようになります。

def find(aID: AID): Option[Found] = for {
  a <- getA(aID)
  b <- getB(a.bID)
  c <- getC(b.cID)
} yield Found(a, b, c)

解説

Optionのfor式はどこかでNoneが返ってきたらその行以降は実行されずにNoneが返ります。

scala> find(AID(5)) // Aが見つからない
res0: Opiton[Found] = None

scala> find(AID(6)) // Bが見つからない
res1: Opiton[Found] = None

scala> find(AID(18)) // Cが見つからない
res2: Opiton[Found] = None

scala> find(AID(8)) // すべて見つかる
res3: Opiton[Found] = Some(Found(A(AID(8), BID(16)), B(BID(16), CID(8)), C(CID(8))))

Step 2: Eitherを使用してエラー内容を特定する

Optionではどこか途中で失敗すると、どこで失敗してもNoneとなります。

どこで失敗したかを知りたい場合の一番シンプルな方法がEitherです。

対象コード

Optionの例そのままで、findだけ変更します。

def find(aID: AID): Either[String, Found]

実装

こちらもfind以外はそのままです。

def find(aID: AID): Either[String, Found] = for {
  a <- getA(aID).toRight(s"AID: ${aID}のAが見つかりませんでした。")
  b <- getB(a.bID).toRight(s"BID: ${a.bID}のBが見つかりませんでした。")
  c <- getC(b.cID).toRight(s"CID: ${b.cID}のCが見つかりませんでした。")
} yield Found(a, b, c)

解説

EitherOptionと同様に1度でもLeftになるとそれ以降の行は実行されません。

scala> find(AID(5)) // Aが見つからない
res0: Either[String, Found] = Left("AID: 5のAが見つかりませんでした。")

scala> find(AID(6)) // Bが見つからない
res1: Either[String, Found] = Left("BID: 12のBが見つかりませんでした。")

scalva> find(AID(19)) // Cが見つからない
res2: Either[String, Found] = Left("CID: 11のCが見つかりませんでした。")

scala> find(AID(8)) // すべて見つかる
res3: Either[String, Found] = Right(Found(A(AID(8), BID(16)), B(BID(16), CID(8)), C(CID(8))))

ただし、ひとつのfor式の中で使われるEitherはすべて左側の型が同じである必要があります。

応用

左側がStringでは扱いにくいので、自身で定義した型をここに入れたりできます。

sealed trait FindingError

case class ANotFound(id: AID) extends FindingError
case class BNotFound(id: BID) extends FindingError
case class CNotFound(id: CID) extends FindingError

def find(aID: AID): Either[FindingError, Found] = for {
  a <- getA(aID).toRight[FindingError](ANotFound(aID))
  b <- getB(a.bID).toRight(BNotFound(a.bID))
  c <- getC(b.cID).toRight(CNotFound(b.cID))
} yield Found(a, b, c)

次回にむけて

次回はIndexedContのシンプルな形Contを扱います。

たぶん今回の記事にも時間があれば追記します。 (コードを実際に動かす時間がなかったので動かしてみて修正したりします。)