ZIO 使ってみる
先日 ZIO に関する発表を同僚が社内カンファレンスでしていておもしろそうだったので、ちょっと触ってみる。適当なWebアプリケーションを作るところを目標にする。
ZIO はこの記事がわかりやすかった。あるいは、公式ドキュメントもよい。
やる前のイメージとしては、cats の IO や Future では、エラー型は Nothing として潰されてしまうので、どのエラーが来ているかを型情報で伝えるのが結構難しい。まずその問題を解決しているもの、という勝手なイメージ。加えて、環境情報 R
を突っ込めて、R
の型レベルでの整合性の検証ができそうな感じもしている。
Scala 3 でどこまでいけるのか試したい!
- ZIO: 大丈夫そう
- http4s: ???
- doobie: ???
- IntelliJ: △ (nightly build を求められる)
簡単な Hello, world
package com.github.yuk1ty
import zio.*
import zio.console.{Console, putStrLn}
import java.io.IOException
object Main extends zio.App {
override def run(args: List[String]) = program.exitCode
val program: ZIO[Console, IOException, Unit] = for {
value <- ZIO.succeed(42)
_ <- putStrLn(s"value: $value")
} yield ()
}
ただし、IDEA の指示通りに型付けした下記はコンパイルエラーになる。
package com.github.yuk1ty
import zio.*
import zio.console.{Console, putStrLn}
import java.io.IOException
object Main extends zio.App {
override def run(args: List[String]) = program.exitCode
val program: ZIO[Console, IOException, Unit] = for {
value <- ZIO.succeed(42)
_ <- putStrLn(s"value: $value")
} yield ()
}
Found: zio.URIO[zio.console.Console, zio.ExitCode]
Required: zio.URIO[Any, zio.ExitCode]
override def run(args: List[String]): URIO[Any, ExitCode] = program.exitCode
Any にしているのがまずいだけの可能性があるので、下記のように型合わせをしてみた。すると通った。実運用上はこれで正しいんだろうか。
package com.github.yuk1ty
import zio.*
import zio.console.{Console, putStrLn}
import java.io.IOException
object Main extends zio.App {
override def run(args: List[String]): URIO[Console, ExitCode] = program.exitCode
val program: ZIO[Console, IOException, Unit] = for {
value <- ZIO.succeed(42)
_ <- putStrLn(s"value: $value")
} yield ()
}
DI の例はあるんだけど、複数個サービスを注入したいみたいな例のときにどうすればいいかが書いてなかったので試した。ZIO.environment
で環境をセットアップすることができる。
Scala 3 とかだと Union Type があるのでそれが適用されているんだと思われる。for-yield 内で適用に結合しても、きちんとその分正しく結合してくれるようみたい。なかなか便利そうな気がいしている。
// 中略
class ServiceA
class ServiceB
val dicheck: ZIO[ServiceB with ServiceA, Nothing, Unit] = for {
serviceA <- ZIO.environment[ServiceA]
serviceB <- ZIO.environment[ServiceB]
} yield ()
DI しつつ、そのモジュールのメソッドを使用したい場合は accessM
ないしは access
という関数を使用できる。
// 中略
class ServiceA {
def print(): Task[Unit] = ZIO.effect(println("serviceA"))
}
class ServiceB {
def print(): Task[Unit] = ZIO.effect(println("serviceB"))
}
val dicheck: ZIO[ServiceB with ServiceA, Throwable, Unit] = for {
_ <- ZIO.accessM[ServiceA](_.print())
_ <- ZIO.accessM[ServiceB](_.print())
} yield ()
レイヤードアーキテクチャのようにレイヤーになるアプリケーションでどうやったら ZIO の環境サイドを上手に扱えるかを考えている。
これはいい例だけど、一層しかないので複数になった場合にどうなるのかは考える必要がある。
ZLayer というものを使って DI できるみたい。Service というのは ZIO 特有のデザインパターンにあたるのかな。あとで読む。
ZLayer はちょっとやりすぎ、みたいなケースには普通にコンストラクタインジェクションを使うといいよ、として例が示されている。たしかに。Task
でもいいし、でも IO
のほうが好きだからそっちを使おうかな。
Webアプリ自体は作ってみたんだけど、doobie との接続がつらすぎて断念してしまった。また時間見つけてやりたい。