安全で柔軟なAlways-Valid Domain Modelの作り方
はじめに
昨今ではますます Immutable な Domain Model の有用性が認知されているように思います。
また、Domain Model が持つ制約(不変条件)を満たさないインスタンスを作れないようにする事の重要性も広く知られてきており、Always-Valid Domain Model といった名称もついているようです。
ここでは、制約を満たしたDomain Model (Always-Valid Domain Model) しか作成できないようにするファクトリメソッドについて、より柔軟で合成可能性の高いファクトリメソッドを定義する方法を紹介します。
この記事で利用する言語とライブラリのバージョンは以下の通りです。
- Scala
3.1.0
- cats-core
2.7.0
最初のエラーで全体を失敗とするファクトリメソッド
まずは単純に Either
を使ったファクトリメソッドを見てみましょう。
case class Name private (value: String)
object Name {
def ofEither(value: String): Either[String, Name] = {
if (value.nonEmpty) Right(Name(value))
else Left("Name could not be blank.")
}
}
String
から Name
を生成するファクトリメソッドを定義しました。
ここでは Name
は空文字列ではないという制約を満たす必要があります。ofEither
ファクトリメソッドではそれを表現するために戻り値を Either[String, Name]
にしています[1]。
制約を満たさない場合は Left[String]
でエラーメッセージを返し、問題なく値が生成できた場合は Right[Name]
で Name
インスタンスを返します。これで必ず制約を守ったインスタンスのみ生成できることが保証できます。
こういった条件を満たせば Right
を返し満たさなければ Left
を返す、というのは非常に頻出するので Either.cond
という便利メソッドが提供されています。 これを使って書き直してみると以下のようになります。
case class Name private (value: String)
object Name {
def ofEither(value: String): Either[String, Name] = {
Either.cond(value.nonEmpty, Name(value), "Name could not be blank.")
}
}
同様に別の制約を持つDomain Modelと、それらを保持するDomain Modelも定義してみます。
case class Age private (value: Int)
object Age {
def ofEither(value: Int): Either[String, Age] = {
Either.cond(value >= 0, Age(value), "Age could not be negative.")
}
}
case class Person(name: Name, age: Age)
object Person {
def ofEither(name: String, age: Int): Either[String, Person] = {
for {
name <- Name.ofEither(name)
age <- Age.ofEither(age)
} yield Person(name, age)
}
}
Age
は Name
と似たようなクラスで、0以上という制約を満たす必要があります。
Person
は Name
と Age
を保持するクラスで、それぞれのファクトリメソッド Name.ofEither
と Age.ofEither
を使ってプリミティブな値から Person
インスタンスを生成するファクトリメソッドが定義されています。
実際にREPLで使ってみましょう。
scala> Person.ofEither("", -1)
val res0: Either[String, Person] = Left(Name could not be blank.)
scala> Person.ofEither("gakuzzzz", 17)
val res1: Either[String, Person] = Right(Person(Name(gakuzzzz),Age(17)))
scala>
Person.ofEither("", -1)
の様に不正な値を渡すと意図した通り Left
でエラーメッセージが返され、Person.ofEither("gakuzzzz", 17)
の様に適切な値を渡すと Right
で Person
インスタンスが返される事が確認できます。
ポイントとしては Person.ofEither("", -1)
で渡している ""
と -1
はどちらもそれぞれ Name
と Age
として不正な値です。ただしその結果は Left(Name could not be blank.)
と Name
が返すエラーになっています。
これは、Person.ofEither
が最初にエラーが見つかった時点で全体を失敗とし即座に結果を返す作りになっているためです。したがって Name.ofEither("")
でエラーが返された段階で結果を返すため Age.ofEither(-1)
は評価されず計算コストもかかりません。
この様な性質は Either
の代わりに例外を使った実装でも同様の性質を持ちますね。
可能な限り全てのエラーを集めて返すファクトリメソッド
Eitherや例外を使うファクトリメソッドの問題
さて、これまでの様な実装でDomain Modelの生成には必ず制約を満たすという事は実現することができました。
ただし、これらのファクトリメソッドが例えば最終的にPublicなWeb APIの実装として呼び出されていた場合をイメージしてみてください。
そのWeb APIの利用者は {'name': '', 'age': -1}
のようなリクエストを送って Status 400 {'error': 'Name could not be blank.'}
みたいなレスポンスを受け取るわけです。
その人は「なるほど name が空文字列なのが悪いんだな」と理解して改めて {'name': 'foo', 'age': -1}
というリクエストを送り直します。そうしたら今度は Age could not be negative.
みたいなエラーが返されるのです。
そんな状況に陥った時、大抵の人は「最初から判ってるエラー全部返さんかーーーい!!!💢」と思うことでしょう。めちゃくちゃ使い辛いWeb APIだと思われてしまいますね。
このような不幸を避けるべく、ファクトリメソッドも可能な限り全てのエラーを集めて返す形にしたい所です。
その際に Either
の代わりに使用できるデータ型が cats などで用意されている Validated
です。
Validated
Either[L, R]
が Left[L]
と Right[R]
で構成されているのと同様に、Validated[E, A]
は Invalid[E]
と Valid[A]
で構成されています。
Either
と異なる点は、Validated
同士を合成する際に両方とも Invalid
だったら、その中身を連結してくれるという性質を持つ所にあります。今のエラーを集めたいという目的にぴったりですね。
という訳で、Either[String, ...]
を返すファクトリメソッドの代わりに Validated[String, ...]
を返すファクトリメソッドを定義すれば良さそうに思います。
ところが一つ問題があって、Validated
のエラーの連結はその型の連結を使います[2]。従ってエラーの型が String
のままだと単純に文字列連結になるので、Name
と Age
の制約を両方満たさない時のエラーが "Name could not be blank.Age could not be negative."
という一つの文字列になってしまうのです。
これは流石に使い勝手が悪すぎるので、エラーの型をコレクションでラッピングすることにしましょう。つまり Validated[List[String], ...]
みたいな型にするわけですね。
NonEmptyChain
さて Validated[List[String], ...]
の様に List
を使っても良いのですが、List
は非常に連結の計算量が多いデータ構造です。なので今回のように連結される事が前提の処理ではあまり使いたくありません。
そこで、List
と同じく順序を維持したコレクションであり、連結操作の計算量が O(1) である Chain
というデータ構造を使うことにします。
また、今回は Validated
のエラー時の型として使うので、必ず空にならない事が保証できます[3]。
そこで必ず空ではない事を型上で表現できる NonEmptyChain
を利用します。つまり Validated[NonEmptyChain[String], ...]
を使うという事ですね。
この Validated
のエラー型に NonEmptyChain
を利用するという使い方は非常に多く使われるので、タイプエイリアスが用意されています。その名も ValidatedNec
です。
type ValidatedNec[E, A] = Validated[NonEmptyChain[E], A]
ではこの ValidatedNec
を使用してファクトリメソッドを定義してみましょう。
ValidatedNec を使ったファクトリメソッドの実装
Name
のファクトリメソッドの実装は以下のようになります。
case class Name private (value: String)
object Name {
def ofValidated(value: String): ValidatedNec[String, Name] = {
Validated.condNec(value.nonEmpty, Name(value), "Name could not be blank.")
}
}
Either
を使った版と比較してみましょう。
def ofEither(value: String): Either[String, Name] = {
Either.cond(value.nonEmpty, Name(value), "Name could not be blank.")
}
def ofValidated(value: String): ValidatedNec[String, Name] = {
Validated.condNec(value.nonEmpty, Name(value), "Name could not be blank.")
}
戻り値の型が Either
から ValidatedNec
になって、Either.cond
のメソッド呼び出しが Validated.condNec
に変わってるだけですね。
同様に Age
のファクトリメソッドも定義しましょう。
case class Age private (value: Int)
object Age {
def ofValidated(value: Int): ValidatedNec[String, Age] = {
Validated.condNec(value >= 0, Age(value), "Age could not be negative.")
}
}
そしてこれらのファクトリメソッドを使う Person
のファクトリメソッドを定義します。
import cats.implicits.given // タプルの拡張メソッド mapN を使えるように
case class Person(name: Name, age: Age)
object Person {
def ofValidated(name: String, age: Int): ValidatedNec[String, Person] = {
(Name.ofValidated(name), Age.ofValidated(age)).mapN(Person.apply)
}
}
ポイントは、 Validated
が flatMap
を持たないため、Either
の様に for式を使うことができない点です。また仮に for式が使えたとしても全てのエラーを先に取得したいので今回のケースでは利用できません。
そこで、まず Name
と Age
の両方の結果である Validated
をタプルにつめて、mapN
メソッドに Person.apply
を渡しています。mapN
はタプルの要素が全て Valid
であればその値を使って Person.apply
を呼び出し、要素のいずれかが Invalid
であれば全体を Invalid
として返します。
Either
版と比べて見ましょう。
def ofEither(name: String, age: Int): Either[String, Person] = {
for {
name <- Name.ofEither(name)
age <- Age.ofEither(age)
} yield Person(name, age)
}
def ofValidated(name: String, age: Int): ValidatedNec[String, Person] = {
(Name.ofValidated(name), Age.ofValidated(age)).mapN(Person.apply)
}
では実際にREPLで使ってみましょう。
scala> Person.ofValidated("", -1)
val res2: Validated[NonEmptyChain[String], Person] =
Invalid(Chain(Name could not be blank., Age could not be negative.))
scala> Person.ofValidated("gakuzzzz", 17)
val res3: Validated[NonEmptyChain[String], Person] =
Valid(Person(Name(gakuzzzz),Age(17)))
scala>
Person.ofValidated("", -1)
で両方のエラーメッセージが返されている事が見て取れますね。
これで無事にエラーを集約するファクトリメソッドを定義する事ができました。めでたしめでたし。
利用者が短絡か集約かを選択できるファクトリメソッド
さて、これで最初にエラーが見つかり次第即座に全体を失敗にして返すファクトリメソッドと、可能な限り全てのエラーを集約して返すファクトリメソッドの両方を用意することができました。
ただこれを全てのDomain Modelで両方定義していくのは非常に面倒です。
どうせなら一つのファクトリメソッドで、利用者が結果を Either
で得るか Validated
で得るかを選択できると嬉しいですよね。果たしてそんなメソッドを定義することが可能なのでしょうか?
実は可能なんです。
それを可能とする秘密兵器が ApplicativeError
になります。
ApplicativeError
は抽象的な概念なので、どんなもなのか言葉で解説するよりも実際に使い方を見て頂くほうがわかりやすいと思います。さくさく実装に進みましょう。
case class Name private (value: String)
object Name {
def of[F[_, _]](value: String)(
using AE: ApplicativeError[F[NonEmptyChain[String], *], NonEmptyChain[String]]
): F[NonEmptyChain[String], Name] = {
if (value.nonEmpty) AE.pure(Name(value))
else AE.raiseError(NonEmptyChain.one("Name could not be blank."))
}
}
シグネチャが恐ろしくややこしくなりましたが、少しずつ見ていきましょう。
def of[F[_, _]](value: String)(
まず def of[F[_, _]]
でメソッドスコープの型引数 F
が導入されています。 この F
には Either
や Validated
を指定する想定ですので、それ自身が型引数を2つ持つ事を示すように F[_, _]
としています。
そして通常の引数グループである (value: String)
が宣言されています。これは今までのファクトリメソッドと同じですね。
using AE: ApplicativeError[F[NonEmptyChain[String], *], NonEmptyChain[String]]
そして利用する型クラスとして (using AE: ApplicativeError[F[NonEmptyChain[String], *], NonEmptyChain[String]])
が宣言されています。ここが非常にわかりにくいですが「メソッドスコープの型引数 F
のエラー型として NonEmptyChain[String]
を利用し、その F に合致した ApplicativeError
型クラスを使うよ」ぐらいの意味だと認識して頂ければここでは十分です。
): F[NonEmptyChain[String], Name] = {
戻り値の型が F[NonEmptyChain[String], Name]
になっているので、メソッド呼び出し時に of[Either](...)
と呼び出せば Either[NonEmptyChain[String], Name]
が結果になり、 of[Validated](...)
と呼び出せば Validated[NonEmptyChain[String], Name]
(つまりValidatedNec[String, Name]
) が結果になる、という訳ですね。
if (value.nonEmpty) AE.pure(Name(value))
else AE.raiseError(NonEmptyChain.one("Name could not be blank."))
メソッドの中身は単純な if式で、制約を満たしていれば値を pure
で包んで返し、満たしていなければ NonEmptyChain
にエラーメッセージを突っ込んだものをエラーとして raiseError
している、という形です。
同様に Age
のファクトリメソッドも定義しましょう。
case class Age private (value: Int)
object Age {
def of[F[_, _]](value: Int)(
using AE: ApplicativeError[F[NonEmptyChain[String], *], NonEmptyChain[String]]
): F[NonEmptyChain[String], Age] = {
if (value >= 0) AE.pure(Age(value))
else AE.raiseError(NonEmptyChain.one("Age could not be negative."))
}
}
次に Person
のファクトリメソッドですね。
case class Person(name: Name, age: Age)
object Person {
def of[F[_, _]](name: String, age: Int)(
using AE: ApplicativeError[F[NonEmptyChain[String], *], NonEmptyChain[String]]
): F[NonEmptyChain[String], Person] = {
AE.map2Eval(Name.of(name), Later(Age.of(age)))(Person.apply).value
}
}
シグネチャは Name
や Age
のファクトリメソッドとほぼほぼ同じです。
実装については ofValidated
の時と同じく以下のような実装でも意図した動作はします。
(Name.of(name), Age.of(age)).mapN(Person.apply)
ただし、これだと Name.of
と Age.of
の両方を評価してタプルを作成するので、仮に Either
を指定した場合でも両方のファクトリメソッドが実行されます。
Either
を指定した場合、せっかくなら短絡評価をして先にエラーが見つかった場合は後続の処理をさせないようにしたいですね。そこで利用できるのが map2Eval
です。
AE.map2Eval(Name.of(name), Later(Age.of(age)))(Person.apply).value
Age.of
を Later
で包むことによって評価を遅延させる事ができ、map2Eval
が必要としなければ Age.of
の呼び出しを省略することができます。
では実際にREPLで使ってみましょう。
scala> Person.of[Either]("", -1)
val res4: Either[NonEmptyChain[String], Person] =
Left(Chain(Name could not be blank.))
scala> Person.of[Validated]("", -1)
val res5: Validated[NonEmptyChain[String], Person] =
Invalid(Chain(Name could not be blank., Age could not be negative.))
scala> Person.of[Either]("gakuzzzz", 17)
val res6: Either[NonEmptyChain[String], Person] =
Right(Person(Name(gakuzzzz),Age(17)))
scala> Person.of[Validated]("gakuzzzz", 17)
val res7: Validated[NonEmptyChain[String], Person] =
Valid(Person(Name(gakuzzzz),Age(17)))
scala>
Person.of[Either]("", -1)
と Either
を指定した場合には Name could not be blank.
しか返ってきませんが、 Person.of[Validated]("", -1)
と Validated
を指定した場合には Name could not be blank.
と Age could not be negative.
の両方が返ってきてることが見て取れるかと思います。大変素晴らしいですね。
これで利用者が結果を Either
で得るか Validated
で得るかを選択できるファクトリメソッドを定義する事ができました。めでたしめでたし。
Always-Valid Domain Model のファクトリメソッドはみんなこんな感じで定義されてると嬉しいですね。
[おまけ] 使い勝手の改善
この記事の本題は前章までなのですが、実は前章で定義したファクトリメソッドには一つ手抜きがありました。というのも Either
と Validated
を統一的に扱うために、エラーの型を NonEmptyChain
に固定してしまった事ですね。
そのため、Person.of[Either]
の戻り値型も Either[NonEmptyChain[String], Person]
型になってしまっています。Either
の時はエラーは一つとわかっているのだから、Either[String, Person]
で返してほしいですよね。
Either
を指定した時は String
を返し、Validated
を指定した時は NonEmptyChain[String]
を返す。そんなメソッドが定義可能なのでしょうか?
実はこれもできてしまいます。
ある型のときにはあちらの型を、また別の型の時にはそちらの型を、といった事をしたい場合、型クラスに抽象型メンバを持たせる事で実現できます。
具体的に見ていきましょう。今回はエラーを集約する型を導出したいので、ErrorCollective
という型クラスを作ることにします。
trait ErrorCollective[F[_, _]] {
type G[E]
def applicativeError[E]: ApplicativeError[F[G[E], *], G[E]]
protected def oneOfError[E](e: E): G[E]
def cond[E, A](p: => Boolean, ifTrue: => A, ifFalse: => E): F[G[E], A] = {
if (p) applicativeError.pure(ifTrue)
else applicativeError.raiseError(oneOfError(ifFalse))
}
}
Either
と Validated
で使い分けたいので、この型クラスには F[_, _]
と2つの型引数を持つ型を指定できるようにします。
そしてエラーの型を抽象型メンバとして type G[E]
を定義します。
後は便利メソッドの追加です。この型クラスの利用側で ApplicativeError
を利用することは判っているので、利用側のシグネチャを簡素にできるように ApplicativeError
インスタンス自体を ErrorCollective
に持たせてしまいます。
同様によく使う Either.cond
のようなメソッドも提供しておきます。
この ErrorCollective
のインスタンス定義が以下のようになります。
object ErrorCollective {
given ErrorCollective[Either] with {
type G[E] = E
def applicativeError[E] = summon
protected def oneOfError[E](e: E): G[E] = e
}
given ErrorCollective[Validated] with {
type G[E] = NonEmptyChain[E]
def applicativeError[E] = summon
protected def oneOfError[E](e: E): G[E] = NonEmptyChain.one(e)
}
}
Either
の場合は、エラーの型である抽象型メンバ G[E]
の実体をそのまま E
とし、Validated
の場合は NonEmptyChain[E]
にします。
そしてファクトリメソッドは ErrorCollective
を使い以下のように宣言します。
case class Name private (value: String)
object Name {
def of2[F[_, _]](value: String)
(using EC: ErrorCollective[F]): F[EC.G[String], Name] = {
EC.cond(value.nonEmpty, Name(value), "Name could not be blank.")
}
}
前章の実装と比べて大変簡素になりました。比較するとこんな感じです。
def of[F[_, _]](value: String)(
using AE: ApplicativeError[F[NonEmptyChain[String], *], NonEmptyChain[String]]
): F[NonEmptyChain[String], Name] = {
if (value.nonEmpty) AE.pure(Name(value))
else AE.raiseError(NonEmptyChain.one("Name could not be blank."))
}
def of2[F[_, _]](value: String)
(using EC: ErrorCollective[F]): F[EC.G[String], Name] = {
EC.cond(value.nonEmpty, Name(value), "Name could not be blank.")
}
ポイントは戻り値の型が F[EC.G[String], Name]
になっている事ですね。ErrorCollective
型の引数 EC
のメンバ型である G
つまり EC.G[String]
が結果のエラー型になっています。
従って Either
が指定された場合は EC.G[String]
が String
になり、Validated
が指定された場合は EC.G[String]
が NonEmptyChain[String]
になるわけです。
さくさくと Age
と Person
も定義しましょう。
case class Age private (value: Int)
object Age {
def of2[F[_, _]](value: Int)
(using EC: ErrorCollective[F]): F[EC.G[String], Age] = {
EC.cond(value >= 0, Age(value), "Age could not be negative.")
}
}
case class Person(name: Name, age: Age)
object Person {
def of2[F[_, _]](name: String, age: Int)
(using EC: ErrorCollective[F]): F[EC.G[String], Person] = {
EC.applicativeError
.map2Eval(Name.of2(name), Later(Age.of2(age)))(Person.apply).value
}
}
では実際にREPLで使ってみましょう。
scala> Person.of2[Either]("", -1)
val res8: Either[String, Person] = Left(Name could not be blank.)
scala> Person.of2[Validated]("", -1)
val res9: Validated[NonEmptyChain[String], Person] =
Invalid(Chain(Name could not be blank., Age could not be negative.))
scala> Person.of2[Either]("gakuzzzz", 17)
val res10: Either[String, Person] = Right(Person(Name(gakuzzzz),Age(17)))
scala> Person.of2[Validated]("gakuzzzz", 17)
val res11: Validated[NonEmptyChain[String], Person] =
Valid(Person(Name(gakuzzzz),Age(17)))
scala>
意図通り Either
を指定した場合の結果が Either[String, Person]
に、Validated
を指定した場合の結果が Validated[NonEmptyChain[String], Person]
になっていますね。
[おまけ] 親方!空から非同期APIが!
こんな感じで便利な ApplicativeError
ですが、実は非同期計算を行うための Future
にも ApplicativeError
インスタンスは用意されています。
従って以下のようなものを用意してあげると
given (using ExecutionContext): ErrorCollective[[E, A] =>> Future[A]] with {
type G[E] = Throwable
def applicativeError[E] = summon
protected def oneOfError[E](e: E): G[E] = new Exception(e.toString)
}
途端にファクトリメソッドを非同期APIにすることができてしまいます。
scala> import scala.concurrent.Future
scala> import scala.concurrent.ExecutionContext.Implicits.global
scala> Person.of2[[E, A] =>> Future[A]]("", -1)
val res12: Future[Person] = Future(Failure(Name could not be blank.))
scala> Person.of2[[E, A] =>> Future[A]]("gakuzzzz", 17)
val res13: Future[Person] = Future(<not completed>)
scala>
便利ですね?
Appendix
以下ソースコードを全量載せておきます。
/*
* ApplicativeError Sample
*
* Copyright(c) gakuzzzz
*
* This software is released under the MIT License.
* http://opensource.org/licenses/mit-license.php
*/
package jp.t2v.lab.errorsample
import cats.data.{NonEmptyChain, Validated, ValidatedNec}
import cats.{Applicative, ApplicativeError, CommutativeApplicative, Later}
import cats.implicits.given
import scala.concurrent.{ExecutionContext, Future}
case class Name private (value: String)
object Name {
def ofEither(value: String): Either[String, Name] = {
Either.cond(value.nonEmpty, Name(value), "Name could not be blank.")
}
def ofValidated(value: String): ValidatedNec[String, Name] = {
Validated.condNec(value.nonEmpty, Name(value), "Name could not be blank.")
}
def of[F[_, _]](value: String)(
using AE: ApplicativeError[F[NonEmptyChain[String], *], NonEmptyChain[String]]
): F[NonEmptyChain[String], Name] = {
if (value.nonEmpty) AE.pure(Name(value))
else AE.raiseError(NonEmptyChain.one("Name could not be blank."))
}
def of2[F[_, _]](value: String)
(using EC: ErrorCollective[F]): F[EC.G[String], Name] = {
EC.cond(value.nonEmpty, Name(value), "Name could not be blank.")
}
}
case class Age private (value: Int)
object Age {
def ofEither(value: Int): Either[String, Age] = {
Either.cond(value >= 0, Age(value), "Age could not be negative.")
}
def ofValidated(value: Int): ValidatedNec[String, Age] = {
Validated.condNec(value >= 0, Age(value), "Age could not be negative.")
}
def of[F[_, _]](value: Int)(
using AE: ApplicativeError[F[NonEmptyChain[String], *], NonEmptyChain[String]]
): F[NonEmptyChain[String], Age] = {
if (value >= 0) AE.pure(Age(value))
else AE.raiseError(NonEmptyChain.one("Age could not be negative."))
}
def of2[F[_, _]](value: Int)
(using EC: ErrorCollective[F]): F[EC.G[String], Age] = {
EC.cond(value >= 0, Age(value), "Age could not be negative.")
}
}
case class Person(name: Name, age: Age)
object Person {
def ofEither(name: String, age: Int): Either[String, Person] = {
for {
name <- Name.ofEither(name)
age <- Age.ofEither(age)
} yield Person(name, age)
}
def ofValidated(name: String, age: Int): ValidatedNec[String, Person] = {
(Name.ofValidated(name), Age.ofValidated(age)).mapN(Person.apply)
}
def of[F[_, _]](name: String, age: Int)(
using AE: ApplicativeError[F[NonEmptyChain[String], *], NonEmptyChain[String]]
): F[NonEmptyChain[String], Person] = {
AE.map2Eval(Name.of(name), Later(Age.of(age)))(Person.apply).value
}
def of2[F[_, _]](name: String, age: Int)
(using EC: ErrorCollective[F]): F[EC.G[String], Person] = {
EC.applicativeError
.map2Eval(Name.of2(name), Later(Age.of2(age)))(Person.apply).value
}
}
trait ErrorCollective[F[_, _]] {
type G[E]
def applicativeError[E]: ApplicativeError[F[G[E], *], G[E]]
def cond[E, A](p: => Boolean, ifTrue: => A, ifFalse: => E): F[G[E], A] = {
if (p) applicativeError.pure(ifTrue)
else applicativeError.raiseError(oneOfError(ifFalse))
}
protected def oneOfError[E](e: E): G[E]
}
object ErrorCollective {
given ErrorCollective[Either] with {
type G[E] = E
def applicativeError[E] = summon
protected def oneOfError[E](e: E): G[E] = e
}
given ErrorCollective[Validated] with {
type G[E] = NonEmptyChain[E]
def applicativeError[E] = summon
protected def oneOfError[E](e: E): G[E] = NonEmptyChain.one(e)
}
given (using ExecutionContext): ErrorCollective[[E, A] =>> Future[A]] with {
type G[E] = Throwable
def applicativeError[E] = summon
protected def oneOfError[E](e: E): G[E] = new Exception(e.toString)
}
}
Discussion