Functional companion to Kotlin's Standard Library - Core module with iosArm64 target support
—
Composable typed error handling system with short-circuiting and error accumulation capabilities. The Raise DSL provides a unified approach to error handling that integrates seamlessly with Arrow's data types.
Core interface for typed error handling that allows raising errors and binding over various error-containing types.
/**
* Core interface for typed error handling
* @param Error The type of errors that can be raised
*/
@RaiseDSL
interface Raise<in Error> {
/**
* Raise an error, short-circuiting the computation
* @param error The error to raise
* @return Nothing (never returns)
*/
fun raise(error: Error): Nothing
/**
* Extract the success value from Either, or raise the error
* @param receiver Either to bind over
* @return Success value if Right, raises error if Left
*/
fun <A> Either<Error, A>.bind(): A
/**
* Extract value from Option, raising Unit if None (requires SingletonRaise)
* @param receiver Option to bind over
* @return Value if Some, raises if None
*/
fun <A> Option<A>.bind(): A // Available in SingletonRaise context
/**
* Extract success value from Ior, accumulating any left values
* @param receiver Ior to bind over
* @return Right or Both right value, accumulates left values
*/
fun <A> Ior<Error, A>.bind(): A // Available in IorRaise context
/**
* Bind over all Either values in a collection
* @param receiver Collection of Either values
* @return Collection of success values, raises first error encountered
*/
fun <A> Iterable<Either<Error, A>>.bindAll(): List<A>
fun <A> NonEmptyList<Either<Error, A>>.bindAll(): NonEmptyList<A>
fun <A> NonEmptySet<Either<Error, A>>.bindAll(): NonEmptySet<A>
fun <K, A> Map<K, Either<Error, A>>.bindAll(): Map<K, A>
}Usage Examples:
import arrow.core.raise.*
import arrow.core.*
// Basic error raising
fun Raise<String>.parsePositiveInt(input: String): Int {
val number = input.toIntOrNull() ?: raise("Not a valid integer: $input")
if (number <= 0) raise("Number must be positive: $number")
return number
}
// Using Either DSL
val result = either {
val x = parsePositiveInt("42") // 42
val y = parsePositiveInt("0") // Raises "Number must be positive: 0"
x + y // This line never executes
} // Left("Number must be positive: 0")
// Binding over Either values
val computation = either {
val a = Either.right(10).bind() // 10
val b = Either.right(20).bind() // 20
val c = Either.left("error").bind() // Raises "error"
a + b + c // Never executes
} // Left("error")Specialized Raise implementation for handling nullable values and Option types.
/**
* Specialized Raise for unit-based errors, typically for null handling
* @param Error Error type (often Unit for simple null checks)
*/
class SingletonRaise<in Error>(private val raise: Raise<Unit>) : Raise<Error> {
/**
* Ensure a condition is true, raising unit error if false
* @param condition Boolean condition to check
*/
fun ensure(condition: Boolean): Unit
/**
* Ensure a value is not null, raising unit error if null
* @param value Nullable value to check
* @return Non-null value
*/
fun <A> ensureNotNull(value: A?): A
/**
* Bind over nullable values
* @param receiver Nullable value
* @return Non-null value or raises
*/
fun <A> A?.bind(): A
/**
* Bind over Option values
* @param receiver Option to bind over
* @return Value if Some, raises if None
*/
fun <A> Option<A>.bind(): A
/**
* Bind over collections of nullable values
*/
fun <A> Iterable<A?>.bindAll(): List<A>
fun <A> NonEmptyList<A?>.bindAll(): NonEmptyList<A>
fun <K, A> Map<K, A?>.bindAll(): Map<K, A>
/**
* Recover from errors with fallback computation
*/
inline fun <A> recover(
block: SingletonRaise<Error>.() -> A,
fallback: () -> A
): A
}Usage Examples:
import arrow.core.raise.*
import arrow.core.*
// Working with nullable values
val result = option {
val name = "Alice".takeIf { it.isNotEmpty() }.bind() // "Alice"
val age = "25".toIntOrNull().bind() // 25
val email = null.bind() // Raises, short-circuits
"$name ($age) - $email" // Never executes
} // None
// Validation with ensure
val validation = option {
val input = "42"
ensure(input.isNotBlank()) { } // Passes
val number = input.toIntOrNull().bind()
ensureNotNull(number.takeIf { it > 0 }) // 42
} // Some(42)
// Binding collections
val numbers = option {
listOf("1", "2", null, "4").mapNotNull { it?.toIntOrNull() }.bind() // Fails on null
} // NoneRaise implementation that accumulates errors while preserving successful computations.
/**
* Raise implementation for error accumulation with Ior
* @param Error Error type that can be combined
*/
class IorRaise<Error>(
val combineError: (Error, Error) -> Error,
private val combineRef: Atomic<Any?>,
private val raise: Raise<Error>
) : Raise<Error> {
/**
* Accumulate an error without short-circuiting
* @param error Error to accumulate
*/
fun accumulate(error: Error): Unit
/**
* Combine an error with any previously accumulated errors
* @param error Error to combine
* @return Combined error
*/
fun combine(error: Error): Error
/**
* Bind over Ior values, accumulating left values
* @param receiver Ior to bind over
* @return Right value, accumulates any left values
*/
fun <A> Ior<Error, A>.bind(): A
/**
* Get value from Either or accumulate the error and provide fallback
* @param receiver Either to process
* @param fallback Function to provide fallback value
* @return Right value or result of fallback after accumulating error
*/
fun <A> Either<Error, A>.getOrAccumulate(fallback: (Error) -> A): A
/**
* Recover from accumulated errors
*/
inline fun <A> recover(
block: IorRaise<Error>.() -> A,
fallback: (Error) -> A
): A
}Usage Examples:
import arrow.core.raise.*
import arrow.core.*
// Error accumulation with IorRaise
data class ValidationError(val message: String)
fun combineErrors(e1: ValidationError, e2: ValidationError): ValidationError =
ValidationError("${e1.message}; ${e2.message}")
val result = ior(::combineErrors) {
val name = validateName("").getOrAccumulate { ValidationError("Name is required") }
val email = validateEmail("invalid").getOrAccumulate { ValidationError("Invalid email") }
val age = validateAge(-1).getOrAccumulate { ValidationError("Age must be positive") }
User(name, email, age) // Creates user even with accumulated errors
} // Ior.Both(ValidationError("Name is required; Invalid email; Age must be positive"), User(...))Convenient functions for creating computations in the Raise context.
/**
* Create an Either computation with error handling
* @param block Computation that can raise errors of type Error
* @return Either.Left with error or Either.Right with result
*/
inline fun <Error, A> either(block: Raise<Error>.() -> A): Either<Error, A>
/**
* Create an Option computation with null handling
* @param block Computation that can fail (raise Unit)
* @return Some with result or None if computation failed
*/
inline fun <A> option(block: SingletonRaise<Unit>.() -> A): Option<A>
/**
* Create an Ior computation with error accumulation
* @param combineError Function to combine multiple errors
* @param block Computation that can raise and accumulate errors
* @return Ior with accumulated errors and/or result
*/
inline fun <Error, A> ior(
combineError: (Error, Error) -> Error,
block: IorRaise<Error>.() -> A
): Ior<Error, A>
/**
* Create a nullable computation
* @param block Computation that can fail
* @return Result value or null if computation failed
*/
inline fun <A> nullable(block: SingletonRaise<Unit>.() -> A): A?
/**
* Recover from errors in Either computation
* @param block Computation that might fail
* @param fallback Function to handle errors
* @return Result of computation or fallback
*/
inline fun <Error, A> recover(
block: Raise<Error>.() -> A,
fallback: (Error) -> A
): A
/**
* Fold over Either result with error and success handlers
* @param block Computation that might fail
* @param onError Handler for error case
* @param onSuccess Handler for success case
* @return Result of appropriate handler
*/
inline fun <Error, A, B> fold(
block: Raise<Error>.() -> A,
onError: (Error) -> B,
onSuccess: (A) -> B
): BUsage Examples:
import arrow.core.raise.*
import arrow.core.*
// Either computation
val parseResult = either {
val input = "not-a-number"
input.toIntOrNull() ?: raise("Invalid number: $input")
} // Left("Invalid number: not-a-number")
// Option computation with nullable handling
val safeComputation = option {
val user = findUser("123").bind() // Returns null if not found
val profile = user.profile.bind() // Returns null if no profile
profile.displayName.bind() // Returns null if no display name
} // None if any step returns null
// Recovery with fallback
val withFallback = recover(
{ parsePositiveInt("-5") } // Raises error
) { error -> 0 } // 0
// Folding over computation
val message = fold(
{ parsePositiveInt("42") },
onError = { "Error: $it" },
onSuccess = { "Success: $it" }
) // "Success: 42"Suspended computations that can raise typed errors, providing a foundation for asynchronous and concurrent programming with proper error handling.
/**
* Suspended computation that can raise typed errors
* @param Error The type of errors that can be raised
* @param A The type of successful result
*/
typealias Effect<Error, A> = suspend Raise<Error>.() -> A
/**
* Non-suspended computation that can raise typed errors
* @param Error The type of errors that can be raised
* @param A The type of successful result
*/
typealias EagerEffect<Error, A> = Raise<Error>.() -> A
/**
* Execute a suspended Effect computation
* @param block The Effect computation to execute
* @return Either containing the error or successful result
*/
suspend fun <Error, A> effect(block: suspend Raise<Error>.() -> A): Either<Error, A>
/**
* Execute an eager Effect computation
* @param block The EagerEffect computation to execute
* @return Either containing the error or successful result
*/
fun <Error, A> eagerEffect(block: Raise<Error>.() -> A): Either<Error, A>
/**
* Execute an Effect and fold over the result
* @param block The Effect computation to execute
* @param onError Function to handle error case
* @param onSuccess Function to handle success case
*/
suspend fun <Error, A, B> Effect<Error, A>.fold(
onError: (Error) -> B,
onSuccess: (A) -> B
): B
/**
* Map over the success value of an Effect
* @param transform Function to transform the success value
*/
suspend fun <Error, A, B> Effect<Error, A>.map(
transform: suspend (A) -> B
): Effect<Error, B>
/**
* Bind two Effects together
* @param transform Function that takes success value and returns new Effect
*/
suspend fun <Error, A, B> Effect<Error, A>.flatMap(
transform: suspend (A) -> Effect<Error, B>
): Effect<Error, B>
/**
* Handle errors in an Effect computation
* @param recover Function to recover from errors
*/
suspend fun <Error, A, E2> Effect<Error, A>.handleErrorWith(
recover: suspend (Error) -> Effect<E2, A>
): Effect<E2, A>
/**
* Get the result of an Effect, returning null if it fails
* @return The success value or null if error was raised
*/
suspend fun <Error, A> Effect<Error, A>.getOrNull(): A?
/**
* Merge Effect where Error and A are the same type
* @return The value, whether it was error or success
*/
suspend fun <A> Effect<A, A>.merge(): A
/**
* Execute multiple Effects in parallel, accumulating results
* @param effects Collection of Effects to execute
* @return Effect containing list of all results
*/
suspend fun <Error, A> Collection<Effect<Error, A>>.parZip(): Effect<Error, List<A>>Usage Examples:
import arrow.core.raise.*
import arrow.core.*
import kotlinx.coroutines.*
// Suspended Effect computation
suspend fun fetchUserProfile(userId: String): Effect<String, UserProfile> = effect {
val user = fetchUser(userId) ?: raise("User not found: $userId")
val profile = fetchProfile(user.id) ?: raise("Profile not found for user: $userId")
UserProfile(user.name, profile.bio, profile.avatar)
}
// Using Effect with error handling
suspend fun handleUserRequest(userId: String) {
fetchUserProfile(userId).fold(
onError = { error ->
println("Failed to fetch profile: $error")
respondWithError(error)
},
onSuccess = { profile ->
println("Successfully fetched profile for ${profile.name}")
respondWithProfile(profile)
}
)
}
// Chaining Effects
suspend fun processUserData(userId: String): Effect<String, ProcessedData> = effect {
val profile = fetchUserProfile(userId).bind()
val settings = fetchUserSettings(userId).bind()
val preferences = fetchUserPreferences(userId).bind()
ProcessedData(profile, settings, preferences)
}
// Parallel execution
suspend fun fetchAllData(userIds: List<String>): Effect<String, List<UserProfile>> = effect {
userIds.map { userId ->
fetchUserProfile(userId)
}.parZip().bind()
}
// Error recovery
suspend fun fetchWithFallback(userId: String): Effect<String, UserProfile> = effect {
fetchUserProfile(userId)
.handleErrorWith { error ->
if (error.contains("not found")) {
effect { getDefaultProfile() }
} else {
effect { raise(error) }
}
}
.bind()
}Advanced error accumulation capabilities for collecting multiple errors while continuing computation.
/**
* Raise context for accumulating errors with NonEmptyList
* @param Error The type of errors to accumulate
*/
class RaiseAccumulate<Error> : Raise<NonEmptyList<Error>> {
/**
* Map over a collection, accumulating errors
* @param transform Function that can raise errors
* @return Collection of results or accumulated errors
*/
fun <A, B> Iterable<A>.mapOrAccumulate(
transform: Raise<Error>.(A) -> B
): List<B>
/**
* Zip multiple values, accumulating errors
* @param action Function combining all values
* @return Combined result or accumulated errors
*/
fun <A, B, C> zipOrAccumulate(
first: Raise<Error>.() -> A,
second: Raise<Error>.() -> B,
action: (A, B) -> C
): C
// Additional zipOrAccumulate overloads for 3-10 parameters...
}
/**
* Execute computation with error accumulation
* @param block Computation that can accumulate errors
* @return Either with accumulated errors or success
*/
fun <Error, A> mapOrAccumulate(
block: RaiseAccumulate<Error>.() -> A
): Either<NonEmptyList<Error>, A>Usage Examples:
import arrow.core.raise.*
import arrow.core.*
data class ValidationError(val field: String, val message: String)
fun validateUser(userData: UserData): Either<NonEmptyList<ValidationError>, ValidUser> =
mapOrAccumulate {
zipOrAccumulate(
{ validateName(userData.name) },
{ validateEmail(userData.email) },
{ validateAge(userData.age) }
) { name, email, age ->
ValidUser(name, email, age)
}
}
fun validateName(name: String): Raise<ValidationError>.() -> String = {
if (name.isBlank()) raise(ValidationError("name", "Name cannot be blank"))
if (name.length < 2) raise(ValidationError("name", "Name too short"))
name
}
// Processing multiple items with accumulation
fun validateAllUsers(users: List<UserData>): Either<NonEmptyList<ValidationError>, List<ValidUser>> =
mapOrAccumulate {
users.mapOrAccumulate { userData ->
validateUser(userData).bind()
}
}Additional utility functions for advanced error handling patterns.
/**
* Ensure a condition is true, otherwise raise an error
* @param condition Boolean condition to check
* @param raise Function to produce error if condition is false
*/
fun <Error> Raise<Error>.ensure(condition: Boolean, raise: () -> Error): Unit
/**
* Ensure a value is not null, otherwise raise an error
* @param value Nullable value to check
* @param raise Function to produce error if value is null
* @return Non-null value
*/
fun <Error, A : Any> Raise<Error>.ensureNotNull(value: A?, raise: () -> Error): A
/**
* Transform errors before raising them
* @param transform Function to transform errors
* @param block Computation that can raise transformed errors
*/
inline fun <Error, A, TransformedError> withError(
transform: (Error) -> TransformedError,
block: Raise<Error>.() -> A
): Raise<TransformedError>.() -> A
/**
* Catch exceptions and convert to raised errors
* @param transform Function to convert exception to error
* @param block Computation that might throw exceptions
* @return Result or raised error
*/
inline fun <Error, A> Raise<Error>.catch(
transform: (Throwable) -> Error,
block: () -> A
): A/**
* DSL marker for Raise contexts
*/
@DslMarker
annotation class RaiseDSL
/**
* Marks potentially unsafe Raise operations
*/
annotation class DelicateRaiseApi
/**
* Marks experimental accumulation APIs
*/
annotation class ExperimentalRaiseAccumulateApi
/**
* Marks experimental tracing APIs
*/
annotation class ExperimentalTraceApiInstall with Tessl CLI
npx tessl i tessl/maven-io-arrow-kt--arrow-core-iosarm64@2.1.2