CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-io-arrow-kt--arrow-core

Functional companion to Kotlin's Standard Library providing core data types and error handling

Pending
Overview
Eval results
Files

raise-dsl.mddocs/

Raise DSL for Typed 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.

Capabilities

Core Raise Interface

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
}

DSL Builders

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()
}

Bind Operations

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(): A

Usage 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()
}

Specialized Raise Types

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
}

Error Accumulation

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 parameters

Usage 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
    }
}

Contextual Binding

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(): A

Recovery and Catch

Handle 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
): A

Usage 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}")
    }
}

Error Handling Patterns

Validation Pattern

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 Pattern

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)
}

Resource Management Pattern

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

docs

collections.md

error-handling.md

functional-utilities.md

inclusive-or.md

index.md

optional-values.md

raise-dsl.md

tile.json