2026/6/30 そろそろ本気でZIOやっておくか
これは @susisu が社内でeffect-tsでやっていたものをZIOで焼き直したものです。前半のエフェクトに関する説明の大多数は@susisuによるものを改変したものです。 想定聴衆
Scalaの基礎を知っている
型に興味がある
計算エフェクト
プログラムを評価すると値が得られる
1 + 2 => 3
計算以外のことも起こる
println(42) => Unit
42と画面に出力される
これは特定の言語に限った話ではない
TypeScript
alert("Yo!") => undefined
Yo!とダイアログが表示される
この「計算の結果得られる純粋な値の部分以外」を副作用という
副作用自体が目的のこともある
ダイアログを出す
音を鳴らす
電源を切る
副作用をうまく型で表現したものをエフェクトと呼ぶ
型を持つ現在の主要な言語であっても、エフェクトが型に乗ってることはすくない
getSHA256(str: String): Array[Byte] という関数があるとき、ロギングや例外スローなどを行っていても、それが読み取れない!
型だけを見れば何が起こるか分かる、という前提が崩れる
われわれは型システムを持っているので「何を返すか」については驚かずに済むようになったが、「何が起こるか」については型から読み取れないので驚きが増えてしまう
「何が起こるか」という関心事はも型に載せよう
例外を投げるかわりに、OptionとかEither、Tryにしよう
型に情報が増える
ZIO
便利なデータ型ZIO[R, E, A]を使ってプログラムをモデリングする
A: 結果
E: 起こりうるエラー
R: 実行に必要な依存関係
型安全・非同期処理・並行処理を中心に据えたエフェクトライブラリ
code:sh
scala-cli repl --dep "dev.zio::zio:2.1.26"
記法
code:scala
import zio._
// 専用のエラー型を定義する
final case class DivideByZero(dividend: Int) extends Exception(s"$dividend を 0 で除算しようとした")
if (b == 0) ZIO.fail(DivideByZero(a))
else ZIO.succeed(a / b)
ZIO型には便利なエイリアスが色々用意されている
IO[E, A]: 依存がないZIO型
ZIO.fail: 計算を失敗させる
ZIO.succeed: 計算を成功させる
ほかにも、ZIO.fromOptionなどがあり、既存の型からZIO型へと昇格させられる
「計算」という概念が明示的に扱われているのがわかるね
実行してもZIO型の値が返るだけで、内部の処理は実行されない
code:scala
scala> divide(42,0)
trace = "<empty>.rs$line$2.divide(rs$line$2:7)",
onState = zio.ZIO$$$Lambda/0x00000000076ff280@5a0fc6cd
)
ZIOは非同期処理が前提のライブラリなので、ZIO型の値を作っても実行されない
組込みのFuture型との違い。Futureは作成したそばから実行されてしまうので扱いが悪かった
この段階ではエフェクトが実行されない。ZIOランタイムがZIO[R, E, A]をA型へと計算する
Runtimeはエフェクトの実行機能を提供するとともに、エフェクトが必要とする環境とスレッドプールを統合したシステムです。(訳は筆者による)
スレッドプールとあるように、非同期処理もやってくれる
明示的にZIOランタイムを利用するにはUnsafe.unsafeを利用する
実際の計算が実行され、型から補足できなくなるのでunsafe
code:scala
val runtime = Runtime.default
Unsafe.unsafe { implicit unsafe =>
runtime.unsafe.run(divide(42, 0)) // ランタイムを使って計算を実行する
.getOrThrowFiberFailure() // 計算結果を取り出す
}
もちろん毎回こういうことをするのは大変なので、アプリケーションのエントリポイントとしてAppと統合されたバージョンが用意されている
code:scala
import zio._
object MyApp extends ZIOAppDefault {
def run = Console.printLine("Yo!") // ここにZIO型を置く
}
普段はランタイムは隠蔽されているので、単にZIOを利用する上では普段は気にしなくてよい
ZIO型を作る
値から作る
ZIO.succeed: 渡した値を返す、成功するだけのZIO型の値を作る。
code:scala
ZIO.succeed(42) // ZIO(42)
ZIO.fail: succeed同様に渡した値を返すが、失敗を表現する。
code:scala
ZIO.fail("boom!!!")
ZIO.fail(new Exception("no---"))
コードから作る
ZIO.attempt: その場で同期的な処理を行ってその値をZIO型で包む。例外も捕捉する
code:scala
ZIO.attempt {
println("woohoo!!")
42
}
// => 即座に"woohoo!!"と表示され、ZIO(42)が返る
複数のZIO型を組み合わせる
ZIO型そのものは「まだ実行していない、エフェクトをともなった計算」のモデル化であり、実行されていないのだから値を取り出せない
そこで、「そのZIO型を計算した結果を取り出してこういった処理を行った結果のZIO型」というふうな合成をすることで、型をZIO型に閉じ続ける必要がある
divideTwice(n: Int, d: Int, e: Int): IO[DivideByZero, Int]を作りたい
divide(n, d)の結果を取り出して、それをまたdivide(???, e)として利用したい
つまり、逐次実行する代わりに合成をしないといけない
ところで・・・
モナドはエフェクトをともなう計算(プログラム)のモデルだったんだよ!!
👬👬な、なんだってー
モナドなのでflatMapしてくっつけることができる
F[A].flatMap(f: A => F[B]): F[B]なのでこの目的に沿っている
code:scala
divide(n, d).flatMap { nd => divide(nd, e) }
前述の方法でdivideTwice(120, 0, 2)を評価するとちゃんとエラーになる
途中で計算を中断するというエフェクト(例外)がちゃんと機能した
調子に乗ってdivideThriceを作る
code:scala
divide(n, d).flatMap { nd => divide(nd, e).flatMap {nde => divide(nde, f)} }
オエー
ScalaにはこうしたflatMapの連鎖をフラットに記述するための構文forがある
code:scala
for {
nd <- divide(n, d)
nde <- divide(nd, e)
ndef <- divide(nde, f)
} yield ndef
というか、forは元々これをやるためのもの
このようにしてScalaの記法をうまく使ってflatMapを隠蔽することで、うまくZIO型を合成できるようになった
エラーを発生させよう
依存性の注入
さて、ZIO[R, E, A]型のうちEとAとについては分かった
Rはどういうふうに使うの
依存性の定義
ZIO.service[A]することで、依存性を要求してそれを取り出せる
依存性の単位は慣例的にサービスと呼ばれている
リポジトリとかロガーとかがサービスになる
Clock・Random・Console・System といった標準的なものはZIOが提供している
こういうサービスを用意する
code:scala
import zio._
final class A {
def foo: UIOString = ZIO.succeed("Hello!") }
final class B {
def bar: UIOInt = ZIO.succeed(42) }
code:scala
for {
a <- ZIO.serviceA // RがAになる r <- a.foo
} yield r
shorthand
code:scala
object A {
}
object B {
}
この型は「環境からAというサービスを取得しなければならない」ということを型レベルで宣言している
複数のサービスに依存すると、型はintersection typesを利用して両方の依存を要求するようになる
code:scala
// Sequential Composition Example
for {
a <- A.foo
b <- B.bar
} yield (a, b)
依存性を注入する
あるZIO型を評価して実行するには、ZIO[R, E, A]のうちRがすべて解決されていなければならない
ZIOランタイムが実行できるのはR = Anyのものだけ
解決: RがAnyになるまで依存性を解決して消去していくという過程が必要
依存性が多ければ多いほどA & B & C...のように狭まっている
逆にすべて解決されている状態ではAnyとなり最大限に広くなっている
しかし手で組み立てるのはまあまあ大変
ZLayerという機能があり、依存性の組み立て過程をコンポーネント化できる
依存性の組み立ての段階で失敗しうる、といった場合に有効
複数用意しておいてこの単位で差し替えられるのでモックとかモード切り替えを実現するのに便利
便利な道具たち