Functional companion to Kotlin's Standard Library providing core data types and error handling
—
Modern DSL for typed error handling using structured concurrency patterns. Raise provides ergonomic builders that eliminate boilerplate while maintaining type safety, serving as the recommended approach for error handling in Arrow Core.
The foundation of the Raise DSL providing basic error-raising operations.
/**
* Core interface for raising typed errors
*/
interface Raise<in Error> {
/**
* Short-circuit the computation with the given error
*/
@RaiseDSL
fun raise(r: Error): Nothing
/**
* Ensure a condition is true, or raise an error
*/
@RaiseDSL
fun ensure(condition: Boolean, raise: () -> Error): Unit
/**
* Ensure a value is not null, or raise an error
*/
@RaiseDSL
fun <A> ensureNotNull(value: A?, raise: () -> Error): A
}Entry points for creating Raise DSL computations that return different container types.
/**
* Create an Either computation using Raise DSL
*/
fun <Error, A> either(block: Raise<Error>.() -> A): Either<Error, A>
/**
* Create an Option computation using Raise DSL
*/
fun <A> option(block: OptionRaise.() -> A): Option<A>
/**
* Create a nullable computation using Raise DSL
*/
fun <A> nullable(block: NullableRaise.() -> A): A?
/**
* Create a Result computation using Raise DSL
*/
fun <A> result(block: ResultRaise.() -> A): Result<A>
/**
* Create an Ior computation with error combination
*/
fun <Error, A> ior(
combineError: (Error, Error) -> Error,
block: IorRaise<Error>.() -> A
): Ior<Error, A>
/**
* Create an IorNel computation with error accumulation
*/
fun <Error, A> iorNel(
block: IorRaise<NonEmptyList<Error>>.() -> A
): IorNel<Error, A>Usage Examples:
// Either computation
val result = either<String, Int> {
val x = ensure(condition) { "Condition failed" }
val y = ensureNotNull(getValue()) { "Value was null" }
x + y
}
// Option computation
val maybeResult = option<String> {
val value = bindOrRaise(findValue())
value.uppercase()
}
// Result computation with exception handling
val safeResult = result<Int> {
val input = ensureNotNull(getInput()) { IllegalArgumentException("No input") }
input.toInt()
}Extract values from container types or raise their error/empty cases.
/**
* Extract Right value from Either or raise Left
*/
context(Raise<Error>)
fun <Error, A> Either<Error, A>.bind(): A
/**
* Extract Some value from Option or raise None
*/
context(OptionRaise)
fun <A> Option<A>.bind(): A
/**
* Extract non-null value or raise null
*/
context(NullableRaise)
fun <A> A?.bind(): A
/**
* Extract success value from Result or raise failure
*/
context(ResultRaise)
fun <A> Result<A>.bind(): A
/**
* Extract Right value from Ior or raise Left
*/
context(IorRaise<Error>)
fun <Error, A> Ior<Error, A>.bind(): AUsage Examples:
fun parseAndAdd(a: String, b: String): Either<String, Int> = either {
val numA = parseNumber(a).bind() // Extract or raise parse error
val numB = parseNumber(b).bind()
numA + numB
}
fun processUser(id: String): Option<String> = option {
val user = findUser(id).bind() // Extract user or raise None
val profile = getProfile(user.id).bind()
"${user.name} - ${profile.title}"
}
fun parseNumber(s: String): Either<String, Int> = either {
Either.catch { s.toInt() }
.mapLeft { "Invalid number: $s" }
.bind()
}Specific Raise implementations for different error types.
/**
* Raise interface specialized for Option (raises None)
*/
interface OptionRaise : Raise<None> {
override fun raise(r: None): Nothing
}
/**
* Raise interface specialized for nullable types (raises null)
*/
interface NullableRaise : Raise<Null> {
override fun raise(r: Null): Nothing
}
/**
* Raise interface specialized for Result (raises Throwable)
*/
interface ResultRaise : Raise<Throwable> {
override fun raise(r: Throwable): Nothing
}
/**
* Raise interface for Ior computations
*/
interface IorRaise<in Error> : Raise<Error> {
/**
* Add a warning/info value alongside continuing computation
*/
fun <A> info(info: Error, value: A): A
}Accumulate multiple errors instead of short-circuiting on the first error.
/**
* Interface for accumulating errors during computation
*/
interface RaiseAccumulate<in Error> : Raise<Error> {
/**
* Transform each element, accumulating any errors
*/
fun <A, B> Iterable<A>.mapOrAccumulate(
transform: RaiseAccumulate<Error>.(A) -> B
): List<B>
}
/**
* Transform iterable elements, accumulating errors in NonEmptyList
*/
fun <Error, A, B> mapOrAccumulate(
iterable: Iterable<A>,
transform: RaiseAccumulate<Error>.(A) -> B
): Either<NonEmptyList<Error>, List<B>>
/**
* Combine multiple Either values, accumulating errors
*/
fun <Error, A, B, C> zipOrAccumulate(
fa: Either<Error, A>,
fb: Either<Error, B>,
f: (A, B) -> C
): Either<NonEmptyList<Error>, C>
// Similar functions exist for 3-10 parametersUsage Examples:
data class ValidationError(val field: String, val message: String)
fun validateUser(user: UserInput): Either<NonEmptyList<ValidationError>, User> {
return zipOrAccumulate(
validateName(user.name),
validateEmail(user.email),
validateAge(user.age)
) { name, email, age ->
User(name, email, age)
}
}
fun validateEmails(emails: List<String>): Either<NonEmptyList<String>, List<String>> {
return mapOrAccumulate(emails) { email ->
ensure(email.contains("@")) { "Invalid email: $email" }
email
}
}Alternative syntax for bind operations using context receivers.
/**
* Context receiver syntax for Either binding
*/
context(Raise<Error>)
fun <Error, A> Either<Error, A>.bind(): A
/**
* Context receiver syntax for Option binding
*/
context(Raise<None>)
fun <A> Option<A>.bind(): A
/**
* Context receiver syntax for nullable binding
*/
context(Raise<Null>)
fun <A> A?.bind(): AHandle and recover from specific error types within Raise computations.
/**
* Recover from specific error types within a Raise computation
*/
context(Raise<NewError>)
fun <OldError, NewError, A> recover(
block: Raise<OldError>.() -> A,
recover: (OldError) -> A
): A
/**
* Catch and handle exceptions within a Raise computation
*/
context(Raise<Error>)
fun <Error, A> catch(
block: () -> A,
catch: (Throwable) -> Error
): AUsage Examples:
fun processWithFallback(input: String): Either<String, Int> = either {
recover({
// This might raise a parsing error
parseComplexNumber(input).bind()
}) { parseError ->
// Fallback to simple parsing
input.toIntOrNull() ?: raise("Cannot parse: $input")
}
}
fun safeComputation(): Either<String, String> = either {
catch({
riskyOperation()
}) { exception ->
raise("Operation failed: ${exception.message}")
}
}Accumulate validation errors across multiple fields.
fun validateUserRegistration(
name: String,
email: String,
age: Int,
password: String
): Either<NonEmptyList<ValidationError>, User> {
return zipOrAccumulate(
validateName(name),
validateEmail(email),
validateAge(age),
validatePassword(password)
) { validName, validEmail, validAge, validPassword ->
User(validName, validEmail, validAge, validPassword)
}
}Chain operations that can fail, short-circuiting on first error.
fun processUserData(userId: String): Either<ServiceError, ProcessedData> = either {
val user = userService.findUser(userId).bind()
val profile = profileService.getProfile(user.id).bind()
val permissions = authService.getPermissions(user.id).bind()
ensure(permissions.canAccessData) {
ServiceError.Unauthorized("User lacks data access")
}
ProcessedData(user, profile, permissions)
}Handle resource cleanup with proper error propagation.
fun processFile(filename: String): Either<FileError, String> = either {
val file = openFile(filename).bind()
val content = try {
processFileContent(file).bind()
} finally {
file.close()
}
content
}Install with Tessl CLI
npx tessl i tessl/maven-io-arrow-kt--arrow-core