[Scala] ZIOで非同期バリデーション・複数のエラーハンドリング (Cats無し)

追記:
この記事で作成したメソッドですが、似たような機能のメソッドを
[ZIOコミュニティのDiscordチャンネル](https://discord.com/invite/2ccFBr4)にてリクエストしたところ、
バージョン`1.0.0-RC20` で標準に`ZIO#validateWith`というメソッドを追加していただきました。

当該PR
https://github.com/zio/zio/pull/3617

コントリビューターの[luis3m](https://github.com/luis3m) ありがとうございました。

リクエストした時のDiscordのコメント
https://discord.com/channels/629491597070827530/630498701860929559/711700647455490141

# 何がしたい？
複数の非同期タスクを実行し、複数のエラーが発生した時は全てのエラーを返すようにしたい。(CatsのValidatedのようなことがしたい)
ScalaのEitherで例えると、以下の`validate(...)`のような関数を実装したい。かつ、Futureのような非同期処理でも可能にしたい。

```Scala
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](https://zio.dev/)とは端的に言うと、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](https://zio.dev)
参考: [ZIO Environment 〜 Tagless Final の後継？ - Qiita](https://qiita.com/yasuabe2613/items/0146887d115df00ef56f)


# 今回の要件
- 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](https://github.com/ponsea/zio-validation/blob/master/src/main/scala/com/github/ponsea/ziovalidation/ZValidation.scala)

使用時のイメージ (下部の`ZValidation.mapParN`が今回実装したもの):

```Scala

// 各フィールドをバリデーションして生成したい結果値
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`です。