ApplicativeError

ApplicativeError is the typeclass used to explicitly represent errors during independent computations. It is parametrized to an error type E, which means the datatype has at least a “success” and a “failure” version.

These errors can come in the form of Throwable, Exception, or any other type that is more relevant to the domain; a sealed class UserNotFoundReason that contains three inheritors, for example.

A datatype like Either<E, A> allows for the user to apply their error type of choice.

Main Combinators

ApplicativeError inherits all the combinators available in Applicative. It also adds several of its own.

raiseError

A constructor function. It lifts an exception into the computational context of a type constructor.

import arrow.*
import arrow.core.*
import arrow.core.extensions.either.applicativeError.*

Either.applicativeError<Throwable>().raiseError<Int>(RuntimeException("Paco"))
// Either.Left(java.lang.RuntimeException: Paco)

Kind<F, A>#handleErrorWith

This method requires a function that creates a new datatype from an error, (E) -> Kind<F, A>. This function is used as a catch + recover clause for the current instance, allowing it to return a new computation after a failure.

If Monad has flatMap to allow mapping the value inside a successful datatype into a new datatype, you can think of handleErrorWith as a way that allows you to map the value of a *failed datatype into a new datatype.

import arrow.core.handleErrorWith

val success: Either<Throwable, Int> = Either.Right(1)

success.handleErrorWith { t -> Either.Right(0) }
// Either.Right(1)
val failure: Either<Throwable, Int> = Either.Left(RuntimeException("Boom!"))

failure.handleErrorWith { t -> Either.Right(0) }
// Either.Right(0)

Kind<F, A>#handleError

Similar to handleErrorWith, except the function can return any regular value. This value will be wrapped and used as a return.

success.handleError { t -> 0 }
// Either.Right(1)
failure.handleError { t -> 0 }
// Either.Right(0)

Kind<F, A>#attempt

Maps the current content of the datatype to an Either<E, A>, recovering from any previous error state.

import arrow.core.Either
import arrow.core.Validated
import arrow.core.valid

//sampleStart
val validation: Validated<String, Int> = 3.valid()
val result = validation.attempt()
//sampleEnd

suspend fun main() {
  println(result)
}
import arrow.core.Either
import arrow.core.Validated
import arrow.core.invalid

//sampleStart
val validation: Validated<String, Int> = "nope".invalid()
val result = validation.attempt()
//sampleEnd

suspend fun main() {
  println(result)
}

fromEither/fromOption

Constructor function from an Either<E, A> or Option<A> to the current datatype.

While fromOption() requires creating a new error value.

Either.applicativeError<Throwable>().run { Some(1).fromOption { RuntimeException("Boom") } }
// Either.Right(1)

In the case of fromEither(), converting from the error type of the Either<EE, A> to the type of the ApplicativeError<F, E> is required.

import arrow.core.Either
import arrow.core.Right
import arrow.core.Validated

//sampleStart
val e: Either<String, Int> = Right(1)
val result = Validated.fromEither(e)
//sampleEnd

suspend fun main() {
  println(result)
}
import arrow.core.Either
import arrow.core.Left
import arrow.core.Validated

//sampleStart
val e: Either<String, Int> = Left("oops")
val result = Validated.fromEither(e)
//sampleEnd

suspend fun main() {
  println(result)
}

catch

Constructor function. It takes two function parameters. The first is a generator function from () -> A. The second is an error mapping function from (Throwable) -> E. catch() runs the generator function to generate a success datatype, and if it throws an exception, it uses the error mapping function to create a new failure datatype.

val eitherAE = Either.applicativeError<Throwable>()

eitherAE.catch(::identity) { 1 }
// Either.Right(1)
eitherAE.catch(::identity) { throw RuntimeException("Boom") }
// Either.Left(java.lang.RuntimeException: Boom)

Laws

Arrow provides ApplicativeErrorLaws in the form of test cases for internal verification of lawful instances and third party apps creating their own ApplicativeError instances.

Example : Alternative validation strategies using ApplicativeError

In this validation example, we demonstrate how we can use ApplicativeError instead of Validated to abstract away validation strategies and raise errors in the context we are computing in.

Model

import arrow.*
import arrow.core.*
import arrow.typeclasses.*

sealed class ValidationError(val msg: String) {
  data class DoesNotContain(val value: String) : ValidationError("Did not contain $value")
  data class MaxLength(val value: Int) : ValidationError("Exceeded length of $value")
  data class NotAnEmail(val reasons: Nel<ValidationError>) : ValidationError("Not a valid email")
}

data class FormField(val label: String, val value: String)
data class Email(val value: String)

Rules

sealed class Rules<F>(A: ApplicativeError<F, Nel<ValidationError>>) : ApplicativeError<F, Nel<ValidationError>> by A {

  private fun FormField.contains(needle: String): Kind<F, FormField> =
    if (value.contains(needle, false)) just(this)
    else raiseError(ValidationError.DoesNotContain(needle).nel())

  private fun FormField.maxLength(maxLength: Int): Kind<F, FormField> =
    if (value.length <= maxLength) just(this)
    else raiseError(ValidationError.MaxLength(maxLength).nel())

  fun FormField.validateEmail(): Kind<F, Email> =
    map(contains("@"), maxLength(250), {
      Email(value)
    }).handleErrorWith { raiseError(ValidationError.NotAnEmail(it).nel()) }

  object ErrorAccumulationStrategy :
    Rules<ValidatedPartialOf<Nel<ValidationError>>>(Validated.applicativeError(NonEmptyList.semigroup()))

  object FailFastStrategy :
    Rules<EitherPartialOf<Nel<ValidationError>>>(Either.applicativeError())

  companion object {
    infix fun <A> failFast(f: FailFastStrategy.() -> A): A = f(FailFastStrategy)
    infix fun <A> accumulateErrors(f: ErrorAccumulationStrategy.() -> A): A = f(ErrorAccumulationStrategy)
  }

}

Rules defines abstract behaviors that can be composed and have access to the scope of ApplicativeError where we can invoke just to lift values into the positive result and raiseError into the error context.

Once we have such abstract algebra defined, we can simply materialize it to data types that support different error strategies:

Error accumulation

Rules accumulateErrors {
  listOf(
    FormField("Invalid Email Domain Label", "nowhere.com"),
    FormField("Too Long Email Label", "nowheretoolong${(0..251).map { "g" }}"), //this accumulates N errors
    FormField("Valid Email Label", "getlost@nowhere.com")
  ).map { it.validateEmail() }
}

Fail Fast

Rules failFast {
  listOf(
    FormField("Invalid Email Domain Label", "nowhere.com"),
    FormField("Too Long Email Label", "nowheretoolong${(0..251).map { "g" }}"), //this fails fast
    FormField("Valid Email Label", "getlost@nowhere.com")
  ).map { it.validateEmail() }
}

Do you like Arrow?

Arrow Org
<