diff --git a/core/src/main/scala/cats/data/Validated.scala b/core/src/main/scala/cats/data/Validated.scala index d55915dab5..708f1156a0 100644 --- a/core/src/main/scala/cats/data/Validated.scala +++ b/core/src/main/scala/cats/data/Validated.scala @@ -44,6 +44,8 @@ sealed abstract class Validated[+E, +A] extends Product with Serializable { /** * Return this if it is Valid, or else fall back to the given default. + * The functionality is similar to that of [[findValid]] except for failure accumulation, + * where here only the error on the right is preserved and the error on the left is ignored. */ def orElse[EE, AA >: A](default: => Validated[EE, AA]): Validated[EE, AA] = this match { @@ -51,6 +53,18 @@ sealed abstract class Validated[+E, +A] extends Product with Serializable { case Invalid(_) => default } + /** + * If `this` is valid return `this`, otherwise if `that` is valid return `that`, otherwise combine the failures. + * This is similar to [[orElse]] except that here failures are accumulated. + */ + def findValid[EE >: E, AA >: A](that: => Validated[EE, AA])(implicit EE: Semigroup[EE]): Validated[EE, AA] = this match { + case v @ Valid(_) => v + case Invalid(e) => that match { + case v @ Valid(_) => v + case Invalid(ee) => Invalid(EE.combine(e, ee)) + } + } + /** * Converts the value to an Either[E, A] */ diff --git a/tests/src/test/scala/cats/tests/ValidatedTests.scala b/tests/src/test/scala/cats/tests/ValidatedTests.scala index d40ee2cf6e..7904f7a860 100644 --- a/tests/src/test/scala/cats/tests/ValidatedTests.scala +++ b/tests/src/test/scala/cats/tests/ValidatedTests.scala @@ -114,6 +114,25 @@ class ValidatedTests extends CatsSuite { } } + test("findValid accumulates failures") { + forAll { (v: Validated[String, Int], u: Validated[String, Int]) => + v findValid u shouldEqual { (v, u) match { + case (vv @ Valid(_), _) => vv + case (_, uu @ Valid(_)) => uu + case (Invalid(s1), Invalid(s2)) => Invalid(s1 ++ s2) + }} + } + } + + test("orElse ignores left failure") { + forAll { (v: Validated[String, Int], u: Validated[String, Int]) => + v orElse u shouldEqual { (v, u) match { + case (vv @ Valid(_), _) => vv + case (_, uu) => uu + }} + } + } + test("valueOr consistent with swap then map then merge") { forAll { (v: Validated[String, Int], f: String => Int) => v.valueOr(f) should === (v.swap.map(f).merge)