Skip to content
38 changes: 38 additions & 0 deletions core/src/main/scala/cats/ErrorControl.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package cats

import cats.data.EitherT

trait ErrorControl[F[_], G[_], E] extends Serializable {
Copy link
Contributor

Choose a reason for hiding this comment

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

I like to use abstract types as "output types". It seems to me we might want G[_] to be an abstract type here, since we might often want to write algorithms to work in F go through G, but return an F. Then we could accept an implicit ErrorControl[F, E] to do that, which might be nice.

Copy link
Member Author

Choose a reason for hiding this comment

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

I like that as well, see my recent issue in parallel, though I'm not 100% sure if it applies here, have to think about it more.

Copy link
Member Author

Choose a reason for hiding this comment

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

val monadErrorF: MonadError[F, E]
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't like using abstract vals on traits since it invites the val init order bug. Can we make these def?

val monadG: Monad[G]

def controlError[A](fa: F[A])(f: E => G[A]): G[A]

Copy link
Contributor

Choose a reason for hiding this comment

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

seems like a def control[A,B](fa: F[A])(f: Either[E, A] => G[B]): G[B] might be a nice function to have. It is more general than controlError and can also be used to implement trial. I think users might often want this variant personally.

The name might not be great, but I would love to see this and have syntax for it.

def accept[A](ga: G[A]): F[A]

def trial[A](fa: F[A]): G[Either[E, A]] =
intercept(monadErrorF.map(fa)(Right(_): Either[E, A]))(Left(_))

def trialT[A](fa: F[A]): EitherT[G, E, A] =
EitherT(trial(fa))

def intercept[A](fa: F[A])(f: E => A): G[A] =
controlError(fa)(f andThen monadG.pure)

def absolve[A](gea: G[Either[E, A]]): F[A] =
monadErrorF.flatMap(accept(gea))(_.fold(monadErrorF.raiseError, monadErrorF.pure))

def assure[A](ga: G[A])(error: => E)(predicate: A => Boolean): F[A] =
assureOr(ga)(_ => error)(predicate)

def assureOr[A](ga: G[A])(error: A => E)(predicate: A => Boolean): F[A] =
Copy link
Contributor

Choose a reason for hiding this comment

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

seems to me A => Option[E] rather than two functions is a nicer way to go. We also only have one invocation. using two functions in one call is usually not super ergonomic in scala.

Copy link
Member Author

@LukaJCB LukaJCB Apr 19, 2018

Choose a reason for hiding this comment

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

I think we could just get rid of what's currently assure, as assureOr covers all the use cases. I just kept both for the sake of symmetry in ensure and ensureOr. I'd rather just have a single function assure that takes A => E like in the blog post.

monadErrorF.flatMap(accept(ga))(a =>
if (predicate(a)) monadErrorF.pure(a) else monadErrorF.raiseError(error(a)))

}

object ErrorControl {

def apply[F[_], G[_], E](implicit ev: ErrorControl[F, G, E]): ErrorControl[F, G, E] = ev

}
16 changes: 16 additions & 0 deletions core/src/main/scala/cats/data/EitherT.scala
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,22 @@ private[data] abstract class EitherTInstances extends EitherTInstances1 {
val F0: Order[F[Either[L, R]]] = F
}

implicit def catsErrorControlForEitherT[F[_]: Monad, E]: ErrorControl[EitherT[F, E, ?], F, E] =
new ErrorControl[EitherT[F, E, ?], F, E] {
val monadErrorF: MonadError[EitherT[F, E, ?], E] = EitherT.catsDataMonadErrorForEitherT
val monadG: Monad[F] = Monad[F]

def controlError[A](fa: EitherT[F, E, A])(f: E => F[A]): F[A] =
Monad[F].flatMap(fa.value) {
case Left(e) => f(e)
case Right(a) => monadG.pure(a)
}

def accept[A](ga: F[A]): EitherT[F, E, A] =
EitherT.liftF(ga)

}

implicit def catsDataShowForEitherT[F[_], L, R](implicit sh: Show[F[Either[L, R]]]): Show[EitherT[F, L, R]] =
Contravariant[Show].contramap(sh)(_.value)

Expand Down
18 changes: 17 additions & 1 deletion core/src/main/scala/cats/data/IndexedStateT.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package cats
package data

import cats.arrow.{Profunctor, Strong}

import cats.syntax.either._

/**
Expand Down Expand Up @@ -247,6 +246,23 @@ private[data] sealed abstract class IndexedStateTInstances extends IndexedStateT
implicit def catsDataAlternativeForIndexedStateT[F[_], S](implicit FM: Monad[F],
FA: Alternative[F]): Alternative[IndexedStateT[F, S, S, ?]] with Monad[IndexedStateT[F, S, S, ?]] =
new IndexedStateTAlternative[F, S] { implicit def F = FM; implicit def G = FA }

implicit def catsErrorControlForStateT[F[_], G[_], S, E]
(implicit E: ErrorControl[F, G, E], M: Monad[G]): ErrorControl[StateT[F, S, ?], StateT[G, S, ?], E] =
Copy link
Contributor

Choose a reason for hiding this comment

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

M: Monad[G] no longer required since it's provided by ErrorControl right?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yep, will fix, this was needed when G was an Applicative so that StateT[G, S, A] could be an Applicative

new ErrorControl[StateT[F, S, ?], StateT[G, S, ?], E] {
implicit val F: MonadError[F, E] = E.monadErrorF

val monadErrorF: MonadError[StateT[F, S, ?], E] = IndexedStateT.catsDataMonadErrorForIndexedStateT
val monadG: Monad[StateT[G, S, ?]] = IndexedStateT.catsDataMonadForIndexedStateT(M)

def accept[A](ga: StateT[G, S, A]): StateT[F, S, A] = ga.mapK(new (G ~> F) {
def apply[T](ga: G[T]): F[T] = E.accept(ga)
})

def controlError[A](fa: StateT[F, S, A])(f: E => StateT[G, S, A]): StateT[G, S, A] =
IndexedStateT(s => E.controlError(fa.run(s))(e => f(e).run(s)))

}
}

private[data] sealed abstract class IndexedStateTInstances1 extends IndexedStateTInstances2 {
Expand Down
18 changes: 18 additions & 0 deletions core/src/main/scala/cats/data/Kleisli.scala
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,24 @@ private[data] sealed abstract class KleisliInstances extends KleisliInstances0 {
new KleisliArrowChoice[F] {
def F: Monad[F] = M
}

implicit def catsErrorControlForKleisli[F[_], G[_], R, E]
(implicit E: ErrorControl[F, G, E]): ErrorControl[Kleisli[F, R, ?], Kleisli[G, R, ?], E] =
new ErrorControl[Kleisli[F, R, ?], Kleisli[G, R, ?], E] {
implicit val F: MonadError[F, E] = E.monadErrorF
implicit val G: Monad[G] = E.monadG

val monadErrorF: MonadError[Kleisli[F, R, ?], E] = Kleisli.catsDataMonadErrorForKleisli
val monadG: Monad[Kleisli[G, R, ?]] = Kleisli.catsDataMonadForKleisli

def accept[A](ga: Kleisli[G, R, A]): Kleisli[F, R, A] = ga.mapK(new (G ~> F) {
def apply[T](ga: G[T]): F[T] = E.accept(ga)
})

def controlError[A](fa: Kleisli[F, R, A])(f: E => Kleisli[G, R, A]): Kleisli[G, R, A] =
Kleisli(r => E.controlError(fa.run(r))(e => f(e).run(r)))

}
}

private[data] sealed abstract class KleisliInstances0 extends KleisliInstances1 {
Expand Down
31 changes: 31 additions & 0 deletions core/src/main/scala/cats/data/OptionT.scala
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,22 @@ private[data] sealed abstract class OptionTInstances extends OptionTInstances0 {
implicit def catsDataMonadForOptionT[F[_]](implicit F0: Monad[F]): Monad[OptionT[F, ?]] =
new OptionTMonad[F] { implicit val F = F0 }

implicit def catsEndeavorForOptionT[F[_]: Monad, E]: ErrorControl[OptionT[F, ?], F, Unit] =
Copy link
Contributor

Choose a reason for hiding this comment

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

should this be catsErrorControlForOptionT?

new ErrorControl[OptionT[F, ?], F, Unit] {
val monadErrorF: MonadError[OptionT[F, ?], Unit] = catsDataMonadErrorUnitForOptionT
val monadG: Monad[F] = Monad[F]

def controlError[A](fa: OptionT[F, A])(f: Unit => F[A]): F[A] =
Monad[F].flatMap(fa.value) {
case Some(a) => monadG.pure(a)
case None => f(())
}

def accept[A](ga: F[A]): OptionT[F, A] =
OptionT.liftF(ga)

}

implicit def catsDataFoldableForOptionT[F[_]](implicit F0: Foldable[F]): Foldable[OptionT[F, ?]] =
new OptionTFoldable[F] { implicit val F = F0 }

Expand Down Expand Up @@ -249,6 +265,9 @@ private[data] sealed abstract class OptionTInstances0 extends OptionTInstances1

private[data] sealed abstract class OptionTInstances1 extends OptionTInstances2 {

implicit def catsDataMonadErrorUnitForOptionT[F[_]](implicit F0: Monad[F]): MonadError[OptionT[F, ?], Unit] =
new OptionTMonadErrorUnit[F] { implicit val F = F0 }

implicit def catsDataMonoidKForOptionT[F[_]](implicit F0: Monad[F]): MonoidK[OptionT[F, ?]] =
new OptionTMonoidK[F] { implicit val F = F0 }

Expand Down Expand Up @@ -297,6 +316,18 @@ private trait OptionTMonadError[F[_], E] extends MonadError[OptionT[F, ?], E] wi
OptionT(F.handleErrorWith(fa.value)(f(_).value))
}

private trait OptionTMonadErrorUnit[F[_]] extends MonadError[OptionT[F, ?], Unit] with OptionTMonad[F] {
implicit def F: Monad[F]

def raiseError[A](e: Unit): OptionT[F, A] = OptionT.none

def handleErrorWith[A](fa: OptionT[F, A])(f: Unit => OptionT[F, A]): OptionT[F, A] =
OptionT(F.flatMap(fa.value) {
case s @ Some(_) => F.pure(s)
case None => f(()).value
})
}

private trait OptionTContravariantMonoidal[F[_]] extends ContravariantMonoidal[OptionT[F, ?]] {
def F: ContravariantMonoidal[F]

Expand Down
18 changes: 18 additions & 0 deletions core/src/main/scala/cats/data/WriterT.scala
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,24 @@ private[data] sealed abstract class WriterTInstances extends WriterTInstances0 {
implicit val L0: Monoid[L] = L
}

implicit def catsErrorControlForWriterT[F[_], G[_], L: Monoid, E]
(implicit M: ErrorControl[F, G, E]): ErrorControl[WriterT[F, L, ?], WriterT[G, L, ?], E] =
new ErrorControl[WriterT[F, L, ?], WriterT[G, L, ?], E] {
implicit val F: MonadError[F, E] = M.monadErrorF
implicit val G: Monad[G] = M.monadG

val monadErrorF: MonadError[WriterT[F, L, ?], E] = WriterT.catsDataMonadErrorForWriterT
val monadG: Monad[WriterT[G, L, ?]] = WriterT.catsDataMonadForWriterT

def accept[A](ga: WriterT[G, L, A]): WriterT[F, L, A] = ga.mapK(new (G ~> F) {
def apply[T](ga: G[T]): F[T] = M.accept(ga)
})

def controlError[A](fa: WriterT[F, L, A])(f: E => WriterT[G, L, A]): WriterT[G, L, A] =
WriterT(M.controlError(fa.run)(e => f(e).run))

}

implicit def catsDataTraverseForWriterTId[L](implicit F: Traverse[Id]): Traverse[WriterT[Id, L, ?]] =
catsDataTraverseForWriterT[Id, L](F)
}
Expand Down
8 changes: 8 additions & 0 deletions core/src/main/scala/cats/instances/all.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package cats
package instances

abstract class AllInstancesBinCompat
extends AllInstances
with AllInstancesBinCompat0

trait AllInstances
extends AnyValInstances
with BigIntInstances
Expand Down Expand Up @@ -32,3 +36,7 @@ trait AllInstances
with TupleInstances
with UUIDInstances
with VectorInstances

trait AllInstancesBinCompat0
extends OptionInstancesExtension
with EitherInstancesExtension
19 changes: 19 additions & 0 deletions core/src/main/scala/cats/instances/either.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package instances

import cats.syntax.EitherUtil
import cats.syntax.either._

import scala.annotation.tailrec

trait EitherInstances extends cats.kernel.instances.EitherInstances {
Expand Down Expand Up @@ -155,3 +156,21 @@ trait EitherInstances extends cats.kernel.instances.EitherInstances {
}
}
}

/**
* Extension to Either instances in a binary compat way
*/
trait EitherInstancesExtension {
implicit def catsErrorControlForEither[E]: ErrorControl[Either[E, ?], Id, E] =
new ErrorControl[Either[E, ?], Id, E] {
val monadErrorF: MonadError[Either[E, ?], E] = cats.instances.either.catsStdInstancesForEither
val monadG: Monad[Id] = cats.catsInstancesForId

def controlError[A](fa: Either[E, A])(f: E => A): A = fa match {
case Left(e) => f(e)
case Right(a) => a
}

def accept[A](ga: A): Either[E, A] = Right(ga)
}
}
18 changes: 18 additions & 0 deletions core/src/main/scala/cats/instances/option.scala
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,21 @@ trait OptionInstances extends cats.kernel.instances.OptionInstances {
}
}
}

/**
* Extension to Option instances in a binary compat way
*/
trait OptionInstancesExtension {
implicit val catsStdErrorControlForOption: ErrorControl[Option, Id, Unit] =
new ErrorControl[Option, Id, Unit] {
val monadErrorF: MonadError[Option, Unit] = cats.instances.option.catsStdInstancesForOption
val monadG: Monad[Id] = cats.catsInstancesForId

def controlError[A](fa: Option[A])(f: Unit => A): A = fa match {
case Some(a) => a
case None => f(())
}

def accept[A](ga: A): Option[A] = Some(ga)
}
}
2 changes: 1 addition & 1 deletion core/src/main/scala/cats/instances/package.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package cats

package object instances {
object all extends AllInstances
object all extends AllInstancesBinCompat
object bigInt extends BigIntInstances
object bigDecimal extends BigDecimalInstances
object bitSet extends BitSetInstances
Expand Down
4 changes: 4 additions & 0 deletions core/src/main/scala/cats/syntax/all.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package syntax
abstract class AllSyntaxBinCompat
extends AllSyntax
with AllSyntaxBinCompat0
with AllSyntaxBinCompat1

trait AllSyntax
extends AlternativeSyntax
Expand Down Expand Up @@ -57,3 +58,6 @@ trait AllSyntaxBinCompat0
extends UnorderedTraverseSyntax
with ApplicativeErrorExtension
with TrySyntax

trait AllSyntaxBinCompat1
extends ErrorControlSyntax
68 changes: 68 additions & 0 deletions core/src/main/scala/cats/syntax/errorControl.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package cats
package syntax

import cats.data.EitherT


trait ErrorControlSyntax {
implicit final def catsSyntaxErrorControlF[F[_], E, A]
(fa: F[A])(implicit F: MonadError[F, E]): ErrorControlFOps[F, E, A] =
new ErrorControlFOps[F, E, A](fa)

implicit final def catsSyntaxErrorControlG[G[_], A]
(ga: G[A])(implicit G: Applicative[G]): ErrorControlGOps[G, A] =
new ErrorControlGOps[G, A](ga)

implicit final def catsSyntaxErrorControlEither[G[_], E, A]
(gea: G[Either[E, A]])(implicit G: Applicative[G]): ErrorControlEitherOps[G, E, A] =
new ErrorControlEitherOps[G, E, A](gea)

}


final class ErrorControlFOps[F[_], E, A](val fa: F[A]) extends AnyVal {

def controlError[G[_]](f: E => G[A])(implicit E: ErrorControl[F, G, E]): G[A] =
E.controlError(fa)(f)

def trial[G[_]](implicit E: ErrorControl[F, G, E]): G[Either[E, A]] =
E.trial(fa)

def trialT[G[_]](implicit E: ErrorControl[F, G, E]): EitherT[G, E, A] =
E.trialT(fa)

def intercept[G[_]](f: E => A)(implicit E: ErrorControl[F, G, E]): G[A] =
E.intercept(fa)(f)
}

final class ErrorControlGOps[G[_], A](val ga: G[A]) extends AnyVal {
def assure[F[_]]: AssurePartiallyApplied[F, G, A] = new AssurePartiallyApplied[F, G, A](ga)

def assureOr[F[_]]: AssureOrPartiallyApplied[F, G, A] = new AssureOrPartiallyApplied[F, G, A](ga)

def accept[F[_]]: AcceptPartiallyApplied[F, G, A] = new AcceptPartiallyApplied[F, G, A](ga)
}

final class ErrorControlEitherOps[G[_], E, A](val gea: G[Either[E, A]]) extends AnyVal {
def absolve[F[_]](implicit E: ErrorControl[F, G, E]): F[A] =
E.absolve(gea)
}

private[syntax] final class AssurePartiallyApplied[F[_], G[_], A](val ga: G[A]) extends AnyVal {
def apply[E](error: => E)
(predicate: A => Boolean)
(implicit E: ErrorControl[F, G, E]): F[A] =
E.assure(ga)(error)(predicate)
}

private[syntax] final class AssureOrPartiallyApplied[F[_], G[_], A](val ga: G[A]) extends AnyVal {
def apply[E](error: A => E)
(predicate: A => Boolean)
(implicit E: ErrorControl[F, G, E]): F[A] =
E.assureOr(ga)(error)(predicate)
}

private[syntax] final class AcceptPartiallyApplied[F[_], G[_], A](val ga: G[A]) extends AnyVal {
def apply[E](implicit E: ErrorControl[F, G, E]): F[A] =
E.accept(ga)
}
Loading