6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-01-14

概要

このページでは

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

を紹介します。

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

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

Catsを使えば、例えば

  • OptionListといった要素数に関するモナド
  • TryEitherといったエラーを表現する時に使うモナド
  • Futureのような非同期を表現するモナドとそれ以外のモナド

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

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

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

#モナドの抽象化

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

cats.Monad

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

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インスタンスが引き当てられるようにします。
(ここではモナドの型引数とモナドインスタンスの暗黙の引数をクラス側に書いてますがメソッド側に書いてしまっても構いません。臨機応変に。)

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

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

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

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

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が例外を投げてしまう場合、それぞれのモナドでは下記の振る舞いとなります。

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で例外を漏らしてしまうのは仕方ないとします。
気になるのは、TryFutureのときは例外を漏らさずFailureにくるまれて戻されていますが、Eitherのときは例外を漏らしていることです。

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

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

cats.MonadError

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

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に相当する
    }
  }
}

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

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

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)

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

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

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の日記

6
4
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?