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

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

There is a newer version: 2.0.1
Show newest version
@file:JvmMultifileClass
@file:JvmName("RaiseKt")
@file:OptIn(ExperimentalTypeInference::class)
package arrow.core.raise

import kotlin.experimental.ExperimentalTypeInference
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName

/**
 * [Effect] represents a function of `suspend Raise.() -> A`, that short-circuit with a value of `R` or `Throwable`, or completes with a value of `A`.
 *
 * So [Effect] is defined by `suspend fun  fold(recover: suspend (Throwable) -> B, resolve: suspend (R) -> B, transform: suspend (A) -> B): B`,
 * to map all values of `R`, `Throwable` and `A` to a value of `B`.
 *
 * 

      * [Writing a program with Effect](#writing-a-program-with-effect)
      * [Handling errors](#handling-errors)
        * [recover](#recover)
        * [catch](#catch)
      * [Structured Concurrency](#structured-concurrency)
        * [Arrow Fx Coroutines](#arrow-fx-coroutines)
          * [parZip](#parzip)
          * [parTraverse](#partraverse)
          * [raceN](#racen)
          * [bracketCase / Resource](#bracketcase--resource)
        * [KotlinX](#kotlinx)
          * [withContext](#withcontext)
          * [async](#async)
          * [launch](#launch)
          * [Strange edge cases](#strange-edge-cases)

 * 
 *
 * ## Writing a program with Effect
 *
 * Let's write a small program to read a file from disk, and instead of having the program work exception based we want to
 * turn it into a polymorphic type-safe program.
 *
 * We'll start by defining a small function that accepts a [String], and does some simply validation to check that the path
 * is not empty. If the path is empty, we want to program to result in `EmptyPath`. So we're immediately going to see how
 * we can raise an error of any arbitrary type `R` by using the function `raise`. The name `raise` comes raising an intterupt, or
 * changing, especially unexpectedly, away from the computation and finishing the `Continuation` with `R`.
 *
 * 
 * ```kotlin
 * object EmptyPath
 *
 * fun readFile(path: String): Effect = effect {
 *   if (path.isEmpty()) raise(EmptyPath) else Unit
 * }
 * ```
 *
 * Here we see how we can define an `Effect` which has `EmptyPath` for the raise type `R`, and `Unit` for the success type `A`.
 *
 * Patterns like validating a [Boolean] is very common, and the [Effect] DSL offers utility functions like [kotlin.require]
 * and [kotlin.requireNotNull]. They're named [ensure] and [ensureNotNull] to avoid conflicts with the `kotlin` namespace.
 * So let's rewrite the function from above to use the DSL instead.
 *
 * ```kotlin
 * fun readFile2(path: String?): Effect = effect {
 *   ensureNotNull(path) { EmptyPath }
 *   ensure(path.isNotEmpty()) { EmptyPath }
 * }
 * ```
 * 
 *
 * Now that we have the path, we can read from the `File` and return it as a domain model `Content`.
 * We also want to take a look at what exceptions reading from a file might occur `FileNotFoundException` & `SecurityError`,
 * so lets make some domain errors for those too. Grouping them as a sealed interface is useful since that way we can resolve *all* errors in a type safe manner.
 *
 * 
 * ```kotlin
 * @JvmInline
 * value class Content(val body: List)
 *
 * sealed interface FileError
 * @JvmInline value class SecurityError(val msg: String?) : FileError
 * @JvmInline value class FileNotFound(val path: String) : FileError
 * object EmptyPath : FileError {
 *   override fun toString() = "EmptyPath"
 * }
 * ```
 *
 * We can finish our function, but we need to refactor the return type from `Unit` to `Content` and the error type from `EmptyPath` to `FileError`.
 *
 * ```kotlin
 * fun readFile(path: String?): Effect = effect {
 *   ensureNotNull(path) { EmptyPath }
 *   ensure(path.isNotEmpty()) { EmptyPath }
 *   try {
 *     val lines = File(path).readLines()
 *     Content(lines)
 *   } catch (e: FileNotFoundException) {
 *     raise(FileNotFound(path))
 *   } catch (e: SecurityException) {
 *     raise(SecurityError(e.message))
 *   }
 * }
 * ```
 *
 * The `readFile` function defines a `suspend fun` that will return:
 *
 * - the `Content` of a given `path`
 * - a `FileError`
 * - An unexpected fatal error (`OutOfMemoryException`)
 *
 * Since these are the properties of our `Effect` function, we can turn it into a value.
 *
 * ```kotlin
 * suspend fun main() {
 *    readFile("").toEither() shouldBe Either.Left(EmptyPath)
 *    readFile("gradle.properties").toIor() shouldBe Ior.Left(FileNotFound("gradle.properties"))
 *    readFile("README.MD").toOption { None } shouldBe None
 *
 *    readFile("build.gradle.kts").fold({ _: FileError -> null }, { it })
 *      .shouldBeInstanceOf()
 *       .body.shouldNotBeEmpty()
 * }
 * ```
 * 
 *
 * The functions above are available out of the box, but it's easy to define your own extension functions in terms
 * of `fold`. Implementing the `toEither()` operator is as simple as:
 *
 * 
 * ```kotlin
 * suspend fun  Effect.toEither(): Either =
 *   fold({ Either.Left(it) }) { Either.Right(it) }
 *
 * suspend fun  Effect.toOption(): Option =
 *   fold(::identity) { Some(it) }
 * ```
 * 
 *
 * Adding your own syntax to `Raise` is not advised, yet, but will be easy once context parameters become available.
 *
 * ```
 * context(_: Raise)
 * suspend fun  Either.bind(): A =
 *   when (this) {
 *     is Either.Left -> raise(value)
 *     is Either.Right -> value
 *   }
 *
 * context(_: Raise)
 * fun  Option.bind(): A =
 *   fold({ raise(it) }, ::identity)
 * ```
 *
 * ## Handling errors
 *
 * An Effect has 2 error channels: `Throwable` and `R`
 * There are two separate handlers to transform either of the error channels.
 *
 * - `recover` to handle, and transform any error of type `R`.
 * - `catch` to handle, and transform and error of type `Throwable`.
 *
 * ### recover
 *
 * `recover` handles the error of type `R`,
 * by providing a new value of type `A`, raising a different error of type `E`, or throwing an exception.
 *
 * Let's take a look at some examples:
 *
 * We define a `val failed` of type `Effect`, that represents a failed effect with value "failed".
 *
 * 
 * ```kotlin
 * val failed: Effect =
 *   effect { raise("failed") }
 * ```
 *
 * We can `recover` the failure, and resolve it by providing a default value of `-1` or the length of the `error: String`.
 *
 * ```kotlin
 * val default: Effect =
 *   failed.recover { -1 }
 *
 * val resolved: Effect =
 *   failed.recover { it.length }
 * ```
 *
 * As you can see the resulting `error` is now of type `Nothing`, since we did not raise any new errors.
 * So our `Effect` knows that no short-circuiting will occur during execution. Awesome!
 * But it can also infer to any other error type that you might want instead, because it's never going to occur.
 * So as you see below, we can even assign our `Effect` to `Effect`, where `E` can be any type.
 *
 * ```kotlin
 * val default2: Effect = default
 * val resolved2: Effect = resolved
 * ```
 *
 * `recover` also allows us to _change_ the error type when we resolve the error of type `R`.
 * Below we handle our error of `String` and turn it into `List` using `reversed().toList()`.
 * This is a powerful operation, since it allows us to transform our error types across boundaries or layers.
 *
 * ```kotlin
 * val newError: Effect, Int> =
 *   failed.recover { str ->
 *     raise(str.reversed().toList())
 *   }
 * ```
 *
 * Finally, since `recover` supports `suspend` we can safely call other `suspend` code and throw `Throwable` into the `suspend` system.
 * This is typically undesired, since you should prefer lifting `Throwable` into typed values of `R` to make them compile-time tracked.
 *
 * ```kotlin
 * val newException: Effect =
 *   failed.recover { str -> throw RuntimeException(str) }
 * ```
 *
 * ### catch
 *
 * `catch` gives us the same powers as `recover`, but instead of resolving `R` we're recovering from any unexpected `Throwable`.
 * Unexpected, because the expectation is that all `Throwable` get turned into `R` unless it's a fatal/unexpected.
 * This operator is useful when you need to work/wrap foreign code, especially Java SDKs or any code that is heavily based on exceptions.
 *
 * Below we've defined a `foreign` value that represents wrapping a foreign API which might throw `RuntimeException`.
 *
 * ```kotlin
 * val foreign = effect {
 *   throw RuntimeException("BOOM!")
 * }
 * ```
 *
 * We can `catch` to run the effect recovering from any exception,
 * and recover it by providing a default value of `-1` or the length of the [Throwable.message].
 *
 * ```kotlin
 * val default3: Effect =
 *   foreign.catch { -1 }
 *
 * val resolved3: Effect =
 *   foreign.catch { it.message?.length ?: -1 }
 * ```
 *
 * A big difference with `recover` is that `catch` **cannot** change the error type of `R` because it doesn't resolve it, so it stays unchanged.
 * You can however compose `recover`, and `v` to resolve the error type **and** recover the exception.
 *
 * ```kotlin
 * val default4: Effect =
 *   foreign
 *     .recover { -1 }
 *     .catch { -2 }
 * ```
 *
 * `catch` however offers an overload that can _refine the exception_.
 * Let's say you're wrapping some database interactions that might throw `java.sql.SqlException`, or `org.postgresql.util.PSQLException`,
 * then you might only be interested in those exceptions and not `Throwable`. `catch` allows you to install multiple handlers for specific exceptions.
 * If the desired exception is not matched, then it stays in the `suspend` exception channel and will be thrown or recovered at a later point.
 *
 * ```kotlin
 * val default5: Effect =
 *   foreign
 *     .catch { _: RuntimeException -> -1 }
 *     .catch { _: java.sql.SQLException -> -2 }
 * ```
 *
 * Finally, since `catch` also supports `suspend` we can safely call other `suspend` code and throw `Throwable` into the `suspend` system.
 * This can be useful if refinement of exceptions is not sufficient, for example in the case of `org.postgresql.util.PSQLException` you might want to
 * check the `SQLState` to check for a `foreign key violation` and rethrow the exception if not matched.
 *
 * ```kotlin
 * suspend fun java.sql.SQLException.isForeignKeyViolation(): Boolean = true
 *
 * val rethrown: Effect =
 *   failed.catch { ex: java.sql.SQLException ->
 *     if(ex.isForeignKeyViolation()) raise("foreign key violation")
 *     else throw ex
 *   }
 * ```
 *
 * 
 *
 * Note:
 *  Handling errors can also be done with `try/catch` but this is **not recommended**, it uses `CancellationException` which is used to cancel `Coroutine`s and is advised not to capture in Kotlin.
 *  The `CancellationException` from `Effect` is `RaiseCancellationException`, this a public type, thus can be distinguished from any other `CancellationException` if necessary.
 *
 * ## Structured Concurrency
 *
 * `Effect` relies on `kotlin.cancellation.CancellationException` to `raise` error values of type `R` inside the `Continuation` since it effectively cancels/short-circuits it.
 * For this reason `raise` adheres to the same rules as [`Structured Concurrency`](https://kotlinlang.org/docs/coroutines-basics.html#structured-concurrency)
 *
 * Let's overview below how `raise` behaves with the different concurrency builders from Arrow Fx & KotlinX Coroutines.
 * In the examples below we're going to be using a utility to show how _sibling tasks_ get cancelled.
 * The utility function show below called `awaitExitCase` will `never` finish suspending, and completes a `Deferred` with the `ExitCase`.
 * `ExitCase` is a sealed class that can be a value of `Failure(Throwable)`, `Cancelled(CancellationException)`, or `Completed`.
 * Since `awaitExitCase` suspends forever, it can only result in `Cancelled(CancellationException)`.
 *
 * 
 * ```kotlin
 * suspend fun  awaitExitCase(exit: CompletableDeferred): A =
 *   guaranteeCase(::awaitCancellation) { exitCase -> exit.complete(exitCase) }
 *
 * ```
 *
 * ### Arrow Fx Coroutines
 * All operators in Arrow Fx Coroutines run in place, so they have no way of leaking `raise`.
 * It's there always safe to compose `effect` with any Arrow Fx combinator. Let's see some small examples below.
 *
 * #### parZip
 *
 * ```kotlin
 *  suspend fun main() {
 *    val error = "Error"
 *    val exit = CompletableDeferred()
 *   effect {
 *     parZip({ awaitExitCase(exit) }, { raise(error) }) { a: Int, b: Int -> a + b }
 *   }.fold({ it shouldBe error }, { fail("Int can never be the result") })
 *   exit.await().shouldBeTypeOf()
 * }
 * ```
 * 
 *
 * #### parTraverse
 * 
 * ```kotlin
 * suspend fun main() {
 *   val error = "Error"
 *   val exits = (0..3).map { CompletableDeferred() }
 *   effect> {
 *     (0..4).parMap { index ->
 *       if (index == 4) raise(error)
 *       else awaitExitCase(exits[index])
 *     }
 *   }.fold({ msg -> msg shouldBe error }, { fail("Int can never be the result") })
 *   // It's possible not all parallel task got launched, and in those cases awaitCancellation never ran
 *   exits.forEach { exit -> exit.getOrNull()?.shouldBeTypeOf() }
 * }
 * ```
 * 
 *
 * `parTraverse` will launch 5 tasks, for every element in `1..5`.
 * The last task to get scheduled will `raise` with "error", and it will cancel the other launched tasks before returning.
 *
 * #### raceN
 * 
 * ```kotlin
 * suspend fun main() {
 *   val error = "Error"
 *   val exit = CompletableDeferred()
 *   effect {
 *     raceN({ awaitExitCase(exit) }) { raise(error) }
 *       .merge() // Flatten Either result from race into Int
 *   }.fold({ msg -> msg shouldBe error }, { fail("Int can never be the result") })
 *   // It's possible not all parallel task got launched, and in those cases awaitCancellation never ran
 *   exit.getOrNull()?.shouldBeTypeOf()
 * }
 * ```
 * 
 *
 * `raceN` races `n` suspend functions in parallel, and cancels all participating functions when a winner is found.
 * We can consider the function that `raise`s the winner of the race, except with a raised value instead of a successful one.
 * So when a function in the race `raise`s, and thus short-circuiting the race, it will cancel all the participating functions.
 *
 * #### bracketCase / Resource
 * 
 * ```kotlin
 * suspend fun main() {
 *   val error = "Error"
 *   val exit = CompletableDeferred()
 *   effect {
 *     bracketCase(
 *       acquire = { File("build.gradle.kts").bufferedReader() },
 *       use = { _: BufferedReader -> raise(error) },
 *       release = { reader, exitCase ->
 *         reader.close()
 *         exit.complete(exitCase)
 *       }
 *     )
 *   }.fold({ it shouldBe error }, { fail("Int can never be the result") })
 *   exit.await().shouldBeTypeOf()
 * }
 * ```
 * 
 *
 * 
 * ```kotlin
 * suspend fun main() {
 *   val error = "Error"
 *   val exit = CompletableDeferred()
 *
 *   suspend fun ResourceScope.bufferedReader(path: String): BufferedReader =
 *     autoCloseable { File(path).bufferedReader() }.also {
 *       onRelease { exitCase -> exit.complete(exitCase) }
 *     }
 *
 *   resourceScope {
 *     effect {
 *       val reader = bufferedReader("build.gradle.kts")
 *       raise(error)
 *       reader.lineSequence().count()
 *     }.fold({ it shouldBe error }, { fail("Int can never be the result") })
 *   }
 *   exit.await().shouldBeTypeOf()
 * }
 * ```
 * 
 *
 * ### KotlinX
 * #### withContext
 * It's always safe to call `raise` from `withContext` since it runs in place, so it has no way of leaking `raise`.
 * When `raise` is called from within `withContext` it will cancel all `Job`s running inside the `CoroutineScope` of `withContext`.
 *
 * 
 * ```kotlin
 * suspend fun main() {
 *   val exit = CompletableDeferred()
 *   effect {
 *     withContext(Dispatchers.IO) {
 *       val job = launch { awaitExitCase(exit) }
 *       val content = readFile("failure").bind()
 *       job.join()
 *       content.body.size
 *     }
 *   }.fold({ e -> e shouldBe FileNotFound("failure") }, { fail("Int can never be the result") })
 *   exit.await().shouldBeInstanceOf()
 * }
 * ```
 * 
 *
 * #### async
 *
 * When calling `raise` from `async` you should **always** call `await`, otherwise `raise` can leak out of its scope.
 *
 * 
 * ```kotlin
 * suspend fun main() {
 *   val errorA = "ErrorA"
 *   val errorB = "ErrorB"
 *   coroutineScope {
 *     effect {
 *       val fa = async { raise(errorA) }
 *       val fb = async { raise(errorB) }
 *       fa.await() + fb.await()
 *     }.fold({ error -> error shouldBeIn listOf(errorA, errorB) }, { fail("Int can never be the result") })
 *   }
 * }
 * ```
 * 
 *
 * #### launch
 *
 * 
 * ```kotlin
 * suspend fun main() {
 *   val errorA = "ErrorA"
 *   val errorB = "ErrorB"
 *   val int = 45
 *   effect {
 *     coroutineScope {
 *       launch { raise(errorA) }
 *       launch { raise(errorB) }
 *       int
 *     }
 *   }.fold({ fail("Raise can never finish") }, { it shouldBe int })
 * }
 * ```
 * 
 *
 * #### Strange edge cases
 *
 * **NOTE**
 * Capturing `raise` into a lambda, and leaking it outside of `Effect` to be invoked outside will yield unexpected results.
 * Below we capture `raise` from inside the DSL, and then invoke it outside its context `Raise`.
 *
 * 
 * 
 * ```kotlin
 *   effect Unit> {
 *     suspend { raise("error") }
 *   }.fold({ }, { leakedRaise -> leakedRaise.invoke() })
 * ```
 *
 * The same violation is possible in all DSLs in Kotlin, including Structured Concurrency.
 *
 * ```kotlin
 *   val leakedAsync = coroutineScope Deferred> {
 *     suspend {
 *       async {
 *         println("I am never going to run, until I get called invoked from outside")
 *       }
 *     }
 *   }
 *
 *   leakedAsync.invoke().await()
 * ```
 * 
 */
public typealias Effect = suspend Raise.() -> A

@Suppress("NOTHING_TO_INLINE")
public inline fun  effect(@BuilderInference noinline block: suspend Raise.() -> A): Effect = block

/** The same behavior and API as [Effect] except without requiring _suspend_. */
public typealias EagerEffect = Raise.() -> A

@Suppress("NOTHING_TO_INLINE")
public inline fun  eagerEffect(@BuilderInference noinline block: Raise.() -> A): EagerEffect = block

public suspend fun  Effect.merge(): A = merge { invoke() }
public fun  EagerEffect.merge(): A = merge { invoke() }

public suspend fun  Effect.get(): A = merge()

public fun  EagerEffect.get(): A = merge()




© 2015 - 2025 Weber Informatics LLC | Privacy Policy