ZIO Environment 〜 Tagless Final の後継？

最近、関数型 Scala 界隈で物議を醸していたプレゼン『The Death of Final Tagless』と、その関連ブログ記事で紹介されていた ZIO Environment について。

## ZIO の形
簡略化すると以下のような型になる。

```scala
trait ZIO[-R, +E, +A]
```
イメージ的には、Cats Effect の `IO[A]` に環境型 R とエラー型 E を追加した型、あるいは `ReaderT[EitherT[IO, E, ?], R, A]` を一つの型にしてモナドトランスフォーマーのパフォーマンス劣化を除いたような型。[bifunctor IO](https://alexn.org/blog/2018/05/06/bifunctor-io.html) の `BIO[E, A]` に R を加えた形になるので *trifunctor* というワードが出ることもある[^trifunctor]。

既存の型に似せた下のような型エイリアスも提供されている。

| エイリアス | ZIO | memo |
| ---      | ---  | --- |
|IO[+E, +A]| ZIO[Any, E, A]|いわゆる Bifunctor IO|
|Task[+A]  | ZIO[Any, Throwable, A]|Monix などと同様、エラー型が Throwable 固定のZIO|
|TaskR[-R, +A]| ZIO[R, Throwable, A]|Task に環境 R を与えたもの|
|UIO[+A]     | ZIO[Any, Nothing, A]|エラーを加味しない IO|

もちろん Monix や Cats Effect が提供するような、非同期／並行といった *functional effect* の性質も備えているが、この記事では 環境 R に着目してみる。

## サンプルコード
文字列→文字列の単純な辞書データを「環境」とするシンプルなプログラムと、これまで Tagless final や Free Monad でエンコードされてきたような複数サービスの合成を「環境」とするプログラムの二つのサンプルを書いてみる。

※ scalaz-zio のバージョンは "1.0-RC3" を使った

### シンプルなプログラム
例えば以下のような関数が書ける。

```scala
def valueOf(key: String): ZIO[Map[String, String], AppError, String] =
  ZIO.accessM { env => ZIO.fromEither(env.get(key).toRight(NoValue)) }
```
この `accessM` は、`Map[String, String]` 型の環境 `env` から `key` に対応する文字列の取得を試み、もしなければ指定した `AppError` 型の `NoValue` を結果とする。

エラー型は `Throwable` でなくともなんでも良くて、以下のような型を定義した。

```scala
sealed trait AppError
case object NoValue extends AppError
```

この関数 `valueOf` は、for comprehension 内で以下のように普通に使える。

```scala
val program: ZIO[Map[String, String], AppError, Unit] = for {
  s <- IO.effectTotal(StdIn.readLine()) // コンソールから入力
  v <- valueOf(s)                       // 所与の「環境」から値を得てみる
  _ <- IO.effectTotal(println(v))       // コンソールに出力
} yield ()
```
※ ここではコンソール入出力を自前で書いたが、ZIO で `Console` クラスが提供されている。次のサンプルではそっちを使う。

program は、`scalaz.zio.App#run` をオーバーライドして、以下のように実行できる。

```scala
object SimpleMain extends App {
  ...
  def run(args: List[String]): ZIO[Environment, Nothing, Int] =
    program.provide(Map("42" -> "Foo")).fold(_ => 1, _ => 0)
}
```
`program.provide` に「環境」`Map("42" -> "Foo")` を与えて実行。失敗なら 1、成功なら 0 を返している。

[ソース Gist](https://gist.github.com/yasuabe/19243f209c8122dc65eab5351e9d23b5)

### 振る舞いの合成と DI[^not_a_di]
次に**振る舞いの合成**を試してみる。

Tagless Final や Free Monad では「代数」などと呼ばれ、`M[F[_]]` といった形のインターフェイスや ADT の Sum Type でエンコードされていた「振る舞い」は、ZIO Environment では結果型を `ZIO` に入れただけの、昔ながらの普通のインターフェイス定義になる。

以下のサンプルでは、独自定義の `Logger`、`KVStore` と、ZIO で提供される `Console` の３つを合成する。

振る舞いの定義の構成として、Goes 氏流にアレンジされた Module パターンを踏襲してみる。Minimal Cake などのようなパターンを使うこともできるが、`Console` を含む ZIO 内部の実装でも、[元ブログ記事](http://degoes.net/articles/zio-environment)でもこの Module パターンが使われているので、せっかくなので合わせることにする。

#### Logger
Module パターンは以下のように構成する。この場合 `Logger` が**モジュール**（*HasLogger* という命名習慣もある）、`Logger.Service` が**サービス**になる。ちなみに Minimal Cake パターンでいえば、`Logger` が UsesLogger、`Logger.Serice` が Logger、`Logger.Live` が MixinLogger に、おおむね対応すると思う。

```scala
trait Logger { val logger: Logger.Service }           // モジュール
object Logger {
  trait Service { def info(line: String): UIO[Unit] } // サービス
  trait Live extends Logger {                         // モジュールの実装
    val logger: Service = new Service {               // サービスの実装
      def info(line: String): UIO[Unit] = UIO.effectTotal(println(line))
    }
  }
  ...
}
```
見ての通り `F[_]` を含んだ `Logger[F[_]]` にはならないので、Tagless Final 的な高カインド型に扱いにくさを感じる向きにも易しく見えるのではないかと思う。

#### KVStore
`KVStore` も同様。ここでは `String -> String` の Map をダミーとして使った。

```scala
trait KVStore { val kvStore: KVStore.Service }
object KVStore {
  trait Service { def valueOf(key: String): IO[AppError, String] }

  trait Live extends KVStore {
    private val dummy: Map[String, String] = Map("42" -> "Foo")

    val kvStore: Service = new Service {
      def valueOf(key: String): IO[AppError, String] =
        IO.fromEither(dummy.get(key).toRight[AppError](NoValue))
    }
  }
  ...
}
```
`valueOf` メソッドの実装では、`IO.fromEither` を使って、`key` に対応する value が無い場合の `None` を `Left(NoValue)` にしてから `ZIO` を作っている。`IO.fromEither` 以外にも、`Option`、`Try`、`Fiber`などから `ZIO` を生成する各種メソッドが提供されている。

#### ヘルパ関数
利便性のため、各インターフェイスに対して以下のようなヘルパ関数を用意しておく。

```scala
object Logger {
  ...
  def info(line: String): ZIO[UsesLogger, Nothing, Unit] =
    ZIO.accessM(_.logger info s"INFO: $line")
}
object KVStore {
  ...
  def valueOf(key: String): ZIO[KVStore, AppError, String] =
    ZIO.accessM(_.kvStore valueOf key)
}
```
※ Goes 氏のブログやプレゼンでも必須ではないとされているが、なければやはり扱いにくそう。このあたりのヘルパ関数は、Free Monad のリフト関数とも似たボイラープレートになる気がしないでもない。

#### 合成と使用
そもそも `F[_]` を使っていないので、型の合成は普通にできる。[^eitherK]

```scala
type AppType = Console with Logger with KVStore
```

上で定義したヘルパー関数をこの `AppType` と併用すると、プログラムは以下のように定義できる。

```scala
val program: ZIO[AppType, AppError, Unit] = for {
  key   <- getStrLn.orDie          // Console で文字列入力を得て
  value <- valueOf(key)            // KVStore から対応する値を得て
  _     <- info(s"$key -> $value") // Logger で出力する
} yield ()
```
implicit で悩んだり、型推論を補助するために型の明示を補ったりすることもなくシンプルに書ける。

インスタンスも型同様に合成して、以下のように実行できる。

```scala
trait Env extends Console with Logger with KVStore
object Env extends Console.Live with Logger.Live with KVStore.Live

object Main extends App {
  ...
  def run(args: List[String]): ZIO[Environment, Nothing, Int] =
    program.provide(Env).fold(
      e => { println(e); -1 },
      _ => 0
    )
}
```

テストでは、例えば以下のようにして普通にコンパイル時DIで書ける。

```scala
object MainSpec {
  object testEnv extends Env {
    val console: Console.Service[Any] = ??? //モック的なやつをあれする
    val logger:  Logger.Service       = ???
    val kvStore: KVStore.Service      = ???
  }
  // Main.program.provide(testEnv) をテストするコードいろいろ
}
```
[ソース Gist](https://gist.github.com/yasuabe/8708ab28fe0c04399796827a6131c0eb)

## 所感など
- 「**インターフェイスに対するプログラミング**」といった古き良き OOP 原則がリバイバルしてきた感。作者が挙げているメリットとして**教えやすさ**もあったが、数学っぽい関数型プログラミングの経験が少ない、オブジェクト指向寄りの Better Java な Scala プログラマにも、少しとっつきやすいのではなかろうかと思う。
- `hoge[F[_]: X: Y: Z]` といった制約や implicit のリストなどもないのでスッキリする。型推論も単純になるので、IDE との相性も良いらしく補完がいろいろ効きやすい。
- ボイラープレートの少なさも謳われていたが、上例の `Logger.info`、`KVStore.valueOf` のようなボイラープレートっぽいヘルパ関数は若干残ることになりそう。
- ただそもそも論として、Cats Effect の `IO[A]`、Scalaz や Monix の `Task[A]`、[cats-bio](https://github.com/LukaJCB/cats-bio/blob/master/src/main/scala/cats/effect/bio/BIO.scala) や Scalaz 8 の`BIO[E, A]`の進化系にはなっているかもしれないけど、(Free Monad) → Tagless Final → ZIO Environment って位置づけにはやや違和感。`IO`、`Task`、`IO[E,?]`をパラメータ化したものが Tagless Final たちが扱ってきた `F[_]` なのだから、そもそも議論の地平が違う気がする[^tracking_and_wrapper]。だからこそ [プレゼン](https://skillsmatter.com/skillscasts/13247-scala-matters)の 1h6m あたりから、`F[_]` としての `ZIO[R, E, ?]` を併用した、ベターな Tagless final の復活について語られているのではなかろうか。
- なので「環境+エラー+非同期並行を併せ持ったハイブリッドでハイパフォーマンスなモナド」くらいの感覚で扱うスタンスで、差し当たりは良いのでないかと思う。

## 参考資料
- [ZIO microsite](https://scalaz.github.io/scalaz-zio/)
- [ZIO github](https://github.com/scalaz/scalaz-zio)
- [The Death of Final Tagless](https://skillsmatter.com/skillscasts/13247-scala-matters)
- [Beautiful, Simple, Testable Functional Effects for Scala](http://degoes.net/articles/zio-environment)
- [Final Tagless seen alive](https://blog.softwaremill.com/final-tagless-seen-alive-79a8d884691d)
 
[^trifunctor]: trimap 関数があると思いきや今のところ見当たらない。代数的法則としては、R/E、R/A の *profunctor* に準じているという（詳細未読）。ちなみに Septifunctor (IO[O,M,G,_,W,T,F])という[ジョーク](https://twitter.com/public_void_grv/status/1002135220840255488) もある。
[^eitherK]: Free Monad の場合、Cats の `EitherK` や Iota の `CopK` で振る舞いを合成していたが、`F[_]` が無いからそうした特別な仕掛けが不要になる。
[^not_a_di]: DI とはオブジェクトのグラフを作るものだという観点から、ZIO Environment それ自身直接には DI に関するものではなくて、effect tracking に関するものであるという注意が [Final Tagless seen alive](https://blog.softwaremill.com/final-tagless-seen-alive-79a8d884691d) でなされている。
[^tracking_and_wrapper]: [Final Tagless seen alive](https://blog.softwaremill.com/final-tagless-seen-alive-79a8d884691d) でも、Tagless final の効用には *effect tracking* と *effect wrapper constraining* の二面があって、Goes 氏の批判が当てはまるのも、ZIO によって改善されるのも前者についてのみであろうとの指摘がある。