CtrlK
BlogDocsLog inGet started
Tessl Logo

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

Functional companion to Kotlin's Standard Library providing type-safe error handling and functional programming constructs for iOS x64 target

Pending
Overview
Eval results
Files

raise-dsl.mddocs/

Raise DSL

The Raise DSL provides modern typed error handling with short-circuiting, error recovery, and accumulation patterns. It allows you to work with logical failures in a structured way, replacing exception-based error handling with typed errors.

Core Interface

interface Raise<in Error> {
    // Raise a logical failure and short-circuit
    fun raise(r: Error): Nothing
    
    // Bind operations for extracting success values
    fun <A> Either<Error, A>.bind(): A
    fun <A> Option<A>.bind(): A  // raises None for None case
    fun <A> EagerEffect<Error, A>.bind(): A
    suspend fun <A> Effect<Error, A>.bind(): A
}

// DSL marker
@DslMarker
annotation class RaiseDSL

DSL Builders

// Build Either using Raise DSL
fun <E, A> either(block: Raise<E>.() -> A): Either<E, A>

// Build Option using Raise DSL (raises None)
fun <A> option(block: Raise<None>.() -> A): Option<A>

// Error recovery
fun <E, A> recover(block: Raise<E>.() -> A, recover: (E) -> A): A

// Error transformation
fun <E, EE, A> recover(block: Raise<E>.() -> A, transform: Raise<EE>.(E) -> A): Either<EE, A>

Validation Functions

// In Raise context
fun Raise<Error>.ensure(condition: Boolean, raise: () -> Error)
fun <A> Raise<Error>.ensureNotNull(value: A?, raise: () -> Error): A

Error Accumulation

// Accumulate errors from multiple operations
fun <Error, A, B, Z> mapOrAccumulate(
    a: () -> A,
    b: () -> B,
    transform: (A, B) -> Z
): Either<NonEmptyList<Error>, Z>

// With custom error combining
fun <Error, A, B, Z> mapOrAccumulate(
    combine: (Error, Error) -> Error,
    a: () -> A,
    b: () -> B, 
    transform: (A, B) -> Z
): Either<Error, Z>

Exception Handling

// Catch exceptions in Raise context
fun <E, A> catch(
    raise: (Throwable) -> E,
    block: () -> A
): Either<E, A>

// Catch specific exceptions
fun <E, T : Throwable, A> catch(
    raise: (T) -> E,
    block: () -> A
): Either<E, A>

Usage Examples

Basic Error Handling

import arrow.core.raise.*
import arrow.core.*

// Define error types
sealed class DivisionError {
    object DivisionByZero : DivisionError()
    data class InvalidInput(val input: String) : DivisionError()
}

// Function using Raise DSL
fun Raise<DivisionError>.safeDivide(x: String, y: String): Double {
    val xNum = x.toDoubleOrNull() 
        ?: raise(DivisionError.InvalidInput(x))
    val yNum = y.toDoubleOrNull() 
        ?: raise(DivisionError.InvalidInput(y))
    
    ensure(yNum != 0.0) { DivisionError.DivisionByZero }
    
    return xNum / yNum
}

// Use with either builder
val result1 = either { safeDivide("10", "2") }  // Right(5.0)
val result2 = either { safeDivide("10", "0") }  // Left(DivisionByZero)
val result3 = either { safeDivide("abc", "2") } // Left(InvalidInput("abc"))

Error Recovery

import arrow.core.raise.*
import arrow.core.*

fun Raise<String>.parseAge(input: String): Int {
    val age = input.toIntOrNull() ?: raise("Invalid number format")
    ensure(age >= 0) { "Age cannot be negative" }
    ensure(age <= 150) { "Age seems unrealistic" }
    return age
}

// Recover with fallback value
val age1 = recover({ parseAge("25") }) { error -> 
    println("Error: $error")
    0  // fallback
}  // Result: 25

val age2 = recover({ parseAge("invalid") }) { error ->
    println("Error: $error") 
    18  // default adult age
}  // Result: 18

Binding Operations

import arrow.core.raise.*
import arrow.core.*

data class User(val id: Int, val name: String, val email: String)

fun findUser(id: Int): Either<String, User> = 
    if (id > 0) User(id, "Alice", "alice@example.com").right()
    else "Invalid user ID".left()

fun validateEmail(email: String): Option<String> =
    if (email.contains('@')) email.some() else none()

// Combine operations using bind
fun Raise<String>.processUser(id: Int): String {
    val user = findUser(id).bind()  // Extracts User or raises String error
    val validEmail = validateEmail(user.email).bind()  // Raises None if invalid
    return "User ${user.name} has valid email: $validEmail"
}

val result = either { processUser(1) }
// Right("User Alice has valid email: alice@example.com")

Error Accumulation

import arrow.core.raise.*
import arrow.core.*

data class ValidationError(val field: String, val message: String)
data class UserForm(val name: String, val email: String, val age: Int)

fun Raise<ValidationError>.validateName(name: String): String {
    ensure(name.isNotBlank()) { ValidationError("name", "Name is required") }
    ensure(name.length >= 2) { ValidationError("name", "Name too short") }
    return name
}

fun Raise<ValidationError>.validateEmail(email: String): String {
    ensure(email.isNotBlank()) { ValidationError("email", "Email is required") }
    ensure(email.contains('@')) { ValidationError("email", "Invalid email") }
    return email
}

fun Raise<ValidationError>.validateAge(age: Int): Int {
    ensure(age >= 18) { ValidationError("age", "Must be 18 or older") }
    ensure(age <= 100) { ValidationError("age", "Age seems unrealistic") }
    return age
}

// Accumulate all validation errors
fun validateUserForm(name: String, email: String, age: Int): Either<NonEmptyList<ValidationError>, UserForm> =
    mapOrAccumulate(
        { validateName(name) },
        { validateEmail(email) },
        { validateAge(age) }
    ) { validName, validEmail, validAge ->
        UserForm(validName, validEmail, validAge)
    }

val valid = validateUserForm("Alice", "alice@example.com", 25)
// Right(UserForm("Alice", "alice@example.com", 25))

val invalid = validateUserForm("", "invalid-email", 16)
// Left(NonEmptyList(
//   ValidationError("name", "Name is required"),
//   ValidationError("email", "Invalid email"), 
//   ValidationError("age", "Must be 18 or older")
// ))

Exception Integration

import arrow.core.raise.*
import arrow.core.*

sealed class FileError {
    data class FileNotFound(val path: String) : FileError()
    data class PermissionDenied(val path: String) : FileError()
    data class IOError(val message: String) : FileError()
}

fun Raise<FileError>.readFile(path: String): String {
    return catch(
        raise = { throwable ->
            when (throwable) {
                is java.io.FileNotFoundException -> 
                    FileError.FileNotFound(path)
                is java.io.IOException -> 
                    FileError.IOError(throwable.message ?: "Unknown IO error")
                else -> FileError.IOError("Unexpected error: ${throwable.message}")
            }
        }
    ) {
        // This could throw various exceptions
        java.io.File(path).readText()
    }.bind()
}

val content = either { readFile("config.txt") }

Complex Workflows

import arrow.core.raise.*
import arrow.core.*

// Multi-step process with different error types
sealed class ProcessingError {
    data class ParseError(val input: String) : ProcessingError()
    data class ValidationError(val message: String) : ProcessingError()
    data class TransformationError(val stage: String) : ProcessingError()
}

fun Raise<ProcessingError>.parseInput(input: String): Map<String, Any> {
    ensure(input.isNotBlank()) { ProcessingError.ParseError("Empty input") }
    // Simulate parsing JSON-like input
    return mapOf("data" to input)
}

fun Raise<ProcessingError>.validateData(data: Map<String, Any>): Map<String, Any> {
    ensure(data.containsKey("data")) { 
        ProcessingError.ValidationError("Missing data field") 
    }
    return data
}

fun Raise<ProcessingError>.transformData(data: Map<String, Any>): String {
    val value = data["data"] as? String 
        ?: raise(ProcessingError.TransformationError("Data conversion"))
    return value.uppercase()
}

// Chain operations in Raise context
fun Raise<ProcessingError>.processWorkflow(input: String): String {
    val parsed = parseInput(input)
    val validated = validateData(parsed)
    return transformData(validated) 
}

// Execute with error recovery
val workflow1 = recover({ processWorkflow("hello world") }) { error ->
    when (error) {
        is ProcessingError.ParseError -> "PARSE_ERROR"
        is ProcessingError.ValidationError -> "VALIDATION_ERROR" 
        is ProcessingError.TransformationError -> "TRANSFORM_ERROR"
    }
}  // Result: "HELLO WORLD"

val workflow2 = recover({ processWorkflow("") }) { error ->
    "ERROR: $error"
}  // Result: "ERROR: ParseError(Empty input)"

Integration with Option

import arrow.core.raise.*
import arrow.core.*

fun findUserById(id: Int): Option<String> =
    if (id in 1..10) "user_$id".some() else none()

// Using Option with Raise DSL
fun Raise<String>.processUserChain(id: Int): String {
    val user = findUserById(id).bind()  // Raises "None" if Option is None
    return "Processing: $user"
}

// Handle None as string error
val result1 = recover({ processUserChain(5) }) { 
    "User not found"
}  // Result: "Processing: user_5"

val result2 = recover({ processUserChain(15) }) {
    "User not found" 
}  // Result: "User not found"

Install with Tessl CLI

npx tessl i tessl/maven-io-arrow-kt--arrow-core-iosx64

docs

collection-extensions.md

error-handling.md

index.md

optional-values.md

partial-results.md

product-types.md

raise-dsl.md

safe-collections.md

utility-functions.md

tile.json