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

commonMain.arrow.core.raise.Raise.kt Maven / Gradle / Ivy

There is a newer version: 2.0.1
Show newest version
@file:OptIn(ExperimentalTypeInference::class, ExperimentalContracts::class)
@file:JvmMultifileClass
@file:JvmName("RaiseKt")

package arrow.core.raise

import arrow.core.Either
import arrow.core.NonEmptyList
import arrow.core.NonEmptySet
import arrow.core.getOrElse
import arrow.core.identity
import arrow.core.nonFatalOrThrow
import arrow.core.recover
import kotlin.coroutines.cancellation.CancellationException
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind.AT_MOST_ONCE
import kotlin.contracts.InvocationKind.EXACTLY_ONCE
import kotlin.contracts.contract
import kotlin.experimental.ExperimentalTypeInference
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName

@DslMarker
public annotation class RaiseDSL

/**
 * 
 *
 * The [Raise] DSL allows you to work with _logical failures_ of type [Error].
 * A _logical failure_ does not necessarily mean that the computation has failed,
 * but that it has stopped or _short-circuited_. The Arrow website has a
 * [guide](https://arrow-kt.io/learn/typed-errors/working-with-typed-errors/)
 * introducing [Raise] and how to use it effectively.
 *
 * The [Raise] DSL allows you to [raise] _logical failure_ of type [Error], and you can [recover] from them.
 *
 * 
 * ```kotlin
 * fun Raise.failure(): Int = raise("failed")
 *
 * fun recovered(): Int =
 *   recover({ failure() }) { _: String -> 1 }
 * ```
 * 
 *
 * Above we defined a function `failure` that raises a logical failure of type [String] with value `"failed"`.
 * And in the function `recovered` we recover from the failure by providing a fallback value, and resolving the error type [String].
 *
 * You can also use the [recover] function inside the [Raise] DSL to transform the error type to a different type such as `List`.
 * And you can do the same for other data types such as `Effect`, `Either`, etc. using [getOrElse] as an alternative to [recover].
 *
 * 
 * ```kotlin
 * fun Raise.failure(): Int = raise("failed")
 *
 * fun Raise>.recovered(): Int =
 *   recover({ failure() }) { msg: String -> raise(msg.toList()) }
 *
 * suspend fun Raise>.recovered2(): Int =
 *   effect { failure() } getOrElse { msg: String -> raise(msg.toList()) }
 *
 * fun Raise>.recovered3(): Int =
 *   "failed".left() getOrElse { msg: String -> raise(msg.toList()) }
 *
 * fun test(): Unit {
 *   recover({ "failed".left().bind() }) { 1 } shouldBe "failed".left().getOrElse { 1 }
 * }
 * ```
 * 
 * 
 *
 * Since we defined programs in terms of [Raise] they _seamlessly work with any of the builders_ available in Arrow,
 * or any you might build for your custom types.
 *
 * 
 * ```kotlin
 * suspend fun test() {
 *   val either: Either =
 *     either { failure() }
 *
 *   val effect: Effect =
 *     effect { failure() }
 *
 *   val ior: Ior =
 *     ior(String::plus) { failure() }
 *
 *   either shouldBe Either.Left("failed")
 *   effect.toEither() shouldBe Either.Left("failed")
 *   ior shouldBe Ior.Left("failed")
 * }
 * ```
 * 
 * 
 *
 * Arrow also exposes [Raise] based error handlers for the most common data types,
 * which allows to recover from _logical failures_ whilst transforming the error type.
 *
 * 
 * ```kotlin
 * fun Raise.failure(): Int = raise("failed")
 *
 * fun test() {
 *   val failure: Either = either { failure() }
 *
 *   failure.recover { _: String -> 1.right().bind() } shouldBe Either.Right(1)
 *
 *   failure.recover { msg: String -> raise(msg.toList()) } shouldBe Either.Left(listOf('f', 'a', 'i', 'l', 'e', 'd'))
 *
 *   recover({ failure.bind() }) { 1 } shouldBe failure.getOrElse { 1 }
 * }
 * ```
 * 
 * 
 */
public interface Raise {

  /**
   * Raises a _logical failure_ of type [Error].
   * This function behaves like a _return statement_,
   * immediately short-circuiting and terminating the computation.
   *
   * __Alternatives:__ Common ways to raise errors include: [ensure], [ensureNotNull], and [bind].
   * Consider using them to make your code more concise and expressive.
   *
   * __Handling raised errors:__ Refer to [recover] and [mapOrAccumulate].
   *
   * @param r an error of type [Error] that will short-circuit the computation.
   * Behaves similarly to _return_ or _throw_.
   *
   * ### Example:
   * ```
   * import arrow.core.Either
   * import arrow.core.mapOrAccumulate
   * import arrow.core.raise.*
   *
   * enum class ServiceType { Free, Paid }
   *
   * data class Config(val mode: Int, val role: String, val serviceType: ServiceType)
   *
   * fun Raise.readConfig(): Config {
   *     val mode = ensureNotNull(readln().toIntOrNull()) {
   *         "Mode should be a valid integer"
   *     }
   *     val role = readln()
   *     ensure(role in listOf("Manager", "Admin")) {
   *         "$role should be either a \"Manager\" or an \"Admin\""
   *     }
   *     val serviceType = parseServiceType(readln()).bind()
   *
   *     return Config(
   *         mode = mode,
   *         role = role,
   *         serviceType = serviceType
   *     )
   * }
   *
   * private fun parseServiceType(rawString: String): Either = catch({
   *     val serviceType = ServiceType.valueOf(rawString)
   *     Either.Right(serviceType)
   * }) {
   *     Either.Left("$rawString is not a valid service type")
   * }
   *
   * fun main() {
   *     val config = recover(::readConfig) { errMsg ->
   *         error("Invalid config, error: $errMsg")
   *     }
   *     // Read 3 additional configs and return Either.Right only if all of them are valid
   *     val additionalConfigs = (1..3).mapOrAccumulate { readConfig() }
   *     println(config) // valid Config
   *     println(additionalConfigs) //  Either, List>
   * }
   * ```
   */
  @RaiseDSL
  public fun raise(r: Error): Nothing

  /**
   * Invoke an [EagerEffect] inside `this` [Raise] context.
   * Any _logical failure_ is raised in `this` [Raise] context,
   * and thus short-circuits the computation.
   *
   * @see [recover] if you want to attempt to recover from any _logical failure_.
   */
  public operator fun  EagerEffect.invoke(): A = invoke(this@Raise)

  /**
   * Invoke an [EagerEffect] inside `this` [Raise] context.
   * Any _logical failure_ is raised in `this` [Raise] context,
   * and thus short-circuits the computation.
   *
   * @see [recover] if you want to attempt to recover from any _logical failure_.
   */
  @RaiseDSL
  public fun  EagerEffect.bind(): A = invoke(this@Raise)

  /**
   * Invoke an [Effect] inside `this` [Raise] context.
   * Any _logical failure_ raised are raised in `this` [Raise] context,
   * and thus short-circuits the computation.
   *
   * @see [recover] if you want to attempt to recover from any _logical failure_.
   */
  public suspend operator fun  Effect.invoke(): A = invoke(this@Raise)

  /**
   * Invoke an [Effect] inside `this` [Raise] context.
   * Any _logical failure_ raised are raised in `this` [Raise] context,
   * and thus short-circuits the computation.
   *
   * @see [recover] if you want to attempt to recover from any _logical failure_.
   */
  @RaiseDSL
  public suspend fun  Effect.bind(): A = invoke(this@Raise)

  /**
   * Extract the [Either.Right] value of an [Either].
   * Any encountered [Either.Left] will be raised as a _logical failure_ in `this` [Raise] context.
   * You can wrap the [bind] call in [recover] if you want to attempt to recover from any _logical failure_.
   *
   * 
   * ```kotlin
   * fun test() {
   *   val one: Either = 1.right()
   *   val left: Either = Either.Left("failed")
   *
   *   either {
   *     val x = one.bind()
   *     val y = recover({ left.bind() }) { _ : String -> 1 }
   *     x + y
   *   } shouldBe Either.Right(2)
   * }
   * ```
   * 
   * 
   */
  @RaiseDSL
  public fun  Either.bind(): A = when (this) {
    is Either.Left -> raise(value)
    is Either.Right -> value
  }

  /**
   * Extracts all the values in the [Map], raising every [Either.Left]
   * as a _logical failure_. In other words, executed [bind] over every
   * value in this [Map].
   */
  public fun  Map>.bindAll(): Map =
    mapValues { (_, a) -> a.bind() }

  @RaiseDSL
  public fun  Iterable>.bindAll(): List =
    map { it.bind() }

  /**
   * Extracts all the values in the [NonEmptyList], raising every [Either.Left]
   * as a _logical failure_. In other words, executed [bind] over every
   * value in this [NonEmptyList].
   */
  @RaiseDSL
  public fun  NonEmptyList>.bindAll(): NonEmptyList =
    map { it.bind() }

  /**
   * Extracts all the values in the [NonEmptySet], raising every [Either.Left]
   * as a _logical failure_. In other words, executed [bind] over every
   * value in this [NonEmptySet].
   */
  @RaiseDSL
  public fun  NonEmptySet>.bindAll(): NonEmptySet =
    map { it.bind() }.toNonEmptySet()
}

/**
 * Execute the [Raise] context function resulting in [A] or any _logical error_ of type [Error],
 * and recover by providing a transform [Error] into a fallback value of type [A].
 * Base implementation of `effect { f() } getOrElse { fallback() }`.
 *
 * 
 * ```kotlin
 * fun test() {
 *   recover({ raise("failed") }) { str -> str.length } shouldBe 6
 *
 *   either {
 *     recover({ raise("failed") }) { _ -> raise(-1) }
 *   } shouldBe Either.Left(-1)
 * }
 * ```
 * 
 * 
 */
@RaiseDSL
public inline fun  recover(
  @BuilderInference block: Raise.() -> A,
  @BuilderInference recover: (error: Error) -> A,
): A {
  contract {
    callsInPlace(block, AT_MOST_ONCE)
    callsInPlace(recover, AT_MOST_ONCE)
  }
  return fold(block, { throw it }, recover, ::identity)
}

/**
 * Execute the [Raise] context function resulting in [A] or any _logical error_ of type [Error],
 * and [recover] by providing a transform [Error] into a fallback value of type [A],
 * or [catch] any unexpected exceptions by providing a transform [Throwable] into a fallback value of type [A],
 *
 * 
 * ```kotlin
 * fun test() {
 *   recover(
 *     { raise("failed") },
 *     { str -> str.length }
 *   ) { t -> t.message ?: -1 } shouldBe 6
 *
 *   fun Raise.boom(): Int = throw RuntimeException("BOOM")
 *
 *   recover(
 *     { boom() },
 *     { str -> str.length }
 *   ) { t -> t.message?.length ?: -1 } shouldBe 4
 * }
 * ```
 * 
 * 
 */
@RaiseDSL
public inline fun  recover(
  @BuilderInference block: Raise.() -> A,
  @BuilderInference recover: (error: Error) -> A,
  @BuilderInference catch: (throwable: Throwable) -> A,
): A {
  contract {
    callsInPlace(block, AT_MOST_ONCE)
    callsInPlace(recover, AT_MOST_ONCE)
    callsInPlace(catch, AT_MOST_ONCE)
  }
  return fold(block, catch, recover, ::identity)
}

/**
 * Execute the [Raise] context function resulting in [A] or any _logical error_ of type [Error],
 * and [recover] by providing a transform [Error] into a fallback value of type [A],
 * or [catch] any unexpected exceptions by providing a transform [Throwable] into a fallback value of type [A],
 *
 * 
 * ```kotlin
 * fun test() {
 *   recover(
 *     { raise("failed") },
 *     { str -> str.length }
 *   ) { t -> t.message ?: -1 } shouldBe 6
 *
 *   fun Raise.boom(): Int = throw RuntimeException("BOOM")
 *
 *   recover(
 *     { boom() },
 *     { str -> str.length }
 *   ) { t: RuntimeException -> t.message?.length ?: -1 } shouldBe 4
 * }
 * ```
 * 
 * 
 */
@RaiseDSL
@JvmName("recoverReified")
public inline fun  recover(
  @BuilderInference block: Raise.() -> A,
  @BuilderInference recover: (error: Error) -> A,
  @BuilderInference catch: (t: T) -> A,
): A {
  contract {
    callsInPlace(block, AT_MOST_ONCE)
    callsInPlace(recover, AT_MOST_ONCE)
    callsInPlace(catch, AT_MOST_ONCE)
  }
  return fold(block, { t -> if (t is T) catch(t) else throw t }, recover, ::identity)
}

/**
 * Allows safely catching exceptions without capturing [CancellationException],
 * or fatal exceptions like `OutOfMemoryError` or `VirtualMachineError` on the JVM.
 *
 * 
 * ```kotlin
 * fun test() {
 *   catch({ throw RuntimeException("BOOM") }) { _ ->
 *     "fallback"
 *   } shouldBe "fallback"
 *
 *   fun fetchId(): Int = throw RuntimeException("BOOM")
 *
 *   either {
 *     catch({ fetchId() }) { t ->
 *       raise("something went wrong: ${t.message}")
 *     }
 *   } shouldBe Either.Left("something went wrong: BOOM")
 * }
 * ```
 * 
 * 
 *
 * Alternatively, you can use `try { } catch { }` blocks with [nonFatalOrThrow].
 * This API offers a similar syntax as the top-level [catch] functions like [Either.catch].
 */
@RaiseDSL
public inline fun  catch(block: () -> A, catch: (throwable: Throwable) -> A): A {
  contract {
    callsInPlace(block, AT_MOST_ONCE)
    callsInPlace(catch, AT_MOST_ONCE)
  }
  return try {
    block()
  } catch (t: Throwable) {
    catch(t.nonFatalOrThrow())
  }
}

/**
 * Allows safely catching exceptions of type `T` without capturing [CancellationException],
 * or fatal exceptions like `OutOfMemoryError` or `VirtualMachineError` on the JVM.
 *
 * 
 * ```kotlin
 * fun test() {
 *   catch({ throw RuntimeException("BOOM") }) { _ ->
 *     "fallback"
 *   } shouldBe "fallback"
 *
 *   fun fetchId(): Int = throw RuntimeException("BOOM")
 *
 *   either {
 *     catch({ fetchId() }) { t: RuntimeException ->
 *       raise("something went wrong: ${t.message}")
 *     }
 *   } shouldBe Either.Left("something went wrong: BOOM")
 * }
 * ```
 * 
 * 
 *
 * Alternatively, you can use `try { } catch(e: T) { }` blocks.
 * This API offers a similar syntax as the top-level [catch] functions like [Either.catch].
 */
@RaiseDSL
@JvmName("catchReified")
public inline fun  catch(block: () -> A, catch: (t: T) -> A): A {
  contract {
    callsInPlace(block, AT_MOST_ONCE)
    callsInPlace(catch, AT_MOST_ONCE)
  }
  return catch(block) { t: Throwable -> if (t is T) catch(t) else throw t }
}

/**
 * Ensures that the [condition] is met;
 * otherwise, [Raise.raise]s a logical failure of type [Error].
 *
 * In summary, this is a type-safe alternative to [require], using the [Raise] API.
 *
 * ### Example:
 * ```
 * @JvmInline
 * value class CountryCode(val code: String)
 *
 * sealed interface CountryCodeError {
 *     data class InvalidLength(val length: Int) : CountryCodeError
 *     object ContainsInvalidChars : CountryCodeError
 * }
 *
 * fun Raise.countryCode(rawCode: String): CountryCode {
 *     ensure(rawCode.length == 2) { CountryCodeError.InvalidLength(rawCode.length) }
 *     ensure(rawCode.any { !it.isLetter() }) { CountryCodeError.ContainsInvalidChars }
 *     return CountryCode(rawCode)
 * }
 *
 * fun main() {
 *     recover({
 *         countryCode("US") // valid
 *         countryCode("ABC") // raises CountryCode.InvalidLength error
 *         countryCode("A1") // raises CountryCode.ContainsInvalidChar
 *     }) { error ->
 *         // Handle errors in a type-safe manner
 *         when (error) {
 *             CountryCodeError.ContainsInvalidChars -> {}
 *             is CountryCodeError.InvalidLength -> {}
 *         }
 *     }
 *
 *     // Can call it w/o error handling => prone to runtime errors
 *     countryCodeOrThrow("Will fail")
 *     countryCode("Will fail") // this line won't compile => we're protected
 *
 *     try {
 *         countryCodeOrThrow("US") // valid
 *         countryCodeOrThrow("ABC") // throw IllegalArgumentException
 *         countryCodeOrThrow("A1") // throw IllegalArgumentException
 *     } catch (e: IllegalArgumentException) {
 *         // Not easy to handle
 *     }
 * }
 *
 * // Not type-safe alternative using require
 * @Throws(IllegalArgumentException::class)
 * fun countryCodeOrThrow(rawCode: String): CountryCode {
 *     require(rawCode.length == 2) { CountryCodeError.InvalidLength(rawCode.length) }
 *     require(rawCode.any { !it.isLetter() }) { CountryCodeError.ContainsInvalidChars }
 *     return CountryCode(rawCode)
 * }
 * ```
 *
 * @param condition the condition that must be true.
 * @param raise a lambda that produces an error of type [Error] when the [condition] is false.
 *
 */
@RaiseDSL
public inline fun  Raise.ensure(condition: Boolean, raise: () -> Error) {
  contract {
    callsInPlace(raise, AT_MOST_ONCE)
    returns() implies condition
  }
  return if (condition) Unit else raise(raise())
}

/**
 * Ensures that the [value] is not null;
 * otherwise, [Raise.raise]s a logical failure of type [Error].
 *
 * In summary, this is a type-safe alternative to [requireNotNull], using the [Raise] API.
 *
 * ### Example
 * ```
 *@JvmInline
 * value class FullName(val name: String)
 *
 * sealed interface NameError {
 *     object NullValue : NameError
 * }
 *
 * fun Raise.fullName(name: String?): FullName {
 *     val nonNullName = ensureNotNull(name) { NameError.NullValue }
 *     return FullName(nonNullName)
 * }
 *
 * fun main() {
 *     recover({
 *         fullName("John Doe") // valid
 *         fullName(null) // raises NameError.NullValue error
 *     }) { error ->
 *         // Handle errors in a type-safe manner
 *         when (error) {
 *             NameError.NullValue -> {}
 *         }
 *     }
 * }
 * ```
 *
 * @param value the value that must be non-null.
 * @param raise a lambda that produces an error of type [Error] when the [value] is null.
 */
@RaiseDSL
public inline fun  Raise.ensureNotNull(value: B?, raise: () -> Error): B {
  contract {
    callsInPlace(raise, AT_MOST_ONCE)
    returns() implies (value != null)
  }
  return value ?: raise(raise())
}

/**
 * Execute the [Raise] context function resulting in [A] or any _logical error_ of type [OtherError],
 * and transform any raised [OtherError] into [Error], which is raised to the outer [Raise].
 *
 * 
 * ```kotlin
 * fun test() {
 *   either {
 *     withError(String::length) {
 *       raise("failed")
 *     }
 *   } shouldBe Either.Left(6)
 * }
 * ```
 * 
 * 
 */
@RaiseDSL
public inline fun  Raise.withError(
  transform: (OtherError) -> Error,
  @BuilderInference block: Raise.() -> A
): A {
  contract {
    callsInPlace(block, EXACTLY_ONCE)
  }
  recover({ return block(this) }) { raise(transform(it)) }
}

/**
 * Execute the [Raise] context function resulting in [A] or any _logical error_ of type [A].
 * Does not distinguish between normal results and errors, thus you can consider
 * `return` and `raise` to be semantically equivalent inside.
 *
 * 
 * ```kotlin
 * fun test() {
 *   merge { if(Random.nextBoolean()) raise("failed") else "failed" } shouldBe "failed"
 * }
 * ```
 * 
 * 
 */
@RaiseDSL
@JvmName("_merge")
public inline fun  merge(
  @BuilderInference block: Raise.() -> A,
): A {
  contract {
    callsInPlace(block, AT_MOST_ONCE)
  }
  return recover(block, ::identity)
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy