Open5

Either を使う時の逆引き辞典

gakuzzzzgakuzzzz

前提

def someDomainLogic(foo: Foo): Either[FooError, Result]

という Either を返すドメインロジックがあり、

val list: List[Foo] = ...

という複数の Foo が存在している状況において、目的に合わせて someDomainLogic をどの様に呼び出せばいいかを示します。

ここでは Scala 3.3.1 と cats-core 2.10.0 を使用します。
サンプルコードには以下の import が含まれているものとします。

import cats.*
import cats.data.*
import cats.implicits.*
gakuzzzzgakuzzzz

全て成功すれば結果を List として、失敗は最初の一つだけ得たい場合

list 内の全ての Foo に対して someDomainLogic を適用し、いずれか一つでも FooError を返した場合は最初の FooError を得、 FooError が一つもなければ List[Result] を得たい場合。

つまり fail fast の場合。

traverse を使います。

def someDomainLogic(foo: Foo): Either[FooError, Result] = ...
val list: List[Foo] = ...

val results: Either[FooError, List[Result]] = list.traverse(someDomainLogic)
gakuzzzzgakuzzzz

全て成功すれば結果を List とし、失敗は全て集約して得たい場合

list 内の全ての Foo に対して someDomainLogic を適用し、FooError を返すものがあった場合は全ての FooError を得、 FooError が一つもなければ List[Result] を得たい場合。

この場合はエラーの集約を行うため、 Either を扱うより Validated を使う方がお勧めです。
もし someDomainLogic の戻り値型を Validated に変更する事が可能であれば最初にそれを検討しましょう。

someDomainLogic のシグネチャを変更できない場合は Either から Validated に変換して traverse しましょう。

// ドメインロジックを Validated に変更できた場合 
def someDomainLogic(foo: Foo): ValidatedNec[FooError, Result] = ...
val list: List[Foo] = ...

val results: ValidatedNec[FooError, List[Result]] = list.traverse(someDomainLogic)
// ドメインロジックを Validated に変更できなかった場合
def someDomainLogic(foo: Foo): Either[FooError, Result] = ...
val list: List[Foo] = ...

val results: ValidatedNec[FooError, List[Result]] = 
  list.traverse(foo => someDomainLogic(foo).toValidatedNec)
gakuzzzzgakuzzzz

成功したものと失敗したものをそれぞれ集めたい場合

List[FooError]List[Result] の両方を得たい場合。

partitionMap を使います。

def someDomainLogic(foo: Foo): Either[FooError, Result] = ...
val list: List[Foo] = ...

val (errors, results) = list.partitionMap(someDomainLogic)
println(errors)  // List[FooError]
println(results) // List[Result]

ちなみに partitionMap は標準APIにあるので cats を使っていなくても利用できます。

gakuzzzzgakuzzzz

失敗は集約し、成功は件数だけ得たい場合

例えば someDomainLogic の副作用だけが重要で結果は必要なく成功した件数だけ得たい場合。

もちろん 成功したものと失敗したものをそれぞれ集めたい場合 と同様に partitionMap をして count しても良いですが、件数が多い等何らかの事情で無駄なオブジェクトを作りたくない場合には foldMap を使います。

def someDomainLogic(foo: Foo): Either[FooError, Result] = ...
val list: List[Foo] = ...

val (errors, successCount) = list.foldMap { foo =>
  someDomainLogic(foo).fold(
    e => (Chain.one(e), 0L),
    _ => (Chain.empty, 1L)
  )
}

println(errors)       // Chain[FooError]
println(successCount) // Long