Skip to content

Commit

Permalink
Simpler implementation of applicative effect for errors catching (#161)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
buzden authored and etorreborre committed Dec 26, 2018
1 parent 6360517 commit e45f93a
Show file tree
Hide file tree
Showing 3 changed files with 143 additions and 25 deletions.
95 changes: 87 additions & 8 deletions jvm/src/test/scala/org/atnos/eff/ValidateEffectSpec.scala
Expand Up @@ -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] =
Expand Down Expand Up @@ -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 = {
Expand All @@ -105,16 +119,81 @@ 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

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
Expand Down
59 changes: 43 additions & 16 deletions shared/src/main/scala/org/atnos/eff/ValidateEffect.scala
Expand Up @@ -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
Expand Down
14 changes: 13 additions & 1 deletion shared/src/main/scala/org/atnos/eff/syntax/validate.scala
Expand Up @@ -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

Expand All @@ -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)
}

}

0 comments on commit e45f93a

Please sign in to comment.