9
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-03-07

追記:
この記事で作成したメソッドですが、似たような機能のメソッドを
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です。

9
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?