diff --git a/core/src/main/scala/cats/data/EitherT.scala b/core/src/main/scala/cats/data/EitherT.scala index 5f73115532..7dc39d170a 100644 --- a/core/src/main/scala/cats/data/EitherT.scala +++ b/core/src/main/scala/cats/data/EitherT.scala @@ -205,6 +205,9 @@ final case class EitherT[F[_], A, B](value: F[Either[A, B]]) { def toValidatedNel(implicit F: Functor[F]): F[ValidatedNel[A, B]] = F.map(value)(_.toValidatedNel) + def toValidatedNec(implicit F: Functor[F]): F[ValidatedNec[A, B]] = + F.map(value)(_.toValidatedNec) + /** Run this value as a `[[Validated]]` against the function and convert it back to an `[[EitherT]]`. * * The [[Applicative]] instance for `EitherT` "fails fast" - it is often useful to "momentarily" have @@ -274,6 +277,12 @@ final case class EitherT[F[_], A, B](value: F[Either[A, B]]) { */ def toNestedValidatedNel(implicit F: Functor[F]): Nested[F, ValidatedNel[A, ?], B] = Nested[F, ValidatedNel[A, ?], B](F.map(value)(_.toValidatedNel)) + + /** + * Transform this `EitherT[F, A, B]` into a `[[Nested]][F, ValidatedNec[A, ?], B]`. + */ + def toNestedValidatedNec(implicit F: Functor[F]): Nested[F, ValidatedNec[A, ?], B] = + Nested[F, ValidatedNec[A, ?], B](F.map(value)(_.toValidatedNec)) } object EitherT extends EitherTInstances { diff --git a/core/src/main/scala/cats/data/Validated.scala b/core/src/main/scala/cats/data/Validated.scala index 4cc8d56884..6ea24189c6 100644 --- a/core/src/main/scala/cats/data/Validated.scala +++ b/core/src/main/scala/cats/data/Validated.scala @@ -314,7 +314,7 @@ sealed abstract class Validated[+E, +A] extends Product with Serializable { } } -object Validated extends ValidatedInstances with ValidatedFunctions{ +object Validated extends ValidatedInstances with ValidatedFunctions with ValidatedFunctionsBinCompat0 { final case class Valid[+A](a: A) extends Validated[Nothing, A] final case class Invalid[+E](e: E) extends Validated[E, Nothing] @@ -602,3 +602,38 @@ private[data] trait ValidatedFunctions { final def condNel[A, B](test: Boolean, b: => B, a: => A): ValidatedNel[A, B] = if (test) validNel(b) else invalidNel(a) } + +private[data] trait ValidatedFunctionsBinCompat0 { + + + /** + * Converts a `B` to a `ValidatedNec[A, B]`. + * + * For example: + * {{{ + * scala> Validated.validNec[IllegalArgumentException, String]("Hello world") + * res0: ValidatedNec[IllegalArgumentException, String] = Valid(Hello world) + * }}} + */ + def validNec[A, B](b: B): ValidatedNec[A, B] = Validated.Valid(b) + + + + /** + * Converts an `A` to a `ValidatedNec[A, B]`. + * + * For example: + * {{{ + * scala> Validated.invalidNec[IllegalArgumentException, String](new IllegalArgumentException("Argument is nonzero")) + * res0: ValidatedNec[IllegalArgumentException, String] = Invalid(Chain(java.lang.IllegalArgumentException: Argument is nonzero)) + * }}} + */ + def invalidNec[A, B](a: A): ValidatedNec[A, B] = Validated.Invalid(NonEmptyChain.one(a)) + + /** + * If the condition is satisfied, return the given `B` as valid NEC, + * otherwise return the given `A` as invalid NEC. + */ + final def condNec[A, B](test: Boolean, b: => B, a: => A): ValidatedNec[A, B] = + if (test) validNec(b) else invalidNec(a) +} diff --git a/core/src/main/scala/cats/data/package.scala b/core/src/main/scala/cats/data/package.scala index 5333a66b41..218300dbc0 100644 --- a/core/src/main/scala/cats/data/package.scala +++ b/core/src/main/scala/cats/data/package.scala @@ -5,6 +5,9 @@ package object data { type ValidatedNel[+E, +A] = Validated[NonEmptyList[E], A] type IorNel[+B, +A] = Ior[NonEmptyList[B], A] type EitherNel[+E, +A] = Either[NonEmptyList[E], A] + type ValidatedNec[+E, +A] = Validated[NonEmptyChain[E], A] + type IorNec[+B, +A] = Ior[NonEmptyChain[B], A] + type EitherNec[+E, +A] = Either[NonEmptyChain[E], A] def NonEmptyStream[A](head: A, tail: Stream[A] = Stream.empty): NonEmptyStream[A] = OneAnd(head, tail) diff --git a/core/src/main/scala/cats/syntax/all.scala b/core/src/main/scala/cats/syntax/all.scala index ebee850312..78a67f51df 100644 --- a/core/src/main/scala/cats/syntax/all.scala +++ b/core/src/main/scala/cats/syntax/all.scala @@ -72,3 +72,6 @@ trait AllSyntaxBinCompat1 trait AllSyntaxBinCompat2 extends ParallelTraverseSyntax + with EitherSyntaxBinCompat0 + with ListSyntaxBinCompat0 + with ValidatedSyntaxBincompat0 diff --git a/core/src/main/scala/cats/syntax/either.scala b/core/src/main/scala/cats/syntax/either.scala index 40bd310652..78fcfc7863 100644 --- a/core/src/main/scala/cats/syntax/either.scala +++ b/core/src/main/scala/cats/syntax/either.scala @@ -1,7 +1,8 @@ package cats package syntax -import cats.data.{EitherT, Ior, NonEmptyList, Validated, ValidatedNel} +import cats.data._ + import scala.reflect.ClassTag import scala.util.{Failure, Success, Try} import EitherSyntax._ @@ -374,6 +375,49 @@ final class EitherIdOps[A](val obj: A) extends AnyVal { } +trait EitherSyntaxBinCompat0 { + implicit final def catsSyntaxEitherBinCompat0[A, B](eab: Either[A, B]): EitherOpsBinCompat0[A, B] = + new EitherOpsBinCompat0(eab) + + implicit final def catsSyntaxEitherIdBinCompat0[A](a: A): EitherIdOpsBinCompat0[A] = + new EitherIdOpsBinCompat0(a) +} + +final class EitherIdOpsBinCompat0[A](val value: A) extends AnyVal { + /** + * Wrap a value to a left EitherNec + * + * For example: + * {{{ + * scala> import cats.implicits._, cats.data.NonEmptyChain + * scala> "Err".leftNec[Int] + * res0: Either[NonEmptyChain[String], Int] = Left(Chain(Err)) + * }}} + */ + def leftNec[B]: Either[NonEmptyChain[A], B] = Left(NonEmptyChain.one(value)) + + /** + * Wrap a value to a right EitherNec + * + * For example: + * {{{ + * scala> import cats.implicits._, cats.data.NonEmptyChain + * scala> 1.rightNec[String] + * res0: Either[NonEmptyChain[String], Int] = Right(1) + * }}} + */ + def rightNec[B]: Either[NonEmptyChain[B], A] = Right(value) +} + +final class EitherOpsBinCompat0[A, B](val value: Either[A, B]) extends AnyVal { + /** Returns a [[cats.data.ValidatedNec]] representation of this disjunction with the `Left` value + * as a single element on the `Invalid` side of the [[cats.data.NonEmptyList]]. */ + def toValidatedNec: ValidatedNec[A, B] = value match { + case Left(a) => Validated.invalidNec(a) + case Right(b) => Validated.valid(b) + } +} + /** Convenience methods to use `Either` syntax inside `Either` syntax definitions. */ private[cats] object EitherUtil { def leftCast[A, B, C](right: Right[A, B]): Either[C, B] = diff --git a/core/src/main/scala/cats/syntax/list.scala b/core/src/main/scala/cats/syntax/list.scala index 710ab5a2fb..820c906168 100644 --- a/core/src/main/scala/cats/syntax/list.scala +++ b/core/src/main/scala/cats/syntax/list.scala @@ -2,7 +2,7 @@ package cats package syntax import scala.collection.immutable.SortedMap -import cats.data.NonEmptyList +import cats.data.{NonEmptyChain, NonEmptyList} trait ListSyntax { implicit final def catsSyntaxList[A](la: List[A]): ListOps[A] = new ListOps(la) @@ -49,3 +49,30 @@ final class ListOps[A](val la: List[A]) extends AnyVal { toNel.fold(SortedMap.empty[B, NonEmptyList[A]])(_.groupBy(f)) } } + +trait ListSyntaxBinCompat0 { + implicit final def catsSyntaxListBinCompat0[A](la: List[A]): ListOpsBinCompat0[A] = new ListOpsBinCompat0(la) +} + +final class ListOpsBinCompat0[A](val la: List[A]) extends AnyVal { + + /** + * Groups elements inside this `List` according to the `Order` of the keys + * produced by the given mapping function. + * + * {{{ + * scala> import cats.data.NonEmptyChain + * scala> import scala.collection.immutable.SortedMap + * scala> import cats.implicits._ + * + * scala> val list = List(12, -2, 3, -5) + * + * scala> list.groupByNec(_ >= 0) + * res0: SortedMap[Boolean, NonEmptyChain[Int]] = Map(false -> Chain(-2, -5), true -> Chain(12, 3)) + * }}} + */ + def groupByNec[B](f: A => B)(implicit B: Order[B]): SortedMap[B, NonEmptyChain[A]] = { + implicit val ordering = B.toOrdering + NonEmptyChain.fromSeq(la).fold(SortedMap.empty[B, NonEmptyChain[A]])(_.groupBy(f).toSortedMap) + } +} diff --git a/core/src/main/scala/cats/syntax/package.scala b/core/src/main/scala/cats/syntax/package.scala index cf16200fff..03b4413f22 100644 --- a/core/src/main/scala/cats/syntax/package.scala +++ b/core/src/main/scala/cats/syntax/package.scala @@ -23,7 +23,7 @@ package object syntax { object contravariant extends ContravariantSyntax object contravariantSemigroupal extends ContravariantSemigroupalSyntax object contravariantMonoidal extends ContravariantMonoidalSyntax - object either extends EitherSyntax + object either extends EitherSyntax with EitherSyntaxBinCompat0 object eq extends EqSyntax object flatMap extends FlatMapSyntax object foldable extends FoldableSyntax @@ -31,7 +31,7 @@ package object syntax { object group extends GroupSyntax object invariant extends InvariantSyntax object ior extends IorSyntax - object list extends ListSyntax + object list extends ListSyntax with ListSyntaxBinCompat0 object monad extends MonadSyntax object monadError extends MonadErrorSyntax object monoid extends MonoidSyntax @@ -51,7 +51,7 @@ package object syntax { object traverse extends TraverseSyntax object nonEmptyTraverse extends NonEmptyTraverseSyntax object unorderedTraverse extends UnorderedTraverseSyntax - object validated extends ValidatedSyntax with ValidatedExtensionSyntax + object validated extends ValidatedSyntax with ValidatedExtensionSyntax with ValidatedSyntaxBincompat0 object vector extends VectorSyntax object writer extends WriterSyntax object set extends SetSyntax diff --git a/core/src/main/scala/cats/syntax/validated.scala b/core/src/main/scala/cats/syntax/validated.scala index fef761d817..8b12f636b2 100644 --- a/core/src/main/scala/cats/syntax/validated.scala +++ b/core/src/main/scala/cats/syntax/validated.scala @@ -1,7 +1,7 @@ package cats package syntax -import cats.data.{ Validated, ValidatedNel } +import cats.data.{Validated, ValidatedNec, ValidatedNel} trait ValidatedSyntax { implicit final def catsSyntaxValidatedId[A](a: A): ValidatedIdSyntax[A] = new ValidatedIdSyntax(a) @@ -23,3 +23,34 @@ final class ValidatedExtension[E, A](val self: Validated[E, A]) extends AnyVal { def liftTo[F[_]](implicit F: ApplicativeError[F, E]): F[A] = new ApplicativeErrorExtensionOps(F).fromValidated(self) } + +trait ValidatedSyntaxBincompat0 { + implicit final def catsSyntaxValidatedIdBinCompat0[A](a: A): ValidatedIdOpsBinCompat0[A] = + new ValidatedIdOpsBinCompat0(a) +} + +final class ValidatedIdOpsBinCompat0[A](val a: A) extends AnyVal { + /** + * Wrap a value to a valid ValidatedNec + * + * For example: + * {{{ + * scala> import cats.implicits._, cats.data._ + * scala> 1.validNec[String] + * res0: Validated[NonEmptyChain[String], Int] = Valid(1) + * }}} + */ + def validNec[B]: ValidatedNec[B, A] = Validated.Valid(a) + + /** + * Wrap a value to an invalid ValidatedNec + * + * For example: + * {{{ + * scala> import cats.implicits._, cats.data._ + * scala> "Err".invalidNec[Int] + * res0: Validated[NonEmptyChain[String], Int] = Invalid(Chain(Err)) + * }}} + */ + def invalidNec[B]: ValidatedNec[A, B] = Validated.invalidNec(a) +} diff --git a/tests/src/test/scala/cats/tests/EitherSuite.scala b/tests/src/test/scala/cats/tests/EitherSuite.scala index 4d3a791ddd..0e3d8586b6 100644 --- a/tests/src/test/scala/cats/tests/EitherSuite.scala +++ b/tests/src/test/scala/cats/tests/EitherSuite.scala @@ -223,6 +223,7 @@ class EitherSuite extends CatsSuite { x.isLeft should === (x.toList.isEmpty) x.isLeft should === (x.toValidated.isInvalid) x.isLeft should === (x.toValidatedNel.isInvalid) + x.isLeft should === (x.toValidatedNec.isInvalid) Option(x.isLeft) should === (x.toEitherT[Option].isLeft) } } diff --git a/tests/src/test/scala/cats/tests/EitherTSuite.scala b/tests/src/test/scala/cats/tests/EitherTSuite.scala index 4e7f198082..e87400d1c9 100644 --- a/tests/src/test/scala/cats/tests/EitherTSuite.scala +++ b/tests/src/test/scala/cats/tests/EitherTSuite.scala @@ -141,6 +141,12 @@ class EitherTSuite extends CatsSuite { } } + test("toValidatedNec") { + forAll { (eithert: EitherT[List, String, Int]) => + eithert.toValidatedNec.map(_.toEither.leftMap(_.head)) should === (eithert.value) + } + } + test("toNested") { forAll { (eithert: EitherT[List, String, Int]) => eithert.toNested.value should === (eithert.value) @@ -159,6 +165,12 @@ class EitherTSuite extends CatsSuite { } } + test("toNestedValidatedNec") { + forAll { (eithert: EitherT[List, String, Int]) => + eithert.toNestedValidatedNec.value should === (eithert.value.map(_.toValidatedNec)) + } + } + test("withValidated") { forAll { (eithert: EitherT[List, String, Int], f: String => Char, g: Int => Double) => eithert.withValidated(_.bimap(f, g)) should === (eithert.bimap(f, g)) diff --git a/tests/src/test/scala/cats/tests/SyntaxSuite.scala b/tests/src/test/scala/cats/tests/SyntaxSuite.scala index 0e3350eeb2..2e8a21ff0e 100644 --- a/tests/src/test/scala/cats/tests/SyntaxSuite.scala +++ b/tests/src/test/scala/cats/tests/SyntaxSuite.scala @@ -4,7 +4,7 @@ package tests import scala.collection.immutable.SortedSet import scala.collection.immutable.SortedMap import cats.arrow.Compose -import cats.data.{Binested, Nested, NonEmptyList, NonEmptySet} +import cats.data.{Binested, Nested, NonEmptyChain, NonEmptyList, NonEmptySet} import cats.instances.AllInstances import cats.syntax.{AllSyntax, AllSyntaxBinCompat} @@ -377,5 +377,12 @@ object SyntaxSuite extends AllSyntaxBinCompat with AllInstances with AllSyntax { val grouped: SortedMap[B, NonEmptyList[A]] = list.groupByNel(f) } + def testNonEmptyChain[A, B: Order] : Unit = { + val f = mock[A => B] + val list = mock[List[A]] + + val grouped: SortedMap[B, NonEmptyChain[A]] = list.groupByNec(f) + } + } diff --git a/tests/src/test/scala/cats/tests/ValidatedSuite.scala b/tests/src/test/scala/cats/tests/ValidatedSuite.scala index 82a779acec..0cf40334f3 100644 --- a/tests/src/test/scala/cats/tests/ValidatedSuite.scala +++ b/tests/src/test/scala/cats/tests/ValidatedSuite.scala @@ -280,6 +280,12 @@ class ValidatedSuite extends CatsSuite { } } + test("condNec consistent with Either.cond + toValidatedNec") { + forAll { (cond: Boolean, s: String, i: Int) => + Validated.condNec(cond, s, i) should === (Either.cond(cond, s, i).toValidatedNec) + } + } + test("liftTo consistent with direct to Option") { forAll { (v: Validated[Unit, Int]) => v.liftTo[Option] shouldBe v.toOption