All Downloads are FREE. Search and download functionalities are using the official Maven repository.

app.cash.quiver.Outcome.kt Maven / Gradle / Ivy

Go to download

Quiver library providing extension methods and type aliases to improve Arrow

The newest version!
@file:Suppress("DEPRECATION")

package app.cash.quiver

import app.cash.quiver.extensions.OutcomeOf
import arrow.core.Either
import arrow.core.Either.Left
import arrow.core.Either.Right
import arrow.core.None
import arrow.core.Option
import arrow.core.Some
import arrow.core.Validated
import arrow.core.flatMap
import arrow.core.getOrElse
import arrow.core.identity
import arrow.core.left
import arrow.core.right
import arrow.core.some
import arrow.core.valid
import app.cash.quiver.extensions.orThrow
import app.cash.quiver.extensions.toResult
import app.cash.quiver.raise.OutcomeRaise
import app.cash.quiver.raise.outcome
import arrow.core.raise.catch
import kotlin.experimental.ExperimentalTypeInference

/**
 * `Outcome` is a type that represents three possible states a result can be in: Present, Absent or Failure. Under the
 * hood it wraps the type `Either>` and supports the common functions that Eithers and Options support such
 * as [`map`](app.cash.quiver.Outcome.map), [`flatMap`](app.cash.quiver.Outcome.flatMap) and
 * [`zip`](app.cash.quiver.Outcome.zip).
 *
 * There are three primary constructors:
 *
 * ```kotlin
 * data class Present(val value: A) : Outcome
 * data class Failure(val error: E) : Outcome
 * object Absent : Outcome
 * ```
 *
 * or you can use the extension methods thusly:
 *
 * ```kotlin
 * A.present()
 * E.failure()
 * ```
 *
 * You can also easily convert an `Either>` to an Outcome using  [`toOutcome()`](app.cash.quiver.toOutcome)
 *
 * ```kotlin
 * val outcome = "hi".some().right().toOutcome()
 * ```
 *
 * There is also a type alias `OutcomeOf` which specialises the error side to a `Throwable` for your convenience.
 */
sealed class Outcome constructor(val inner: Either>) {

  /**
   * Map safely transforms a value in the Outcome. It has no effect on `Absent` or `Failure` instances.
   *
   * ```kotlin
   * Present(1).map { it + 1 }     // Present(2)
   * Absent.map { it + 1 }         // Absent
   * Failure("bad").map { it + 1 } // Failure("bad")
   * ```
   */
  inline fun  map(f: (A) -> B): Outcome = inner.map { it.map(f) }.toOutcome()

  /**
   * Performs an effect over the value and preserves the original `Outcome`
   *
   * ```kotlin
   * "hi".present().tap { println("$it world") } // Present("hi")
   * ```
   */
  inline fun  tap(f: (A) -> B): Outcome = map { a -> f(a); a }

  fun isPresent(): Boolean = inner.fold({ false }) { it.isSome() }
  fun isAbsent(): Boolean = inner.fold({ false }) { it.isNone() }
  fun isFailure(): Boolean = inner.isLeft()

  companion object {
    /**
     * Catches any exceptions thrown by the function and lifts the result into an Outcome.  If your function
     * returns an option use `catchOption` instead
     */
    inline fun  catch(f: () -> R): Outcome =
      catch({ f().present() }) { it.failure() }

    /**
     * Catches any exceptions thrown by the function and lifts the result into an Outcome.  The Optional
     * value will be preserved as Present or Absent accordingly.
     *
     * Converts a function that throws an exception (throwable) and returns an Option into an Outcome
     *
     *
     * ```kotlin
     * val outcome: Outcome = Outcome.catchOption {
     *   val customer: Option = maybeLoadCustomerOrThrow() // May or may not return a customer but throws on error
     *   customer
     * }
     * ```
     */
    inline fun  catchOption(f: () -> Option): Outcome = Either.catch(f).toOutcome()
  }
}

/**
 * A data class representing the Presence of a value `A`.
 */
data class Present(val value: A) : Outcome(value.some().right())
data class Failure(val error: E) : Outcome(error.left())
data object Absent : Outcome(None.right())

fun  A.present(): Outcome = Present(this)
fun  E.failure(): Outcome = Failure(this)

/**
 * FlatMap allows multiple `Outcome`s to be safely chained together, passing the value from the previous as input into the
 * next function that produces an `Outcome`
 *
 * ```kotlin
 * fun  Outcome.flatMap(f: (A) -> Outcome): Outcome
 * ```
 *
 * ```kotlin
 * Present(5).flatMap {
 *   if (it < 5) {
 *     Present(it)
 *   } else if (it < 10) {
 *     Absent
 *   } else {
 *     Failure("Value too high")
 *   }
 * }
 * ```
 */
inline fun  Outcome.flatMap(f: (A) -> Outcome): Outcome =
  outcome { f(bind()).bind() }

inline fun  Outcome.filter(p: (A) -> Boolean): Outcome = outcome {
  bind().also { a -> ensure(p(a)) }
}

/**
 * Performs a flatMap across the supplied function, propagating failures or absence
 * but preserving the original present value.
 *
 *
 * ```kotlin
 * 1.present().flatTap { a -> "bad".failure() } // Failure("bad")
 * 1.present().flatTap { a -> Absent } // Absent
 * 1.present().flatTap { a -> a + 2 } // Present(1) -- value preserved
 * ```
 */
inline fun  Outcome.flatTap(f: (A) -> Outcome): Outcome = flatMap { a ->
  f(a).map { a }
}

inline fun  Outcome.tapFailure(f: (E) -> B): Outcome = mapFailure { f(it); it }
inline fun  Outcome.tapAbsent(f: () -> B): Outcome = onAbsentHandle { f(); Absent }

fun  Outcome>.flatten() = this.flatMap(::identity)

/**
 * Zip allows you to combine two or more `Outcome`s easily with a supplied function.
 *
 * ```kotlin
 * Present(2).zip(Present(3)) { a, b -> a + b }     // Present(5)
 * Present(2).zip(Absent) { a, b -> a + b }         // Absent
 * Present(2).zip(Failure("nup")) { a, b -> a + b } // Failure("nup")
 * ```
 */
inline fun  Outcome.zip(other: Outcome, f: (A, B) -> C): Outcome =
  this.flatMap { a -> other.map { b -> f(a, b) } }

inline fun  Outcome.zip(
  o1: Outcome,
  o2: Outcome,
  crossinline f: (A, B, C) -> D
): Outcome =
  this.zip(o1) { a, b ->
    o2.map { c -> f(a, b, c) }
  }.flatten()

inline fun  Outcome.zip(
  o1: Outcome,
  o2: Outcome,
  o3: Outcome,
  crossinline f: (A, B, C, D) -> EE
): Outcome =
  this.zip(o1, o2) { a, b, c ->
    o3.map { d -> f(a, b, c, d) }
  }.flatten()

inline fun  Outcome.zip(
  o1: Outcome,
  o2: Outcome,
  o3: Outcome,
  o4: Outcome,
  crossinline f: (A, B, C, D, EE) -> F
): Outcome = outcome {
  f(bind(), o1.bind(), o2.bind(), o3.bind(), o4.bind())
}

/**
 * An extension method on Either> that converts it to an Outcome.
 *
 * ```
 * Left("bad").toOutcome() // Failure("bad")
 * Right(None).toOutcome() // Absent
 * Right(Some("hi")).toOutcome() // Present("hi")
 * ```
 */
fun  Either>.toOutcome(): Outcome = when (this) {
  is Left -> Failure(value)
  is Right -> value.map(::Present).getOrElse { Absent }
}

fun  Either.asOutcome(): Outcome = this.map(::Some).toOutcome()

fun  Result.toOutcome(): Outcome = fold(
  onSuccess = { Present(it) },
  onFailure = { Failure(it) }
)

fun  Option.toOutcome(): Outcome = this.right().toOutcome()

inline fun  Outcome.orThrow(onAbsent: () -> Throwable): A = when (this) {
  Absent -> throw onAbsent()
  is Failure -> throw this.error
  is Present -> this.value
}

fun  Outcome.optionOrThrow(): Option = this.inner.orThrow()

/**
 * Converts an Outcome to an option treating Failure as Absent
 */
fun  Outcome.asOption(): Option = inner.getOrElse { None }
inline fun  Outcome.asEither(onAbsent: () -> E): Either =
  inner.flatMap { it.map(::Right).getOrElse { onAbsent().left() } }

/**
 * Converts an OutcomeOf to a Result>. This reflects the inner structure of the
 * Outcome.
 */
fun  OutcomeOf.asResult(): Result> = inner.toResult()

inline fun  Outcome.foldOption(onAbsent: () -> B, onPresent: (A) -> B): Either =
  inner.map { it.fold(onAbsent, onPresent) }

inline fun  Outcome.getOrElse(onAbsentOrFailure: () -> A): A =
  this.foldOption(onAbsentOrFailure, ::identity).getOrElse { onAbsentOrFailure() }

inline fun  Outcome.fold(onFailure: (E) -> B, onAbsent: () -> B, onPresent: (A) -> B): B = when (this) {
  Absent -> onAbsent()
  is Failure -> onFailure(this.error)
  is Present -> onPresent(this.value)
}

inline fun  Outcome.onAbsentHandle(onAbsent: () -> Outcome): Outcome =
  when (this) {
    Absent -> onAbsent()
    is Failure -> this
    is Present -> this
  }

inline fun  Outcome.onFailureHandle(onFailure: (E) -> Outcome): Outcome =
  when (this) {
    Absent -> this
    is Failure -> onFailure(this.error)
    is Present -> this
  }

@OptIn(ExperimentalTypeInference::class)
inline fun  Outcome.recover(@BuilderInference block: OutcomeRaise.(E) -> A): Outcome =
  when(this) {
    Absent -> Absent
    is Failure -> outcome { block(error) }
    is Present -> this
  }

inline fun  Outcome.mapFailure(f: (E) -> EE): Outcome = when (this) {
  Absent -> Absent
  is Failure -> f(this.error).failure()
  is Present -> this
}

fun  Outcome>.sequence(): List> = when (this) {
  Absent -> listOf(Absent)
  is Failure -> listOf(this)
  is Present -> this.value.map(::Present)
}

fun  Outcome>.sequence(): Option> = when (this) {
  Absent -> Some(Absent)
  is Failure -> Some(this)
  is Present -> this.value.map(::Present)
}

fun  Outcome>.sequence(): Either> = when (this) {
  Absent -> Absent.right()
  is Failure -> this.right()
  is Present -> this.value.map(::Present)
}

fun  Outcome>.sequence(): Validated> = when (this) {
  Absent -> Absent.valid()
  is Failure -> this.valid()
  is Present -> this.value.map(::Present)
}

fun  Iterable>.sequence(): Outcome> =
  outcome { map { it.bind() } }

inline fun  Outcome.traverse(f: (A) -> List): List> = this.map(f).sequence()
inline fun  Outcome.traverse(f: (A) -> Option): Option> = this.map(f).sequence()

@OptIn(ExperimentalTypeInference::class)
@OverloadResolutionByLambdaReturnType
inline fun  Outcome.traverse(f: (A) -> Either): Either> = this.map(f).sequence()
inline fun  Outcome.traverse(f: (A) -> Validated): Validated> =
  this.map(f).sequence()