Cats MTL 〜 8つの型クラス


モナドトランスフォーマーの型クラスを提供する Cats の MTL について。
※ Scala 3 と最新ライブラリで書き直した (2022-08-14)

## はじめに
モナドあるいはエフェクトの合成といえば、昨今では書法のシンプルさでもパフォーマンスでも Eff が有力な選択肢になるかもしれない。ただしモナドトランスフォーマーの型クラスを提供する **MTL**（あるいは MTLスタイル）も結構使われている[^monad_action]ので、MTL を活用した既存ライブラリを使う際にも、ぱっと見てだいたい理解できるくらいにはしておきたい。この記事では、Cats Mtl の[８つの型クラス](https://typelevel.org/cats-mtl/mtl-classes.html)に着目してみる。

表にすると以下のようになる。

| 型クラス   | 使いみち                | 主なメソッド | 継承元 |
|:--:       |:--                     |:--         |:--   |
| Tell      |ログ（値の累積）           |tell        |      |
| Ask       |環境（設定、依存オブジェクト）|ask         |      |
| Stateful  |状態                     |get, set    |  |
| Chronicle |ログ＋エラーによる中断	     |dictate, confess||
| Listen	|途中で読めるログ	     |listen      |Tell|
| Local     |ローカルに変更できる環境     |local       |Ask|
| Raise     |エラーによる中断            | raise      ||
| Handle    |エラーとエラー処理／リカバリー | handleWith |Raise|

合成・併用をせずに単体で使う場合は単なるモナドとあまり変わりないが、まず使い勝手を見るために、以下、一個ずつサンプルコードを書いてみる。

### Tell
[Tell](https://typelevel.org/cats-mtl/mtl-classes/tell.html) は `WriterT#tell` を型クラスとして抽出したようなもので、典型的には計算過程で何かの値を「文脈」に蓄積するような、たとえばログ目的で使われる。たとえば`String`値を `Chain` に蓄積する以下のような関数が書ける。

```scala
type Logs = Chain[String]

def tellFunc[F[_]](n: Int)(using F: Tell[F, Logs], S: Sync[F]): F[Unit] =
  for
    _ <- F.tell(one("begin"))
    _ <- S.delay { println(s"n = $n") }
    _ <- F.tell(one("end"))
  yield ()
```
`F[_]` の制約として `Tell` の他に、for comprehension のための `FlatMap` と、遅延IOのための `Sync` を指定している。次のように実行できる。

```scala
def program: IO[Logs] = tellFunc[WriterT[IO, Logs, *]](42).written

program.unsafeRunSync()
// n = 42
// res0: Logs = Chain(begin, end)
```
Monix の `Task`でも、Cats Effect の `IO` でも良いが、ここでは後者を `WriterT` と組み合わせた。

`WriterT` の他に、`Tuple2`、`RWST` のインスタンスが提供されている。

ソース: [tell.worksheet.sc](https://github.com/yasuabe/qiita-cats3-sample/blob/main/src/main/scala/mtl/tell.worksheet.sc)

### Ask
[Ask](https://typelevel.org/cats-mtl/mtl-classes/ask.html) は `Kleisli#ask` を型クラスとして抽出したようなもので、「文脈」から何かの値、例えば、環境、設定、依存オブジェクトなどを取得する目的で使われる。たとえば以下のように `Map[String, String]`型の設定情報にアクセスする関数が書ける。遅延IO で println している部分は、他サービスや永続化層へのアクセスなどを模している。

```scala
type Config = Map[String, String]

def askFunc[F[_]](key: String)(using F: Ask[F, Config], S: Sync[F]): F[String] =
  for
    config <- F.ask
    result =  config.getOrElse(key, "none")
    _      <- S.delay { println(s"$key -> $result") }
  yield result
```
`Kleisli` と Cats Effect `IO` を組み合わせて `traverse` すると、以下のような結果が得られる。

```scala
val program: IO[List[String]] =
  List("21", "42", "63")
    .traverse(askFunc[Kleisli[IO, Config, *]])
    .run(Map("42" -> "foo"))

program.unsafeRunSync()
// 21 -> none// 42 -> foo
// 63 -> none
// res0: List[String] = List(none, foo, none)
```
`Kleisli`=`ReaderT`以外にも、環境 => ? となる関数や、`RWST` のインスタンスも提供される。

ソース: [ask.worksheet.sc](https://github.com/yasuabe/qiita-cats3-sample/blob/main/src/main/scala/mtl/ask.worksheet.sc)

### Stateful
[Stateful](https://typelevel.org/cats-mtl/mtl-classes/stateful.html) はその名の通り `StateT` の型クラスで、「文脈」が持つ**状態**の取得・変更のために使われる。たとえば以下のように、整数を３倍する計算を`Map[Int, Int]` 型の状態にキャッシュする関数が書ける。

```scala
type Cache = Map[Int, Int]

def triple[F[_]: Sync](n: Int): F[Int] =
  Sync[F].delay { println(show"triple($n)") } as (n * 3)

def tripleWithCache[F[_]: Sync](n: Int)(using F: Stateful[F, Cache]): F[Int] =
  for
    memo  <- F.get
    value <- memo.get(n)
      .fold(triple[F](n) >>= { v => F.modify(_.updated(n, v)) as v })(_.pure[F])
  yield value
```
実行すると以下のような結果になる。1 はリストに二つ含まれているが、キャッシュされるので `triple` は一度しか呼ばれない。3は最初からキャッシュに入っているので全く呼ばれない。

```scala
List(1, 3, 1, 2, 3)
  .traverse(tripleWithCache[StateT[IO, Cache, *]])
  .run(Map(3 -> 9))
  .unsafeRunSync()
// triple(1)
// triple(2)
// (Map(3 -> 9, 1 -> 3, 2 -> 6), List(3, 9, 3, 6, 9))
```
`StateT`の他に `RWST` のインスタンスも提供される。

ソース: [state.worksheet.sc](https://github.com/yasuabe/qiita-cats3-sample/blob/main/src/main/scala/mtl/state.worksheet.sc)

### Chronicle
[Chronicle](https://typelevel.org/cats-mtl/mtl-classes/chronicle.html) は他のMTL型クラスたちと違って、既存のモナドの動きからは類推しにくい。[ドキュメント](https://typelevel.org/cats-mtl/mtl-classes/chronicle.html)によれば `Tell` と後述の `Handle` のハイブリッドで、以下が主な操作になるという（定義を見ると他にもたくさんある）。

```scala
 trait MonadChronicle[F[_], E] {
  def dictate(c: E): F[Unit]               // Tell#tell
  def confess[A](c: E): F[A]               // Raise#raise
  def materialize[A](fa: F[A]): F[E Ior A] // 直前の計算が dictate か confess かで変わる Ior
}
```
例えば以下のような関数が書ける。与えられた`n` が5以上ならログに追加して処理中断、3以上ならログに追加して続行、2以下ならそのまま `n` を値とするような計算としてみた。

```scala
def func[F[_]: Monad](n: Int)(using F: Chronicle[F, Chain[Int]]): F[Int] =
  if      n > 4 then F.confess(Chain(-n))     // Raise#raise 相当
  else if n > 2 then F.dictate(Chain(n)) as n // Tell#tell 相当
                else n.pure[F]
```
以下のように計算を実行できる。`IorT` のインスタンスも提供されているが、簡単のためここでは `Ior` を使った[^ior]。

```scala
type IorC[A] = Ior[Chain[Int], A]

(1 to 1).toList.traverse(func[IorC]) // Right(List(1))
(1 to 2).toList.traverse(func[IorC]) // Right(List(1, 2))
(1 to 3).toList.traverse(func[IorC]) // Both(Chain(3),List(1, 2, 3))
(1 to 4).toList.traverse(func[IorC]) // Both(Chain(3, 4),List(1, 2, 3, 4))
(1 to 5).toList.traverse(func[IorC]) // Left(Chain(3, 4, -5))
(1 to 6).toList.traverse(func[IorC]) // Left(Chain(3, 4, -5))
```
1、1〜2 では、値のみが `Ior` の `Right` で得られる。1〜3、1〜4では、ログと値の両方が `Both` で得られる。1〜5、1〜6では、ログのみが `Left` で得られるが、`func(5)` で `confess` されるため、1〜6 でも 6 は含まれない。

ソース: [chronicle.worksheet.sc](https://github.com/yasuabe/qiita-cats3-sample/blob/main/src/main/scala/mtl/chronicle.worksheet.sc)

### Listen
[Listen](https://typelevel.org/cats-mtl/mtl-classes/listen.html) は、文脈に蓄積されたログにアクセスするためのメソッド `listen` を`Tell` に追加したもの。たとえば、上の `Tell`のサンプルで書いたメソッド `tellFunc` のログを、同じ`F[_]`のコンテキストで別の関数（ここではログ収集サーバへの送信を模した）に渡すプログラムが、以下のように書ける。

```scala
def send[F[_]: Sync](logs: Logs): F[Unit] =
  Sync[F].delay { println(s"Sending to log server: $logs") }

def program[F[_]: Sync: [F[_]] =>> Listen[F, Logs]](n: Int) =
  tellFunc[F](n).listen >>= ((_, logs) => send[F](logs))
```
実行すると以下のようになる。

```scala
program[WriterT[IO, Logs, *]](12345).run.unsafeRunSync()
// n = 12345
// Sending to log server: Chain(begin, end)
// res0: (cats.data.Chain[String], Unit) = (Chain(begin, end),())
```
提供されるインスタンスは `Tell` と同じ。

ソース: [listen.worksheet.sc](https://github.com/yasuabe/qiita-cats3-sample/blob/main/src/main/scala/mtl/listen.worksheet.sc)

### Local
[Local](https://typelevel.org/cats-mtl/mtl-classes/local.html) は、ローカルに「環境」を変更するメソッド `local` を `Ask` に加えたもの。例えば以下のように書ける。

```scala
type Config = Map[String, String]

def get42[F[_]: Applicative](using F: Ask[F, Config]): F[String] =
  F.ask.map(_.getOrElse("42", "none"))

def both[F[_]: Monad](using L: Local[F, Config]): F[(String, String)] =
  for
    m <- L.local(get42[F])(_.updated("42", "modified"))
    o <- get42[F]
  yield (m, o)
```
`both` は Apply 構文でも書けるが、先に `local` を使った後で、オリジナルの環境にアクセスしても変更が無いことを確認するために、あえて for comprehension で書いた[^flatMap]。実行すると以下のようになる。

```scala
both[ReaderT[IO, Config, *]].run(Map("42" -> "original")).unsafeRunSync()
//  (modified, original)
```
ソース: [local.worksheet.sc](https://github.com/yasuabe/qiita-cats3-sample/blob/main/src/main/scala/mtl/local.worksheet.sc)

### Raise
[Raise](https://typelevel.org/cats-mtl/mtl-classes/raise.html) は、`ApplicativeError#raiseError` と同等のメソッド `raise` を提供するが、`F[_]` は`Functor` であれば充分で、必ずしも `ApplicativeError` は求められない。

下の関数は[公式サンプル](https://typelevel.org/cats-mtl/mtl-classes/raise.html)とほぼ同じ `parseNumber` で、、、

```scala
def parseNumber[F[_]: Applicative](in: String)(using F: Raise[F, String]): F[Double] =
  if in.matches("-?[0-9]+") then in.toDouble.pure[F]
                            else F.raise(in) // raise 構文
```
これに `Validated[String, ?]` を指定して、エラーとなる入力を含めて実行すると以下のようになる。

```scala
val result: Validated[String, (Double, Double, Double)] = Semigroupal.tuple3(
  parseNumber[Validated[String, *]]("abc"),
  parseNumber[Validated[String, *]]("123"),
  parseNumber[Validated[String, *]]("xyz")
)
// result = Invalid(abcxyz)
```
`Validated` の他には、`EitherT`、`OptionT` のインスタンスが提供される。

ソース: [raise.worksheet.sc](https://github.com/yasuabe/qiita-cats3-sample/blob/main/src/main/scala/mtl/raise.worksheet.sc)

### Handle
`Raise` にエラー処理メソッド `handle` を追加したものが [Handle](https://typelevel.org/cats-mtl/mtl-classes/handle.html)。 上の `Raise` サンプルの `parseNumber` を使って、`handle` 有り版と無し版で比較すると下のようになる。

```scala
List("100", "abc", "200", "def") traverse parseNumber[Validated[String, *]]
// Invalid(abcdef)

List("100", "abc", "200", "def") traverse { s =>
  parseNumber[Validated[String, ?]](s).handle[String](_ => Double.NaN) // handle 構文。
}
// Valid(List(100.0, NaN, 200.0, NaN))
```
提供されるインスタンスは `Raise` と同様。

ソース: [handle.worksheet.sc](https://github.com/yasuabe/qiita-cats3-sample/blob/main/src/main/scala/mtl/handle.worksheet.sc)

----
次に軽く合成して、他の方式と比較してみる。

## 普通のモナドトランスフォーマー及び Eff との比較
[このブログ](
https://rubenpieters.github.io/monadtransformer/cats/eff/2017/01/27/monadtransformer-vs-effmonad-1.html)で、普通の Monad Transformers、MTL、Eff が比較されているが、MTL の記述がかなり古いので、今の Cats MTL にアップデートしつつ比べなおしてみたい。ここではパフォーマンスは一旦置いておいて[^performance]、書きっぷりに着目して比較する。

お題としては、Int 型の現在「状態」が１以上であればデクリメントし、０以下であればエラーとするような、StateT と EitherT の合成を考える。

#### 普通のモナドトランスフォーマー

まずこれを普通のモナドトランスフォーマーで書くと次のようになる。

```scala
type SET[E, S, R] = StateT[Either[E, *], S, R]
type EST[E, S, R] = EitherT[State[S, *], E, R]

def decrementSE: SET[String, Int, Unit] = for
  x <-               StateT.get[Either[String, *], Int]
  _ <- if x > 0 then StateT.set[Either[String, *], Int](x - 1)
                else StateT.liftF[Either[String, *], Int, Unit](Left("error"))
yield ()

def decrementES: EST[String, Int, Unit] = for
  x <-               EitherT.liftF[State[Int, *], String, Int](State.get[Int])
  _ <- if x > 0 then EitherT.liftF[State[Int, *], String, Unit](State.set(x - 1))
                else EitherT.leftT[State[Int, *], Unit]("error")
yield ()
```
モナドスタックの重ね方で２通りに書ける。というか重なり方の順序に従って書き分ける必要がある。

使う側のコードも、当然、モナドの順序によって変わってくる。

```scala
def runSE(n: Int): Either[String, (Int, Unit)] = decrementSE.run(n)
def runES(n: Int): (Int, Either[String, Unit]) = decrementES.value.run(n).value
```
実行結果は以下のようになる。

```
runSE(0) // Left(error)
runSE(1) // Right((0,()))
runES(0) // (0,Left(error))
runES(1) // (0,Right(()))
```
ソース: [monad_transformer.worksheet.sc](https://github.com/yasuabe/qiita-cats3-sample/blob/main/src/main/scala/mtl/monad_transformer.worksheet.sc)

#### Eff 版
次に Eff で書いてみる。

```scala
type _eitherString[R] = Either[String, *] |= R
type _stateInt[R]     = State[Int, *]     |= R

def decr[R: _eitherString: _stateInt]: Eff[R, Unit] = for
  x <- get
  _ <- if x > 0 then put(x - 1) else left("error")
yield ()
```
さすがにスッキリと書ける。実行も簡潔。

```scala
type ETree = Fx.fx2[State[Int, *], Either[String, *]]

def runSE(n: Int): Either[String, (Unit, Int)] = decr[ETree].runState(n).runEither.run
def runES(n: Int): (Either[String, Unit], Int) = decr[ETree].runEither.runState(n).run
```
モナドトランスフォーマー版とほぼ同じような結果になるが、Eff の state 構文の仕様で State のタプルの左右が逆になる。

```
runSE(0) // Left(error)
runSE(1) // Right(((), 0))
runES(0) // (Left(error),0)
runES(1) // (Right(()),0)
```
ソース: [eff.worksheet.sc](https://github.com/yasuabe/qiita-cats3-sample/blob/main/src/main/scala/mtl/eff.worksheet.sc)

#### MTL 版
最後に MTL。敢えて Eff 風に書いてみた。参考にした記事とは違って、今の Cats MTL では結構シンプルに書ける。

```scala
type MSI[F[_]] = Stateful[F, Int]
type FRS[F[_]] = Raise[F, String]

def decr[F[_]: Monad: MSI: FRS]: F[Unit] = for
  x <- get
  _ <- if x > 0 then set(x - 1) else raise("error")
yield ()
```
関数の定義自体は、Eff と似たような感じで書けるが、使う側のコードは Eff に比べると合成した型の指定に少し違いが出る。

```scala
  type SET[R] = StateT [Either[String, *], Int   , R]
  type EST[R] = EitherT[State [Int   , *], String, R]

  def runSE(n: Int): Either[String, (Int, Unit)] = decr[SET].run(n)
  def runES(n: Int): (Int, Either[String, Unit]) = decr[EST].value.run(n).value
```
モナドの重ね方を明示した上で、値の取り出し方もそれに合わせる必要がある。結果の値は、普通のモナドトランスフォーマー版と同じものになる。


ソース: [mtl.worksheet.sc](https://github.com/yasuabe/qiita-cats3-sample/blob/main/src/main/scala/mtl/mtl.worksheet.sc)

## 補足
### バージョン等
- Scala: 3.1.3
- cats-core: 2.8.0
- cats-effect: 3.3.12
- cats-mtl-core: 1.3.0
- eff: 6.0.1
- その他 [この辺り](https://github.com/yasuabe/qiita-cats3-sample/blob/main/build.sbt)

※ サンプルは VSCode の worksheet で書いた

### 資料
- [Cats MTL 公式](https://typelevel.org/cats-mtl/)
- [From Monad Transformers to the Eff Monad (2017/01/27)](https://rubenpieters.github.io/monadtransformer/cats/eff/2017/01/27/monadtransformer-vs-effmonad-1.html)

[^monad_action]: [Aecor](https://pavkin.ru/aecor-part-1/)の `MonadAction` や `MonadActionReject`など。
[^ior]: Ior は Either のように「どちらか」だけではなく「どちらも」を含む型
[^performance]: 合成するエフェクトの数／深さで変わってくる。
[^flatMap]: FlatMap で実行順序を保証