Precena Tech Book
コーポレートサイト採用サイト
  • はじめに
  • ソフトウェア開発
    • 開発環境構築
      • Homebrew
        • Homebrew用語の意味
      • ngrok
        • ngrokの導入
        • ngrokのアップグレード(v2 to v3)
      • Slack
        • Slackの/remind コマンドの形式
        • 対面での相談を気軽にするためのSlack設定
      • AWS CLI
      • Ruby
      • Scala
      • Prettier
      • zsh
        • zsh-completion
      • Mac
        • M1 Macでの開発環境構築(rosetta 無し)
    • バックエンド
      • OpenAPI
        • OpenAPI 定義ファイル分割のすゝめ
      • Ruby on Rails
        • ActiveRecordのfind_or_initialize_byメソッドにブロックを渡したときの挙動
        • Railsのアプリケーションサーバーのプロセス数とスレッド数の設定方法
        • Railsを6.1系から7.0系へアップグレードした時に調査したこと
        • schema.rbで差分が発生する事例とその復旧について
        • tmux + overmind を利用して、複数システムを1コマンドで起動できるよう設定する
        • Rails Migrationチートシート
        • GithubのプライベートリポジトリをGemfileで参照する方法
        • ActiveSupportのto_jsonメソッドの注意点
        • 危険なJSON出力を禁止するRuboCopカスタムルールの作成方法
      • Scala
        • Validated を直列に処理したい
      • DB
        • PostgreSQLにおける、削除行に対するロック獲得時の挙動
    • フロントエンド
      • React
        • Storybookを利用したビジュアルリグレッションテスト
  • インフラ開発
    • AWS
      • IAM
        • スイッチロールの設定手順
        • AWS CLIでのスイッチロールの設定手順
        • AWS Vaultを使ったスイッチロール設定手順
        • Github ActionsでIAMロールを利用してAWSリソースを操作する
      • ECS
      • SES
        • AWS SESメールボックスシミュレーターにて、カスタムヘッダや添付ファイル付きのテストEメールを送信する
      • CloudWatch
        • Amazon SNS + Slack Workflowを使って、CloudWatch Alarmの通知をSlackチャンネルへ投稿する
      • Lambda
        • lambrollでAWS Lambda関数をデプロイしたときのTips
    • Heroku
      • HerokuのStackの設定
      • Heroku Postgresの運用でよく使うコマンド集
  • セキュリティ
    • Web
      • Same Origin PolicyとCORS
      • 脆弱性診断 2社同時依頼実施記録
  • Mail
    • SPF、DKIM、DMARCを使用した迷惑メール対策
  • データ分析
    • データ分析プロセス
  • SaaS
    • Zendesk
      • 問い合わせフォームの項目をサービスごとに出し分け、各サービス担当者に自動で振り分けてメールで通知する
  • イベント
    • RubyKaigi
      • RubyKaigi 2023 に現地参加しました
    • EMConf
      • EMConfJP2025_参加レポート
  • やってみた
    • IoT
      • Raspberry Pi + PaSoRi + Python で、勤怠打刻マシンを作ってみた
  • Precena Tech Book 管理
    • コンテンツ執筆時のルール
  • 関連リンク
    • プレセナエンジニア公式Twitter
GitBook提供
このページ内
  • 概要
  • Validated の値を使って、別の Validated を作りたい
  • 解法:andThen メソッドを使う
  • 複数の Validated の値を使って、別の Validated を作りたい
  • 解法:一度 Either にする
  • ダメな例
  • 利用したコード

役に立ちましたか?

PDFとしてエクスポート
  1. ソフトウェア開発
  2. バックエンド
  3. Scala

Validated を直列に処理したい

前へScala次へDB

最終更新 2 年前

役に立ちましたか?

概要

cats に含まれる Validated は、複数の入力値のバリデーションを一つにまとめて返すことができる大変便利な型です。

業務において、入力値のバリデーションを行った値をさらに別の入力値として使いたいというケースがままあり、プロジェクトの新規参入者が悩むことがあるため備忘録として記します。

Validated の値を使って、別の Validated を作りたい

以下のようなコードがあります。

case class Error(message: String)

case class Age(age: Int)
object Age {
  def validate(age: Int): Either[Error, Age] =
    Either.cond(age < 1000, Age(age), Error("そんなに生きられません"))
}

case class Child(age: Age)
object Child {
  def validate(age: Age): Either[Error, Child] = // チェック済みの Age を使いたい
    Either.cond(age.age < 18, Child(age), Error("もう大人です"))
}

バリデーション済みの Age を使って Child のバリデーションを行いたい。

次に記すように flatMap を使って書けないものでしょうか。

import cats.syntax.all._

val uncompilableValidatedChild = for {
  validatedAge <- Age.validate(15).toValidated // flatMap がないのでコンパイルエラー
  validatedChild <- Child.validate(validatedAge).toValidated
} yield validatedChild

しかし Validated には flatMap がないのでコンパイルエラーになってしまいます。

その理由は Validatedは Monad を実装できず Applicative であるからで、公式ドキュメントに詳しくかかれていました。

解法:andThen メソッドを使う

andThen メソッドを使うことで Validated な値を直列に処理することができます。

val validatedAge = Age.validate(15).toValidatedNec
val validatedChild = validatedAge.andThen(age => Child.validate(age).toValidatedNec)

当然ですが、直列になるため Age でバリデーションエラーになった場合には Child のバリデーションは評価されません。 Age も Child も満たさない値 2000 で実行してみましょう。

console
scala> val validatedChild = Age.validate(2000).toValidatedNec.andThen(age => Child.validate(age).toValidatedNec)
val validatedChild: cats.data.Validated[cats.data.NonEmptyChain[example.Error],example.Child] = Invalid(Chain(Error(そんなに生きられません)))

Child のバリデーションエラー Error("もう大人です") は追加されていないことが確認できますね。

複数の Validated の値を使って、別の Validated を作りたい

一つ前の例は入力が1つでしたが、今度は入力が2つ以上のケースです。

case class Age(age: Int)
object Age {
  def validate(age: Int): Either[Error, Age] =
    Either.cond(age < 1000, Age(age), Error("そんなに生きられません"))
}

case class Job(name: String)
object Job {
  def validate(name: String): Either[Error, Job] =
    Either.cond(name.nonEmpty, Job(name), Error("空文字はダメです"))
}

case class Adult(age: Age, job: Job)
object Adult {
  def validate(age: Age, job: Job): Either[Error, Adult] = // 入力が2つ
    Either.cond(18 <= age.age, Adult(age, job), Error("まだ子供です"))
}

普通に書くとネストしてしまいますが、先ほどと同様に flatMap がないためこのように書くことはできません。

val validatedAge = Age.validate(2000).toValidatedNec
val validatedJob = Job.validate("").toValidatedNec

val nestedValidatedAdult: ValidatedNec[Error, ValidatedNec[Error, Adult]] =
  (validatedAge, validatedJob).mapN((age, job) => Adult.validate(age, job).toValidatedNec)
val validatedAdult: ValidatedNec[Error, Adult] = nestedValidatedAdult.flatten // flatMap がないのでコンパイルエラー

解法:一度 Either にする

withEither メソッドを使うと、一旦 Either に変換してから Validated に戻せるため、 flatMap を使ってネストを解消できます。

val validatedAdult = (validatedAge, validatedJob)
  .mapN((age, job) => Adult.validate(age, job).toValidatedNec.toEither)
  .withEither(_.flatten)

または

val validatedAdult = (validatedAge, validatedJob)
  .mapN((age, job) => Adult.validate(age, job).toValidatedNec)
  .withEither(_.flatMap(_.toEither))

実行してみましょう。両方のバリデーションエラーが合成できていることを確認できます。

console
scala> (validatedAge, validatedJob).mapN((age, job) => Adult.validate(age, job).toValidatedNec.toEither).withEither(_.flatten)
val res2: cats.data.Validated[cats.data.NonEmptyChain[example.Error],example.Adult] = Invalid(Chain(Error(そんなに生きられません), Error(空文字はダメです)))

ダメな例

andThen を2回使うことでも一応可能ですが、本来並列にできるはずの Age と Job のバリデーションも直列になってしまい、 Age でバリデーションエラーになると Job が評価されなくなってしまいます。

val validatedAdult = validatedAge
  .andThen(age => validatedJob
    .andThen(job => Adult.validate(age, job).toValidatedNec))

実行すると、次のように Job のバリデーションエラーは表示されず Age のバリデーションエラーだけが表示されてしまいます。

console
scala> validatedAge.andThen(age => validatedJob.andThen(job => Adult.validate(age, job).toValidatedNec))
val res1: cats.data.Validated[cats.data.NonEmptyChain[example.Error],example.Adult] = Invalid(Chain(Error(そんなに生きられません)))

利用したコード

コード
/build.sbt
import Dependencies._

ThisBuild / scalaVersion     := "2.13.10"
ThisBuild / version          := "0.1.0-SNAPSHOT"
ThisBuild / organization     := "com.example"
ThisBuild / organizationName := "example"

lazy val root = (project in file("."))
  .settings(
    name := "validated_sandbox",
    libraryDependencies ++= Seq(
      "org.typelevel"       %% "cats-core"                % "2.9.0",
      munit % Test
    )
  )
/src/main/scala/example/Main.scala
package example

import cats.data.ValidatedNec
import cats.implicits._

object ValidateTest extends App {
  /*
   * 通常パターン
   */
  val validatedAge = Age.validate(2000).toValidatedNec
  val validatedJob = Job.validate("").toValidatedNec

  val param: ValidatedNec[Error, InputParam] = (validatedAge, validatedJob).mapN(InputParam.apply)

  /*
   * ケース1:Validated の値を使って、別の Validated を作りたい場合
   */

  // ダメな例:flatMap がないのでこうは書けない
  val uncompilableValidatedChild = for {
    validatedAge <- Age.validate(15).toValidated // flatMap がないのでコンパイルエラー
    validatedChild <- Child.validate(validatedAge).toValidated
  } yield validatedChild

  // 解法:andThen を使う
  val validatedChild = validatedAge.andThen(age => Child.validate(age).toValidatedNec)

  /*
   * ケース2:複数の Validated から Validated を作りたい
   */

  // ダメな例: 入れ子になってしまうが、 flatMap 使えないしどうすれば...
  val nestedValidatedAdult = // ValidatedNec[Error, ValidatedNec[Error, Adult]]
    (validatedAge, validatedJob).mapN((age, job) => Adult.validate(age, job).toValidatedNec)
  val validatedAdult = nestedValidatedAdult.flatetn // コンパイルエラー

  // 解法: 一旦 Either にして flatMap してから Validated にもどす
  val validatedAdult1 = (validatedAge, validatedJob)
    .mapN((age, job) => Adult.validate(age, job).toValidatedNec.toEither) // ValidatedNec[Error, Either[NonEmptyChain[Error], Adult]]
    .withEither(_.flatten)
  val validatedAdult2 = (validatedAge, validatedJob)
    .mapN((age, job) => Adult.validate(age, job).toValidatedNec) // ValidatedNec[Error, ValidatedNec[Error, Adult]]
    .withEither(_.flatMap(_.toEither))

  // よくない解法: 並列に処理できるところも直列にしてしまう
  val badValidatedAdult = validatedAge
    .andThen(age => validatedJob
      .andThen(job => Adult.validate(age, job).toValidatedNec))
}

case class Error(message: String)

case class Age(age: Int)

object Age {
  def validate(age: Int): Either[Error, Age] =
    Either.cond(age < 1000, Age(age), Error("そんなに生きられません"))
}

case class Child(age: Age)

object Child {
  def validate(age: Age): Either[Error, Child] =
    Either.cond(age.age < 18, Child(age), Error("もう大人です"))
}

case class Job(name: String)

object Job {
  def validate(name: String): Either[Error, Job] =
    Either.cond(name.nonEmpty, Job(name), Error("空文字はダメです"))
}

case class Adult(age: Age, job: Job)

object Adult {
  def validate(age: Age, job: Job): Either[Error, Adult] =
    Either.cond(18 <= age.age, Adult(age, job), Error("まだ子供です"))
}

case class InputParam(age: Age, job: Job)

本サイトの更新情報は、Twitterので発信しています。ご確認ください。

https://typelevel.org/cats/datatypes/validated.html#of-flatmaps-and-eithers
株式会社プレセナ・ストラテジック・パートナーズエンジニア公式アカウント
Validated
Logo