Functional companion to Kotlin's Standard Library providing type-safe error handling and functional programming constructs for iOS x64 target
—
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.
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// 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>// In Raise context
fun Raise<Error>.ensure(condition: Boolean, raise: () -> Error)
fun <A> Raise<Error>.ensureNotNull(value: A?, raise: () -> Error): A// 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>// 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>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"))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: 18import 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")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")
// ))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") }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)"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