ZIOのプログラムについて
現状公式ドキュメントは英語でとっつきにくく、
ZIOの基本だけでもざっと動かして学べないかと思い、ドキュメントの一部の内容を参考にしながら、1つのプログラムに基本的な要素を組み込みまとめてみました。
このページの構成
ZIOの基本事項について
↓
ZIOのプログラム例
という流れで、ざっとZIOについて書いたのち
実際に動かせるプログラム例を示しています。
ZIOの基礎知識
まずはコード例を示す前に、ざっと基本的な知識についてまとめてみました。
ZIOプログラムの雛形
ZIOAppDefaultを継承することでZIOを使ったアプリケーションを簡単に作成できます。
runメソッドがmain関数のような役割を果たし、ZIOのエフェクトを実行します。
(エフェクトについては後述)
import zio._
import zio.Console._
object MyApp extends ZIOAppDefault {
def run = myAppLogic
val myAppLogic =
for {
_ <- printLine("Hello! What is your name?")
name <- readLine
_ <- printLine(s"Hello, ${name}, welcome to ZIO!")
} yield ()
}
エフェクト
ZIOでは処理のまとまりをエフェクトとして表現し、エフェクトは必ずZIO型を返すように作ります。
ZIO型を返すエフェクトはZIOが用意したメソッドを使って値や処理をラップすることで得られます。
(エフェクトを作成した段階では実行はされず、runメソッドが呼ばれた段階で実行される)
例)
//成功値(Int型)を返すエフェクトを作成
val s1: ZIO[Any, Nothing, Int] = ZIO.succeed(42)
//失敗値(String型)を返すエフェクトを作成
val f1: ZIO[Any, String, Nothing] = ZIO.fail("Uh oh!")
ZIO型
ZIO型はZIO[R, E, A]
という型で表現され、
R, E, A
の3つの型パラメータを持ちます。
R: 必要な環境の型
Any: エフェクトに必要な環境がないことを示す
E: エラーの型
Nothing: エラーが発生しないことを示す
A: 成功時の値の型
例を見た方が分かりやすいと思われるので例を挙げます。
ZIO[Any, Nothing, String]
任意の環境を必要とせず、エラーが発生しない(Nothing)、成功時にStringを返すエフェクトを表します。
ZIO[Any, IOException, Byte]
任意の環境を必要とせず、IOException型のエラーが発生する可能性があり、成功時にByteを返すエフェクトを表します。
ZIO[Connection, SQLException, ResultSet]
Connection型の環境を必要とし、SQLException型のエラーが発生する可能性があり、成功時にResultSetを返すエフェクトを表します。
ZIO[HttpRequest, HttpFailure, HttpSuccess]
HttpRequest型の環境を必要とし、HttpFailure型のエラーが発生する可能性があり、成功時にHttpSuccessを返すエフェクトを表します。
ZIOのプログラム
ZIOでは主に
- エフェクトを作成する
- エフェクトに対して操作(変換など)を行う
- エフェクトを実行する
という流れでプログラムを作成します。
今回はZIO公式のOverview
ページの内容を参考に
- エフェクトの作成
- エフェクトの変換
- エフェクトの実行
- エラー処理
- 並列実行
- リソース管理
を使ったプログラムを書いてみました。
プログラム例の概要
名前と年齢を標準入力から受け取り、条件チェック(バリデーション)をしてから挨拶を表示し、内容をファイルに書き込むプログラム
- 名前が空文字の場合や年齢が数字でない場合はエラーを返す
- 複数の処理(表示とファイル書き込み)を並列で実行
- ファイルリソースは安全に開閉管理
- エラーハンドリングも組み込み
プログラム例
package batch
import zio._
import zio.Console._
import java.io._
// ユーザー情報モデル
case class User(name: String, age: Int)
object ZIOBasicExample extends ZIOAppDefault {
//=========エフェクトの定義=========
// 名前を検証し、空文字なら失敗(String型のエラー)を返すエフェクト
def validateName(name: String): ZIO[Any, String, String] =
if (name.trim.isEmpty) ZIO.fail("名前が空です") else ZIO.succeed(name)
// 年齢文字列をIntに変換し失敗したらString型のエラーを返すエフェクト(エラー処理)
def validateAge(ageStr: String): ZIO[Any, String, Int] =
ZIO.attempt(ageStr.toInt).mapError(_ => "年齢は数字で入力してください")
// ファイルを安全に開いたり閉じたりするのにZIO.acquireReleaseWithを使う(リソース管理)
def saveToFile(user: User): ZIO[Any, Throwable, Unit] =
ZIO.acquireReleaseWith(
ZIO.attempt(new PrintWriter(new FileWriter("output.txt", true))) // リソース確保
)(writer => ZIO.succeed(writer.close())) { (writer: PrintWriter) =>
ZIO.attempt(writer.println(s"Name: ${user.name}, Age: ${user.age}")) // リソース使用
}
// ユーザー入力の一連処理。printLineやreadLineは失敗型がThrowableなのでmapErrorでStringに変換し、
// for内で失敗型(Eの値)を統一している
def userInput: ZIO[Any, String, User] =
for {
_ <- printLine("> あなたの名前を入力してください:").mapError(_.toString)
name <- readLine.mapError(_ => "入力時のエラーです").flatMap(validateName)
_ <- printLine("> あなたの年齢を入力してください:").mapError(_.toString)
age <- readLine.mapError(_ => "入力時のエラーです").flatMap(validateAge)
user = User(name, age)
_ <- printLine(s"> こんにちは、${user.name}さん(${user.age}歳)").mapError(_.toString)
} yield user
// ログ保存のエフェクト。saveToFileの失敗をキャッチしてコンソールに表示
def logging(user: User): ZIO[Any, Throwable, Unit] =
saveToFile(user).catchAll(e => printLine(s"ログ保存中のエラー: ${e.getMessage}"))
//=========エフェクトの実行=========
def run =
(for {
// ユーザー入力
user <- userInput
// 挨拶の表示とログ保存をそれぞれFiberでforkし、並列で実行
greetingFiber <- printLine(s"こんにちは、${user.name}さん(${user.age}歳)、ZIOへようこそ!").fork
logFiber <- logging(user).fork
// Fiberの完了を待つ
_ <- greetingFiber.join
_ <- logFiber.join
} yield ()).catchAll(err => printLine(s"エラー: $err")) // 全体のエラーをキャッチして表示
}
実行例
正常な入力:
> あなたの名前を入力してください:
たろう
> あなたの年齢を入力してください:
29
> こんにちは、たろうさん(29歳)
こんにちは、たろうさん(29歳)、ZIOへようこそ!
エラー時の実行例(名前入力が空):
> あなたの名前を入力してください:
エラー: 名前が空です
プロセスは終了コード 0 で終了しました
エラー時の実行例(年齢が数字ではない):
> あなたの名前を入力してください:
じろう
> あなたの年齢を入力してください:
さぶろう
エラー: 年齢は数字で入力してください
プロセスは終了コード 0 で終了しました
解説
主にエフェクトをZIOの用意した関数で定義したのち、
runメソッドにエフェクトを渡すというのが大まかな流れになります。
このプログラムでやっていること
エフェクトの作成
ZIO.succeed
、ZIO.fail
、ZIO.attempt
を使って成功や失敗のエフェクトを定義
エフェクトの変換
mapError
やmap
でエラー型や成功値を変換して型安全に操作
エフェクトの実行
ZIOAppDefault
のrun
メソッドでエフェクトを実行し、アプリ全体の入り口にする
エラー処理
catchAll
で失敗時の処理(ユーザーへのメッセージ表示など)を安全に実装
リソース管理
ZIO.acquireReleaseWith
を用いてファイルなどの外部リソースを安全に確保・解放
並行実行(Fiber)
fork
メソッドでFiberを起動し、処理を非同期かつ並行的に実行します。
プログラムでは printLine(標準出力への表示)と logging(ファイルへのログ書き込み)のエフェクトを別々のFiberで起動し、ほぼ同時に動作させています(並列実行)。
join
でFiberの完了を待ち、結果を取得