追記:
この記事で作成したメソッドですが、似たような機能のメソッドを
ZIOコミュニティのDiscordチャンネルにてリクエストしたところ、
バージョン1.0.0-RC20
で標準にZIO#validateWith
というメソッドを追加していただきました。
当該PR
https://github.com/zio/zio/pull/3617
コントリビューターのluis3m ありがとうございました。
リクエストした時のDiscordのコメント
https://discord.com/channels/629491597070827530/630498701860929559/711700647455490141
何がしたい?
複数の非同期タスクを実行し、複数のエラーが発生した時は全てのエラーを返すようにしたい。(CatsのValidatedのようなことがしたい)
ScalaのEitherで例えると、以下のvalidate(...)
のような関数を実装したい。かつ、Futureのような非同期処理でも可能にしたい。
def validate(...) = ??? // 作りたいもの
// 各フィールドをバリデーションして生成したい結果の型
case class User(
name: String, // 1文字以上ないといけない
age: Int // 18以上でないといけない
)
// nameのバリデーション (Leftはエラーメッセージ, Rightは成功値のEither)
def validateName(input: String): Either[String, String] =
Either.cond(input.nonEmpty, "エラー! 1文字以上必要", input)
// ageのバリデーション
def validateAge(input: Int): Either[String, Int] =
Either.cond(input >= 18, "エラー! 18以上必要", input)
// エラーメッセージのListか成功値のUser
val result: Either[List[String], User] =
validate( // ← このように使う。(かつ今回は、各バリデーション関数はFutureのような非同期タスクでも可)
validateName(""),
validateAge(10)
)((name, age) => User(name, age)) // 複数の成功値を最終的な値に変換する関数を添えられる
ZIOとは?
ZIOとは端的に言うと、FutureとEither (とReaderモナド)を組み合わせたようなZIO[-R, +E, +A]
という型をベースに、型安全でコンポーサブルな非同期処理を実現するライブラリ群です。
現時点(2020年3月上旬)ではまだ正式リリースはされておらず、RC版のみ使えます。
ScalaのFutureをはじめ、javaのjava.util.concurrent.Future
やmonixのmonix.eval.Task
など多様な非同期処理の実装から変換できます。
以降はZIOのOverviewをざっとなめたぐらいの知見を前提とします。
公式: ZIO — A type-safe, composable library for async and concurrent programming in Scala
参考: ZIO Environment 〜 Tagless Final の後継? - Qiita
今回の要件
- Cats, Scalazなどの関数型ライブラリに依存しない。ScalaまたはZIOの標準機構だけで完結させる。
- 1つ1つのバリデーション関数は、最終的に複数のエラーを積み上げる用途で使用されるのか、単にflatMap内などで使用されるのかを意識しないでいい。(戻り値はただの
ZIO
型でいい。) - 使用する側は、全てのバリデーション結果のエラー型を複数or単数で簡単に切り替えられる
要は余計な中間型を蔓延させず、面倒な型の変換処理を最小限にし、全て粒度の小さいZIO
型として統一的に合成できるようにしたいです。
- ※ 補足
- flatMap, zipなどのZIO標準の合成関数は基本的に、1つエラーが発生すれば合成値は即終了となる(short-circuitな挙動をとる)。Futureと同じ
- CatsのValidatedは、エラー型がListのような結合可能な型(Semigroup)であるかを随所で切り分けないといけなく、変換が面倒 (個人的意見)
結論
調査したところ当記事の投稿時点のZIOのバージョンRC18-1では、今回の目的を1メソッドで実現するものは見つからなかったのでヘルパー関数を実装します。今後標準として備わることを期待します。(すでにあればご教授ください)
ソースは以下にあります。
zio-validation/ZValidation.scala at master · ponsea/zio-validation
使用時のイメージ (下部のZValidation.mapParN
が今回実装したもの):
// 各フィールドをバリデーションして生成したい結果値
case class User(
name: String, // 1文字以上ないといけない
age: Int // 18以上でないといけない
)
// nameのバリデーション エラー時はエラーメッセージのString, 成功時はその入力値(ZIO[Any, String, String]のエイリアス)
def validateName(input: String): IO[String, String] =
ZIO.fromEither {
Either.cond(input.nonEmpty, "エラー! 1文字以上必要", input)
}
// ageのバリデーション
def validateAge(input: Int): IO[String, Int] =
ZIO.fromEither {
Either.cond(input >= 18, "エラー! 18以上必要", input)
}
// ZValidationの使用例。はじめは"単一"のエラー型が返る。エラーの値は一番初めのもの。全てのエラーはZIO値が持つCauseに溜まっている
result: IO[String, User] =
ZValidation.mapParN(
validateName(""),
validateAge(10)
)((name, age) => User(name, age))
// エラーの型を`::[エラー型]`に変換し、全てのエラーを取得。(`::`は空ではないList型)
resultWithAllErrors: IO[::[String], User] =
result.parallelErrors // ZIO#parallelErrorsはZIO標準のメソッド
ZValidation.mapParN
は全てのタスクを並行で実行しますが、直列で実行したい場合のためのZValidation.mapN
というメソッドもあります。
また、ZIO型にいくつか拡張メソッドを追加しています。
import com.github.ponsea.ziovalidation.syntax._
のようにして使います。
|拡張メソッド|説明|派生メソッド
|---|---|---|---|
|conzip|標準のzip
とほぼ同じ。違いは、どちらかがエラーでも中断はせず、エラーを積み上げる点。派生メソッドは標準のzipWithなどと同様| conzipWith, conzipRight, conzipLeft|
|conzipPar|標準のzipPar
とほぼ同じ。タスクを並行で実行する。違いは同上。|conzipWithPar, conzipParRight, conzipParLeft |
エラーならconcatして成功ならzipするのでconzip
です。