【Cats】モナドの抽象化とエラーハンドリングについて

# 概要

このページでは

- `cats.Monad`を使ってモナドを抽象化する方法
- `cats.MonadError`を使ってモナドのエラーハンドリングを抽象化する方法

を紹介します。

## モナドを抽象化してできること

Scalaでコードを書いてる時、
**`あぁ、このメソッドどんなモナドでくるまれた値に対しても透過的に処理できるようにしておきたいなー`:innocent:**
ってパターンありますよね？

Catsを使えば、例えば

- **`Option`や`List`といった要素数に関するモナド**
- **`Try`と`Either`といったエラーを表現する時に使うモナド**
- **`Future`のような非同期を表現するモナドとそれ以外のモナド**

**を抽象化して実装できますし、**

- **モナドにくるまれてる時もくるまれてないときも1本のクラスやメソッドを使いまわす**

**ことも可能**です。(もちろんScalazでも同様のことができます)

#モナドの抽象化

catsには全てのモナドを抽象化するための型、`cats.Monad`が用意されています。

## cats.Monad

ここからは単純なコード例で紹介していきます。

```scala
import cats._, data._, implicits._
import scala.language.higherKinds

trait Foo[F[_]] {
  def bar(fa: F[Int]): F[String]
}

class FooImpl[F[_]](implicit monad: Monad[F]) extends Foo[F] {
  def bar(fa: F[Int]): F[String] = monad.map(fa)("bar" * _)
}
```

まず、抽象化したいモナドの型を型引数`F[_]`としてしまいます。　　
そして、暗黙の引数に`Monad[F]`を足すことでコンパイル時にCatsの`Monadインスタンス`が引き当てられるようにします。
(ここではモナドの型引数とモナドインスタンスの暗黙の引数をクラス側に書いてますがメソッド側に書いてしまっても構いません。臨機応変に。)

上記のクラスの使い方と実行結果(コメント)は下記のとおりです。

```scala
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
import scala.util.Try

type ThrowableOr[A] = Either[Throwable, A]

println(new FooImpl[cats.Id].bar(1))
// "bar"

println(new FooImpl[Option].bar(1.some))
// Some("bar")

println(new FooImpl[List].bar(1 :: 2 :: 3 :: Nil))
// List("bar", "barbar", "barbarbar")

println(new FooImpl[Try].bar(1.pure[Try]))
// Success("bar")

new FooImpl[Future].bar(1.pure[Future]) onComplete println
// Success("bar")

println(new FooImpl[ThrowableOr].bar(1.asRight)
// Right("bar")
```

いいですねCats！:cat:

**クラスの型引数に型を指定するだけで任意のモナドを透過的に1本の実装で対応できています。**
**しかも、モナドにくるまれてない引数に対しても、`cats.Id`(IDモナド)でエイリアスを張ることができています。**

ここまでは、思ったとおりの実行結果になっていますね:muscle:


#エラーハンドリングとMonadError

##モナドのエラーハンドリングの必要性について

しかし、実際のプログラミングでは不正な入力データや外部通信に失敗したときに当たり前のように例外が投げられてしまうものです。


```scala
class FooMightThrowImpl[F[_]](implicit monad: Monad[F]) extends Foo[F] {
  def bar(fa: F[Int]): F[String] = monad.map(fa)(mightErrorProcess)
  private def mightErrorProcess(a: Int): String = throw new Exception
}
```

上記のようにbarが例外を投げてしまう場合、それぞれのモナドでは下記の振る舞いとなります。

```scala
println(new FooMightThrowImpl[cats.Id].bar(1))
// 例外投げる

println(new FooMightThrowImpl[Option].bar(1.some)) 
// 例外投げる

println(new FooMightThrowImpl[List].bar(1 :: 2 :: 3 :: Nil))
// 例外投げる

println(new FooMightThrowImpl[Try].bar(1.pure[Try]))
// Failure(java.lang.Exception)

new FooMightThrowImpl[Future].bar(1.pure[Future]) onComplete println
// Failure(java.lang.Exception)

println(new FooMightThrowImpl[ThrowableOr].bar(1.asRight))
// 例外投げる (あれれ・・？？)
```

この場合、`cats.Id` `Option` `List`で例外を漏らしてしまうのは仕方ないとします。
気になるのは、`Try`と`Future`のときは例外を漏らさず`Failure`にくるまれて戻されていますが、**`Either`のときは例外を漏らしている**ことです。

`Either`の場合Left側の型をThrowableとしても特にCats側で気を利かせて例外をハンドルしてくれるわけではありません。これは一見直感に反しますが、`Either`は汎用的なOrを表現するモナドだからThrowableを特別視しないのは当然の振る舞いになります。

例外が投げられてくるような処理のエラーハンドリングは自前で考慮して実装しなければいけません。
これに対応するために、**Catsでは`cats.MonadError`が用意されています。**

## cats.MonadError

`cats.MonadError`は、エラーが発生する可能性があるモナドを抽象化したもので`cats.Monad`を継承しています。
コード中では、`Monad[F]`の代わりに`MonadError[F, E]`を使います。
実装例は下記になります。

```scala
class FooMonadErrorImpl[F[_]](implicit monadError: MonadError[F, Throwable]) extends Foo[F] {
  def bar(fa: F[Int]): F[String] = {
    for {
      a   <- fa
      res <- mightErrorProcess(a)
    } yield res
  }

  private def mightErrorProcess(a: Int): F[String] = {
    Either.catchNonFatal {
      throw new Exception
    } match {
      case Right(ok) => monadError.pure(ok)
      case Left (ko) => monadError.raiseError(ko) // raiseErrorはerror側のpureに相当する
    }
  }
}
```


`Try`や`Future`、`Either`のCats標準のモナドインスタンスには、既にMonadErrorの実装が用意されています。
特に、`Either`に対し`Try`や`Future`とエラー処理を共通化する場合は、`MonadError[F, Throwable]`とするとよいでしょう。

上記を使った結果はこちら。

```scala
println(new FooMonadErrorImpl[Try].bar(1.pure[Try]))
// Failure(java.lang.Exception)

new FooMonadErrorImpl[Future].bar(1.pure[Future]) onComplete println
// Failure(java.lang.Exception)

println(new FooMonadErrorImpl[ThrowableOr].bar(1.asRight))
// Left(java.lang.Exception)
```

想定通り、**`Try`と`Future`の時は`Failure`に、`Either`の時は`Left`に例外がくるまれて戻される**ようになりましたね:clap:

上記のようにMonadErrorで抽象化する場合、素の`Option`や`List`などのようにMonadErrorに対応しないモナドからは直接使えなくなりますが、モナドトランスフォーマーの`EitherT`と連携することで対応すればよいでしょう。

```scala
type OptionThrowableOr[A] = EitherT[Option, Throwable, A]
println(new FooMonadErrorImpl[OptionThrowableOr].bar(EitherT.pure(1)))
// EitherT(Some(Left(java.lang.Exception)))

type ListThrowableOr[A] = EitherT[List, Throwable, A]
println(new FooMonadErrorImpl[ListThrowableOr].bar(EitherT.right(1 :: 2 :: 3 :: Nil)))
// EitherT(List(Left(java.lang.Exception), Left(java.lang.Exception), Left(java.lang.Exception)))
```

#最後に

今回は `def raiseError[A](e: E): F[A]` しか使わない非常に単純な例で紹介しましたが、他にも`cats.MonadError`のメソッドには

- `def handleError[A](fa: F[A])(f: E => A): F[A]`: 失敗内容を見て成功に変換する。(TryやFutureのrecoverに相当)
- `def ensure[A](fa: F[A])(e: E)(f: A => Boolean): F[A]`: 成功の結果を条件でfilterして、false時失敗に変換する。

等があります。
OSSやプロダクションコード等ではこれらを活用しもっとしっかり作り込む必要があるでしょう。
よろしければ試してみて下さい:bow:

##参考資料

MonadErrorに関して、自分が勤める会社のCats勉強会の資料が下記に公開されています。
[scala with cats: Either, MonadError, Eval Monad @ road288の日記](http://road288.hatenablog.com/entry/2018/05/29/182953)