From e45f93a099463b4f5173daf1b49fb54b3e854eea Mon Sep 17 00:00:00 2001 From: Denis Buzdalov Date: Wed, 26 Dec 2018 19:07:07 +0300 Subject: [PATCH] Simpler implementation of applicative effect for errors catching (#161) * Simpler implementation of applicative effect for errors catching * Simple implementation of the generalized `catchWrongs` was added This implementation does not handle multiple at once in case of S = List when several wrongs came from several `flatMap`'s, but catch all correctly when several wrongs came from `Traverse`. * Method for catching the first Wrong was implemented thru the general one. * Old catch implementation was aliased to `catchFirstWrong` and deprecated * Errors were made to be catched not as eager as they were before. This allows us to implement catch-last and catch-all behaviours correctly. * Syntax methods were added for newer functions, deprecation was corrected * Catch-all method was added taking `Nel[E] => Eff` handler. * Catch-last was added. * Deprecated method was replaced with its substitute in the spec. * Bunch of tests for multi-errors catch was added. * Continuation was made to be called in the applicative case of catch * Try to fix compatibility with scala 2.11. * Simplification of applicative catch, getting rid of coercion. * Getting rid of another coertion (in slightly unrelated place) * Versions in were updated in deprecations * Getting rid of unnecessary repetition. * One test's name was made to be cleaner * Smashing of all strings in the `catchAllWrongs` test has been moved out * Tests of catching using `Applicative` instance of `Eff` were added. * Multi-catching tests were reorganized, common parts were moved out. * Small typo was fixed. * One more small tests reorganization was done. * Errors in applicative block were renumbered to be the same with monadic * Multicatch tests were made to be two-level. * One typo was fixed. --- .../org/atnos/eff/ValidateEffectSpec.scala | 95 +++++++++++++++++-- .../scala/org/atnos/eff/ValidateEffect.scala | 59 ++++++++---- .../scala/org/atnos/eff/syntax/validate.scala | 14 ++- 3 files changed, 143 insertions(+), 25 deletions(-) diff --git a/jvm/src/test/scala/org/atnos/eff/ValidateEffectSpec.scala b/jvm/src/test/scala/org/atnos/eff/ValidateEffectSpec.scala index 050b0fdd..875cbda8 100644 --- a/jvm/src/test/scala/org/atnos/eff/ValidateEffectSpec.scala +++ b/jvm/src/test/scala/org/atnos/eff/ValidateEffectSpec.scala @@ -11,18 +11,32 @@ class ValidateEffectSpec extends Specification with ScalaCheck { def is = s2""" run the validate effect $validateOk run the validate effect with nothing $validateKo - run the validate effect (IorNel variant) $validateIorOk - run the validate effect with warnings $validateWarn - run the validate effect with warn & err $validateWarnAndErr - run the validate effect with errs & warn $validateWarnAndErr + `Ior`ish or warnings-oriented validation + run resulting IorNel $validateIorOk + run with warnings $validateWarn + run with warnings and errors $validateWarnAndErr + run with errors and warning $validateWarnAndErr recover from wrong values $catchWrongValues1 recover from wrong values and tell errors $catchWrongValues2 + recover from several, monadic + the first is catched ${ForCatchingEffMonadic.catchFirstWrongValue} + all are catched ${ForCatchingEffMonadic.catchAllWrongValues} + the last is catched ${ForCatchingEffMonadic.catchLastWrongValue} + + recover from several, applicative + the first is catched ${ForCatchingEffApplicative.catchFirstWrongValue} + all are catched ${ForCatchingEffApplicative.catchAllWrongValues} + the last is catched ${ForCatchingEffApplicative.catchLastWrongValue} + + recover, the whole list is catched $catchListOfWrongValues + run is stack safe with Validate $stacksafeRun """ type S = Fx.fx1[ValidateString] + type ValidateString[A] = Validate[String, A] def validateOk = { val validate: Eff[S, Int] = @@ -94,7 +108,7 @@ class ValidateEffectSpec extends Specification with ScalaCheck { def is = s2""" a <- EffMonad[S].pure(3) } yield a - validate.catchWrong((s: String) => pure(4)).runNel.run ==== Right(4) + validate.catchFirstWrong((s: String) => pure(4)).runNel.run ==== Right(4) } def catchWrongValues2 = { @@ -105,8 +119,8 @@ class ValidateEffectSpec extends Specification with ScalaCheck { def is = s2""" val handle: E => Check[Unit] = { case e => tell[Comput, E](e).as(()) } val comp1: Check[Int] = for { - _ <- wrong[Comput, E]("1").catchWrong(handle) - _ <- wrong[Comput, E]("2").catchWrong(handle) + _ <- wrong[Comput, E]("1").catchFirstWrong(handle) + _ <- wrong[Comput, E]("2").catchFirstWrong(handle) } yield 0 val comp2: Check[Int] = comp1 @@ -114,7 +128,72 @@ class ValidateEffectSpec extends Specification with ScalaCheck { def is = s2""" comp2.runNel.runWriter.run ==== ((Right(0), List("1", "2"))) } - type ValidateString[A] = Validate[String, A] + private def smashNelOfStrings[R](ss: NonEmptyList[String]): Eff[R, String] = pure(ss.mkString_("", ", ", "")) + + object ForCatchingEffMonadic { + val intermediate: Eff[S, Unit] = for { + _ <- ValidateEffect.wrong[S, String]("error1") + _ <- ValidateEffect.wrong[S, String]("error1.5") + } yield () + + val v: Eff[S, String] = + for { + _ <- ValidateEffect.correct[S, String, Int](1) + _ <- intermediate + a <- EffMonad[S].pure(3) + _ <- ValidateEffect.wrong[S, String]("error2") + } yield a.toString + + def catchFirstWrongValue = { + v.catchFirstWrong((s: String) => pure(s)).runNel.run ==== Right("error1") + } + + def catchAllWrongValues = { + v.catchAllWrongs(smashNelOfStrings).runNel.run ==== Right("error1, error1.5, error2") + } + + def catchLastWrongValue = { + v.catchLastWrong((s: String) => pure(s)).runNel.run ==== Right("error2") + } + } + + object ForCatchingEffApplicative { + val v1: Eff[S, Int] = ValidateEffect.validateValue(condition = true, 5, "no error") + val v2: Eff[S, String] = for { + x <- ValidateEffect.validateValue(condition = false, "str", "error1") + _ <- ValidateEffect.wrong("error1.5") + } yield x + val v3: Eff[S, Int] = ValidateEffect.validateValue(condition = false, 6, "error2") + + final case class Prod(x: Int, s: String, y: Int) + + val prod: Eff[S, Prod] = (v1, v2, v3).mapN(Prod) + val v: Eff[S, String] = prod.map(_.toString) + + def catchFirstWrongValue = { + v.catchFirstWrong((s: String) => pure(s)).runNel.run ==== Right("error1") + } + + def catchAllWrongValues = { + v.catchAllWrongs(smashNelOfStrings).runNel.run ==== Right("error1, error1.5, error2") + } + + def catchLastWrongValue = { + v.catchLastWrong((s: String) => pure(s)).runNel.run ==== Right("error2") + } + } + + def catchListOfWrongValues = { + type C = Fx.fx2[ValidateString, List] + val validate: Eff[C, String] = + for { + v <- ListEffect.values[C, Int](1, 2) + _ <- ValidateEffect.wrong[C, String]("error" + v.toString) + a <- EffMonad[C].pure(3) + } yield a.toString + + validate.runList.catchAllWrongs((ss: NonEmptyList[String]) => pure(ss.toList)).runNel.run ==== Right(List("error1", "error2")) + } def stacksafeRun = { val list = (1 to 5000).toList diff --git a/shared/src/main/scala/org/atnos/eff/ValidateEffect.scala b/shared/src/main/scala/org/atnos/eff/ValidateEffect.scala index eaec673f..721c48b7 100644 --- a/shared/src/main/scala/org/atnos/eff/ValidateEffect.scala +++ b/shared/src/main/scala/org/atnos/eff/ValidateEffect.scala @@ -121,38 +121,65 @@ trait ValidateInterpretation extends ValidateCreation { def onApplicativeEffect[X, T[_] : Traverse](xs: T[Validate[E, X]], continuation: Continuation[U, T[X], L SomeOr A]): Eff[U, L SomeOr A] = { l = xs.foldLeft(l)(combineLV) - Eff.impure(xs.map(_ => ().asInstanceOf[X]), continuation) + + val tx: T[X] = xs.map { case Correct() | Warning(_) | Wrong(_) => () } + Eff.impure(tx, continuation) } }) /** catch and handle possible wrong values */ - def catchWrong[R, E, A](effect: Eff[R, A])(handle: E => Eff[R, A])(implicit member: (Validate[E, ?]) <= R): Eff[R, A] = + def catchWrongs[R, E, A, S[_]: Applicative](effect: Eff[R, A])(handle: S[E] => Eff[R, A])(implicit member: Validate[E, ?] <= R, semi: Semigroup[S[E]]): Eff[R, A] = intercept(effect)(new Interpreter[Validate[E, ?], R, A, A] { - def onPure(a: A): Eff[R, A] = - Eff.pure(a) + private var errs: Option[S[E]] = None - def onEffect[X](m: Validate[E, X], continuation: Continuation[R, X, A]): Eff[R, A] = - m match { - case Correct() | Warning(_) => Eff.impure((), continuation) - case Wrong(e) => handle(e) + def onPure(a: A): Eff[R, A] = + errs.map(handle).getOrElse(Eff.pure(a)) + + def onEffect[X](m: Validate[E, X], continuation: Continuation[R, X, A]): Eff[R, A] = { + val x: X = m match { + case Correct() | Warning(_) => () + case Wrong(e) => { + errs = errs |+| Some(Applicative[S].pure(e)) + () + } } + Eff.impure(x, continuation) + } def onLastEffect[X](x: Validate[E, X], continuation: Continuation[R, X, Unit]): Eff[R, Unit] = continuation.runOnNone >> Eff.pure(()) def onApplicativeEffect[X, T[_]: Traverse](xs: T[Validate[E, X]], continuation: Continuation[R, T[X], A]): Eff[R, A] = { - val traversed: State[Option[E], T[X]] = xs.traverse { - case Correct() | Warning(_) => State[Option[E], X](state => (None, ())) - case Wrong(e) => State[Option[E], X](state => (Some(e), ())) + val (eo, tx): (Option[S[E]], T[X]) = xs.traverse { + case Correct() | Warning(_) => (None, ()) + case Wrong(e) => (Some(Applicative[S].pure(e)), ()) } - traversed.run(None).value match { - case (None, tx) => Eff.impure(tx, continuation) - case (Some(e), tx) => handle(e) - } + errs = errs |+| eo + Eff.impure(tx, continuation) } - }) + + /** catch and handle the first wrong value */ + def catchFirstWrong[R, E, A](effect: Eff[R, A])(handle: E => Eff[R, A])(implicit member: Validate[E, ?] <= R): Eff[R, A] = { + implicit val first: Semigroup[E] = Semigroup.instance{ (a, _) => a } + catchWrongs[R, E, A, Id](effect)(handle) + } + + /** catch and handle the last wrong value */ + def catchLastWrong[R, E, A](effect: Eff[R, A])(handle: E => Eff[R, A])(implicit member: Validate[E, ?] <= R): Eff[R, A] = { + implicit val last: Semigroup[E] = Semigroup.instance{ (_, b) => b } + catchWrongs[R, E, A, Id](effect)(handle) + } + + /** catch and handle all wrong values */ + def catchAllWrongs[R, E, A](effect: Eff[R, A])(handle: NonEmptyList[E] => Eff[R, A])(implicit member: Validate[E, ?] <= R): Eff[R, A] = + catchWrongs(effect)(handle) + + /** catch and handle possible wrong values */ + @deprecated("Use catchFirstWrong or more general catchWrongs instead", "5.4.2") + def catchWrong[R, E, A](effect: Eff[R, A])(handle: E => Eff[R, A])(implicit member: (Validate[E, ?]) <= R): Eff[R, A] = + catchFirstWrong(effect)(handle) } object ValidateInterpretation extends ValidateInterpretation diff --git a/shared/src/main/scala/org/atnos/eff/syntax/validate.scala b/shared/src/main/scala/org/atnos/eff/syntax/validate.scala index cc37efe1..8fed08b2 100644 --- a/shared/src/main/scala/org/atnos/eff/syntax/validate.scala +++ b/shared/src/main/scala/org/atnos/eff/syntax/validate.scala @@ -2,7 +2,7 @@ package org.atnos.eff.syntax import cats.data.{Ior, IorNel, NonEmptyList, ValidatedNel} import org.atnos.eff._ -import cats.Semigroup +import cats.{Applicative, Semigroup} object validate extends validate @@ -25,9 +25,21 @@ trait validate { def runIorNel[E](implicit m: Member[Validate[E, ?], R]): Eff[m.Out, E IorNel A] = ValidateInterpretation.runIorNel(e)(m.aux) + @deprecated("Use catchFirstWrong or more general catchWrongs instead", "5.4.2") def catchWrong[E](handle: E => Eff[R, A])(implicit m: Member[Validate[E, ?], R]): Eff[R, A] = ValidateInterpretation.catchWrong(e)(handle) + def catchWrongs[E, S[_]: Applicative](handle: S[E] => Eff[R, A])(implicit m: Member[Validate[E, ?], R], semi: Semigroup[S[E]]): Eff[R, A] = + ValidateInterpretation.catchWrongs(e)(handle) + + def catchFirstWrong[E](handle: E => Eff[R, A])(implicit m: Member[Validate[E, ?], R]): Eff[R, A] = + ValidateInterpretation.catchFirstWrong(e)(handle) + + def catchLastWrong[E](handle: E => Eff[R, A])(implicit m: Member[Validate[E, ?], R]): Eff[R, A] = + ValidateInterpretation.catchLastWrong(e)(handle) + + def catchAllWrongs[E](handle: NonEmptyList[E] => Eff[R, A])(implicit m: Member[Validate[E, ?], R]): Eff[R, A] = + ValidateInterpretation.catchAllWrongs(e)(handle) } }