Cats MTL のご紹介
はじめに
スタンバイではシステム開発に主に Scala
を使用しています。またその一部では、モナドやエフェクトを使用して型安全で堅牢なシステムを構築しているシステムもあります。
モナドやエフェクトの合成においては、それらを書きやすい形でラップアップした Eff などがありますが、一方で Scala
の関数型ライブラリである Cats とシームレスに統合可能である Cats MTL というライブラリがあり、書きやすさの面でも過去のバージョンと比較してかなり向上しています。
ここでは、 Cats MTL をアプリケーションにどのように取り入れるのかをサンプルコードを元に紹介していきます。
本文中のサンプルコードで使用した各ライブラリのバージョンは以下の通りです。
scala : 3.1.0 cats : 2.6.1 cats-mtl : 1.2.1 cats-effect : 3.3.1
また、Scala のコンパイルオプションは kind projector
を有効にするため -Ykind-projector
を加える必要があります。詳細な情報は下記URLを参照してください。
https://docs.scala-lang.org/scala3/guides/migration/plugin-kind-projector.html
Cats MTL とは
Cats MTL (Monad Transformer Library) はその名の通りScalaの関数型ライブラリである Cats の Applicative
などに対応するモナドトランスフォーマー用の型クラスを提供するライブラリです。
例えば、下記のようにモナドトランスフォーマーを複数使用する場合、ネストしたモナドトランスフォーマーをそのまま使用するとコードが非常に複雑になってしまいます。
import cats.data._ def checkState: EitherT[StateT[List, Int, ?], Exception, String] = for { currentState <- EitherT.liftF(StateT.get[List, Int]) result <- if (currentState > 10) EitherT.leftT[StateT[List, Int, ?], String](new Exception("Too large")) else EitherT.rightT[StateT[List, Int, ?], Exception]("All good") } yield result
Cats MTL では、このような問題を解決しモナドトランスフォーマーをハンドリングするコードを抑えることで、コードの記述性や可読性を高めます。
import cats.MonadError import cats.syntax.all._ import cats.mtl.Stateful def checkState[F[_]](implicit S: Stateful[F, Int], E: MonadError[F, Exception]): F[String] = for { currentState <- S.get result <- if (currentState > 10) E.raiseError(new Exception("Too large")) else E.pure("All good") } yield result
同じ内容のコードが非常に見やすくなりました。
ユースケース
Cats MTL で提供される型クラスの内、アプリケーション内でよく利用される Ask
と Raise
についてサンプルコードを交えて紹介していきます。
ここでは、IDに一致するユーザー情報を取得し、特定の条件に適合すればユーザー情報をそのまま返し、適合しなければエラーを返すという簡単な
ユースーケースを例として考えてみます。
Ask
は Kleisli#ask
を型クラスとして表したもので、何かしらの値を保持し、それを読み出して利用するために使われます。
この型クラスを利用することで、処理の中で動的にアプリケーションの設定を読み込んだり、依存するインスタンスを注入(DI)したりできます。
import cats._ import cats.syntax.all._ import cats.mtl._ object UserUseCase: def user[F[_]: Monad]( id: UserId )(implicit A: Ask[F, UserAlgebra] ): F[IdentifiedUser] = for { alg <- A.ask user <- alg.user[F](id) } yield user
Raise
は Functor
に対して例外やエラーの質を表す型,例えば Either[E, A]
を発生させる機能を提供するものです。
ApplicativeError#mapError
と同等の機能を提供しますが、型は ApplicativeError
である必要はありません。
この型クラスを利用することで、処理条件によってエラーとして Left
を生成できます。
特定の条件に適合しない場合にエラーを返す、という仕様をRaise
を使用して実装してみましょう。
import cats._ import cats.syntax.all._ import cats.mtl._ object UserUseCase: def user[F[_]: Monad]( id: UserId )(implicit R: Raise[F, UserError], A: Ask[F, UserAlgebra] ): F[IdentifiedUser] = for { alg <- A.ask user <- alg.user[F](id) identified <- if(user.invalid) R.raise(UserError(id)) else user } yield identified
上記で作成したプログラムを実行します。実行結果を
実行するメソッドの型引数には型クラスのインスタンスである EitherT
と Kleisli
を指定します。(実際には型引数は別途 type
として宣言しておくと見やすくなります。)
実行結果として取得できた EitherT[Kleisli[...]]
に対して EitherT#value
および Kleisli#run
を行い、最終的に Either[UserError, IdentifiedUser]
を取得します。
import cats._ import cats.data._ import cats.effect.IO object Main: def main(args: List[String]) = UserUseCase.user[EitherT[Kleisli[IO, UserAlgebra, _], UserError, _]] (UserId(args(0))) .value .run(UserAlgebra)
おわりに
Cats MTLを使用することで、コードの記述と実行をうまく分離しつつ、アプリケーションを構築するために必要となる機能(エラー処理や依存性の注入など)を簡易なコードで実現できました。
また モナドトランスフォーマーの lift
を行わずとも、平坦な for
文のみでUseCase を記述することが出来、記述性、可読性がともに大きく向上しました。
モナドの性質や使い方を学習していくにはそれなりにコストがかかりますが、Cats MTL の様なライブラリを導入することであまりモナドやモナドトランスフォーマーに対する造詣が無くてもアプリケーションの
機能や記述性を損なうこと無く適切な制約を持たせた強いコードを書くことができます。
参照
Cats MTL: https://typelevel.org/cats-mtl/
積極採用中です
スタンバイのプロダクトや組織について詳しく知りたい方は、気軽にご相談ください。