JKになりたい

何か書きたいことを書きます。主にWeb方面の技術系記事が多いかも。

モナドトランスフォーマーを使ってモナドを合成する

ネストしたモナドの取り扱い

モナドがネストして帰ってくることがあります。
例えば、DBに接続してユーザレコードを取り出す時、DBに接続できたかどうかを表すEither、該当のユーザがいたかどうかを表すOptionのモナドがネストして帰ってくるケースを考えてみます。

val fetchResult: Either[Error, Option[User]] = //DBからUserをFetch

このfetchResultから、ユーザ情報にアクセスするためには、以下のようなコードを書く事になると思います。

for {
  maybeUser <- fetchResult
} yield  {
  for {
    user <- maybeUser
  } yield user
}
fetchResult.map{maybeUser =>
  maybeUser.flatMap{ user =>
    ???
  }
}

どっちもネストして嫌なコードになってしまいました。

モナドAとモナドBを合成できれば、1つのflatMapだけで操作ができるようになるのですが、モナドは通常合成できません。
というのも、flatMapの実装ができないからです。

def compose[M1[_]: Monad, M2[_]: Monad] = {
  type Composed[A] = M1[M2[A]]
  new Monad[Composed] {
    def pure[A](a: A): Composed[A] =
      a.pure[M2].pure[M1]
    def flatMap[A, B](fa: Composed[A])(f: A => Composed[B]): Composed[B] = ???
  }
}

ただし、M2モナドが確定すれば、合成することができます。
例えば、M2がOptionだとしたら、以下のように合成できます。

type Composed[A] = M[Option[A]]

def flatMap[A, B](fa: Composed[A])(f: A => Composed[B]): Composed[B] = 
    Monad[M].flatMap(fa) {
      case None => Option.empty[B].pure[M]
      case Some(a) => f(a)
    }

と、いうわけで、内部のモナドを確定できればモナドの合成ができるらしいです。

catsでは、EitherやOptionなどを内部のモナドとして合成できる便利なトランスフォーマーが定義されています。

モナドトランスフォーマーを使ってモナドを合成、操作する

catsでは、OptionTやEitherT等の「T」をつけた型でもなどトランスフォーマーが提供されています。

これを使ってListモナドとOptionモナドを合成する例を見てみます。

val listOption: OptionT[List, A] = OptionT(List(Option(10)))
val listOption2: OptionT[List, A] = OptionT(List(Option(10)))
val listOption3 = for {
    x <- listOption
    y <- listOption2
} yield x + y

簡単に合成したモナドを取り扱う事ができるようになりました。

次に、EitherとOptionの合成を見てみます。先の例で出てきた、Either[Error, Option[User]]です。

内部のモナドはOptionなので、OptionTを使えばよいのですが、Eitherの型が確定しません。

val fetchResult: OptionT[Either, User] = //Eitherは2つ型パラメータを取るのに1つしか渡せない

そこで、タイプエイリアスを使用してEItherの片方の型を確定させます。

type ErrorOr[A] = Either[Error, A]
val fetchResult: OptionT[ErrorOr, User]

このようにモナドを合成することができます。 また、3つ以上のモナドを合成する時も同様、タイプエイリアスを使って型パラメータを補完しながら合成していきます。

//Future[Either[String, Option[Int]]]の合成
type FutureEither[A] = EitherT[Future, String, A]
type FutureEitherOption[A] = OptionT[FutureEither, A]
10.pure[FutureEitherOption] 

また、モナドトランスフォーマはvalueメソッドを持っており、これを利用して合成されたモナドをアンパックすることができます。

valueメソッドを呼び出す度、内部から順番に1つずつアンパックされていきます。

val first: FutureEitherOption[Int] = 10.pure[FutureEitherOption]
val second: FutureEither[Option[Int]] = first.value
val third: Future[Either[String, Option[Int]]] = second.value

モナドトランスフォーマーの利用は局所的な関数内でとどめておき、値をかえす時はvalueによりアンパックした状態にしておくのが良いかもしれません。