Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ensureWith to Validated and Either (#1550) #1612

Merged
merged 12 commits into from
May 16, 2017
6 changes: 6 additions & 0 deletions core/src/main/scala/cats/MonadError.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ trait MonadError[F[_], E] extends ApplicativeError[F, E] with Monad[F] {
def ensure[A](fa: F[A])(error: => E)(predicate: A => Boolean): F[A] =
flatMap(fa)(a => if (predicate(a)) pure(a) else raiseError(error))

/**
* Turns a successful value into an error specified by the `error` function if it does not satisfy a given predicate.
*/
def ensureOr[A](fa: F[A])(error: A => E)(predicate: A => Boolean): F[A] =
flatMap(fa)(a => if (predicate(a)) pure(a) else raiseError(error(a)))

}

object MonadError {
Expand Down
12 changes: 11 additions & 1 deletion core/src/main/scala/cats/data/EitherT.scala
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ final case class EitherT[F[_], A, B](value: F[Either[A, B]]) {
def ensure[AA >: A](onFailure: => AA)(f: B => Boolean)(implicit F: Functor[F]): EitherT[F, AA, B] =
EitherT(F.map(value)(_.ensure(onFailure)(f)))

def ensureOr[AA >: A](onFailure: B => AA)(f: B => Boolean)(implicit F: Functor[F]): EitherT[F, AA, B] =
EitherT(F.map(value)(_.ensureOr(onFailure)(f)))

def toOption(implicit F: Functor[F]): OptionT[F, B] = OptionT(F.map(value)(_.toOption))

def to[G[_]](implicit F: Functor[F], G: Alternative[G]): F[G[B]] =
Expand Down Expand Up @@ -465,7 +468,14 @@ private[data] abstract class EitherTInstances1 extends EitherTInstances2 {

private[data] abstract class EitherTInstances2 extends EitherTInstances3 {
implicit def catsDataMonadErrorForEitherT[F[_], L](implicit F0: Monad[F]): MonadError[EitherT[F, L, ?], L] =
new EitherTMonadError[F, L] { implicit val F = F0 }
new EitherTMonadError[F, L] {
implicit val F = F0
override def ensure[A](fa: EitherT[F, L, A])(error: => L)(predicate: (A) => Boolean): EitherT[F, L, A] =
fa.ensure(error)(predicate)(F)

override def ensureOr[A](fa: EitherT[F, L, A])(error: (A) => L)(predicate: (A) => Boolean): EitherT[F, L, A] =
fa.ensureOr(error)(predicate)(F)
}

implicit def catsDataSemigroupKForEitherT[F[_], L](implicit F0: Monad[F]): SemigroupK[EitherT[F, L, ?]] =
new EitherTSemigroupK[F, L] { implicit val F = F0 }
Expand Down
14 changes: 14 additions & 0 deletions core/src/main/scala/cats/data/Validated.scala
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,20 @@ sealed abstract class Validated[+E, +A] extends Product with Serializable {
*/
def ensure[EE >: E](onFailure: => EE)(f: A => Boolean): Validated[EE, A] =
fold(_ => this, a => if (f(a)) this else Validated.invalid(onFailure))

/**
* Ensure that a successful result passes the given predicate,
* falling back to the an Invalid of the result of `onFailure` if the predicate
* returns false.
*
* For example:
* {{{
* scala> Validated.valid("ab").ensureOr(s => new IllegalArgumentException("Must be longer than 3, provided '" + s + "'"))(_.length > 3)
* res0: Validated[IllegalArgumentException, String] = Invalid(java.lang.IllegalArgumentException: Must be longer than 3, provided 'ab')
* }}}
*/
def ensureOr[EE >: E](onFailure: A => EE)(f: A => Boolean): Validated[EE, A] =
fold(_ => this, a => if (f(a)) this else Validated.invalid(onFailure(a)))
}

object Validated extends ValidatedInstances with ValidatedFunctions{
Expand Down
3 changes: 3 additions & 0 deletions core/src/main/scala/cats/instances/either.scala
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ trait EitherInstances extends cats.kernel.instances.EitherInstances {
override def ensure[B](fab: Either[A, B])(error: => A)(predicate: B => Boolean): Either[A, B] =
fab.ensure(error)(predicate)

override def ensureOr[B](fab: Either[A, B])(error: B => A)(predicate: B => Boolean): Either[A, B] =
fab.ensureOr(error)(predicate)

override def reduceLeftToOption[B, C](fab: Either[A, B])(f: B => C)(g: (C, B) => C): Option[C] =
fab.right.map(f).toOption

Expand Down
5 changes: 5 additions & 0 deletions core/src/main/scala/cats/syntax/either.scala
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ final class EitherOps[A, B](val eab: Either[A, B]) extends AnyVal {
case Right(b) => if (f(b)) eab else Left(onFailure)
}

def ensureOr[AA >: A](onFailure: B => AA)(f: B => Boolean): Either[AA, B] = eab match {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this is following a precedent, but I find myself wondering if this Either syntax should really be providing syntax for methods that should already be available via type class syntax (in this case MonadError). Let's table that for a separate issue though.

case Left(_) => eab
case Right(b) => if (f(b)) eab else Left(onFailure(b))
}

def toIor: A Ior B = eab match {
case Left(a) => Ior.left(a)
case Right(b) => Ior.right(b)
Expand Down
3 changes: 3 additions & 0 deletions core/src/main/scala/cats/syntax/monadError.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,7 @@ trait MonadErrorSyntax {
final class MonadErrorOps[F[_], E, A](val fa: F[A]) extends AnyVal {
def ensure(error: => E)(predicate: A => Boolean)(implicit F: MonadError[F, E]): F[A] =
F.ensure(fa)(error)(predicate)

def ensureOr(error: A => E)(predicate: A => Boolean)(implicit F: MonadError[F, E]): F[A] =
F.ensureOr(fa)(error)(predicate)
}
6 changes: 6 additions & 0 deletions laws/src/main/scala/cats/laws/MonadErrorLaws.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ trait MonadErrorLaws[F[_], E] extends ApplicativeErrorLaws[F, E] with MonadLaws[

def monadErrorLeftZero[A, B](e: E, f: A => F[B]): IsEq[F[B]] =
F.flatMap(F.raiseError[A](e))(f) <-> F.raiseError[B](e)

def monadErrorEnsureConsistency[A](fa: F[A], e: E, p: A => Boolean): IsEq[F[A]] =
F.ensure(fa)(e)(p) <-> F.flatMap(fa)(a => if (p(a)) F.pure(a) else F.raiseError(e))

def monadErrorEnsureOrConsistency[A](fa: F[A], e: A => E, p: A => Boolean): IsEq[F[A]] =
F.ensureOr(fa)(e)(p) <-> F.flatMap(fa)(a => if (p(a)) F.pure(a) else F.raiseError(e(a)))
}

object MonadErrorLaws {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ trait MonadErrorTests[F[_], E] extends ApplicativeErrorTests[F, E] with MonadTes
def bases: Seq[(String, RuleSet)] = Nil
def parents: Seq[RuleSet] = Seq(applicativeError[A, B, C], monad[A, B, C])
def props: Seq[(String, Prop)] = Seq(
"monadError left zero" -> forAll(laws.monadErrorLeftZero[A, B] _)
"monadError left zero" -> forAll(laws.monadErrorLeftZero[A, B] _),
"monadError ensure consistency" -> forAll(laws.monadErrorEnsureConsistency[A] _),
"monadError ensureOr consistency" -> forAll(laws.monadErrorEnsureOrConsistency[A] _)
)
}
}
Expand Down
24 changes: 0 additions & 24 deletions tests/src/test/scala/cats/tests/EitherTTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -354,28 +354,4 @@ class EitherTTests extends CatsSuite {
x.value.map(_.right.toOption) should === (x.toOption.value)
}
}

test("ensure on left is identity") {
forAll { (x: EitherT[Id, String, Int], s: String, p: Int => Boolean) =>
if (x.isLeft) {
x.ensure(s)(p) should === (x)
}
}
}

test("ensure on right is identity if predicate satisfied") {
forAll { (x: EitherT[Id, String, Int], s: String, p: Int => Boolean) =>
if (x.isRight && p(x getOrElse 0)) {
x.ensure(s)(p) should === (x)
}
}
}

test("ensure should fail if predicate not satisfied") {
forAll { (x: EitherT[Id, String, Int], s: String, p: Int => Boolean) =>
if (x.isRight && !p(x getOrElse 0)) {
x.ensure(s)(p) should === (EitherT.leftT[Id, Int](s))
}
}
}
}
8 changes: 0 additions & 8 deletions tests/src/test/scala/cats/tests/EitherTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -200,14 +200,6 @@ class EitherTests extends CatsSuite {
}
}

test("ensure on left is identity") {
forAll { (x: Either[Int, String], i: Int, p: String => Boolean) =>
if (x.isLeft) {
x.ensure(i)(p) should === (x)
}
}
}

test("toIor then toEither is identity") {
forAll { (x: Either[Int, String]) =>
x.toIor.toEither should === (x)
Expand Down
14 changes: 14 additions & 0 deletions tests/src/test/scala/cats/tests/MonadErrorSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,18 @@ class MonadErrorSuite extends CatsSuite {
failed.ensure(())(i => true) should === (failed)
}

test("ensureOr raises an error if the predicate fails") {
successful.ensureOr(_ => ())(_ => false) should === (None)
}

test("ensureOr returns the successful value if the predicate succeeds") {
successful.ensureOr(_ => ())(_ => true) should === (successful)
}

test("ensureOr returns the failure, when applied to a failure") {
failed.ensureOr(_ => ())(_ => false) should === (failed)
failed.ensureOr(_ => ())(_ => true) should === (failed)
}


}
16 changes: 16 additions & 0 deletions tests/src/test/scala/cats/tests/ValidatedTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -248,4 +248,20 @@ class ValidatedTests extends CatsSuite {
}
}
}

test("ensureOr on Invalid is identity") {
forAll { (x: Validated[Int,String], f: String => Int, p: String => Boolean) =>
if (x.isInvalid) {
x.ensureOr(f)(p) should === (x)
}
}
}

test("ensureOr should fail if predicate not satisfied") {
forAll { (x: Validated[String, Int], f: Int => String, p: Int => Boolean) =>
if (x.exists(!p(_))) {
x.ensureOr(f)(p).isInvalid shouldBe true
}
}
}
}