kotlin-result

repository·master·Indexed 23 days ago

https://github.com/michaelbull/kotlin-result

A multiplatform Result monad for Kotlin designed for Railway Oriented Programming. It allows for explicit error handling using Ok and Err types, supporting any type as an error rather than just Throwable. The library provides a functional API with map, andThen, binding, and collection extensions, as well as a coroutine-aware module (kotlin-result-coroutines) featuring coroutineBinding and runSuspendCatching. Implemented as an inline value class to minimize runtime overhead.

Tokens
2.3K
Snippets
6
Records
13
Agent score
29%

What's inside kotlin-result

  1. What is the Result monad?

    master

    The Result<V, E> type is a monadic type used for modelling success or failure operations. It follows the concept of Railway Oriented Programming, where execution follows a clear happy path or an unhappy path.

    • Ok(value): Represents a successful operation containing the successful value.
    • Err(error): Represents a failed operation containing the error that caused the failure.
  2. Why use kotlin-result instead of kotlin.Result

    master

    While Kotlin provides a standard library kotlin.Result, kotlin-result is designed for domain-specific error handling and provides several advantages:

    • Error Type Flexibility: Unlike kotlin.Result, which requires errors to be subclasses of Throwable, kotlin-result allows any type to be used as an error.
    • Coroutine Safety: kotlin.Result.runCatching catches CancellationException, which can break coroutine cooperative cancellation. kotlin-result provides runSuspendCatching to handle this correctly.
    • Functional API: Provides a rich set of operators consistent with other functional languages (e.g., map, mapError, mapBoth, mapEither, and, andThen, or, orElse, unwrap).
    • Brevity: Uses top-level Ok and Err functions for instantiation instead of verbose Result.success/Result.failure or Result.Ok/Result.Err calls.
    • Monadic Support: Includes binding and coroutineBinding for imperative-style monadic comprehension.
    • Collection Support: Provides extension functions on Iterable and List for operations like folding, combining, and partitioning.
  3. How Result differs from Either

    master
    Result is an opinionated type that models success as the left generic parameter and failure as the right generic parameter. This removes the ambiguity found in Either types, where developers often disagree on which side should represent success or failure (the 'bias' problem). In kotlin-result, the semantics are fixed: Success is always the left side, and Error is always the right side.
  4. Use coroutine-aware Result extensions

    master

    The kotlin-result-coroutines module provides tools for asynchronous error handling:

    • coroutineBinding: The concurrent equivalent of binding. It runs inside a coroutineScope. If any bind() call fails, the scope is cancelled, cancelling all other children.
    • runSuspendCatching: A coroutine-safe version of runCatching. Unlike the standard library version, it rethrows CancellationException to ensure cooperative cancellation works correctly.
    • Flow Extensions: Provides filterOk, filterErr, onEachOk, onEachErr, combine, and partition for Flow<Result<V, E>>.
    // Dependency required for coroutine support
    dependencies {
        implementation("com.michael-bull.kotlin-result:kotlin-result:2.3.1")
        implementation("com.michael-bull.kotlin-result:kotlin-result-coroutines:2.3.1")
    }
    // coroutineBinding
    suspend fun fetchCustomerProfile(id: CustomerId): Result<CustomerProfile, DomainMessage> {
        return coroutineBinding {
            val customer = async { findById(id) }
            val orders = async { findOrderHistory(id) }
            CustomerProfile(customer.await(), orders.await())
        }
    }
    
    // runSuspendCatching
    suspend fun findCustomer(id: CustomerId): Result<CustomerEntity, Throwable> {
        return runSuspendCatching {
            repository.findById(id)
        }
    }
  5. Create Results using Ok, Err, and runCatching

    master

    You can manually create results using Ok and Err, or use helper functions to wrap existing logic:

    1. Manual creation: Return Ok(value) for success and Err(error) for failure.
    2. runCatching: Wrap code that may throw exceptions to capture the result as a Result<T, Throwable>.
    3. toResultOr: Convert nullable types into a Result by providing an error value if the type is null.
  6. Install kotlin-result

    master

    Add the following dependency to your build.gradle file to use the core Result monad. Ensure mavenCentral() is included in your repositories.

    Note: If you require coroutine support, you must also install the kotlin-result-coroutines artifact.

    repositories {
        mavenCentral()
    }
    
    dependencies {
        implementation("com.michael-bull.kotlin-result:kotlin-result:2.3.1")
    }
  7. Transform Results with map, mapError, and mapBoth

    master

    Use these functions to transform the contents of a Result without manually unwrapping it:

    • map: Transforms the success value.
    • mapError: Transforms the error value into a different type.
    • mapBoth (or fold): Handles both success and error cases to produce a single value (e.g., mapping a Result to an HTTP response).
  8. Combine multiple Results with zip and zipOrAccumulate

    master

    Use these functions to combine multiple independent results:

    • zip: Returns the combined result or returns early with the first error encountered.
    • zipOrAccumulate: Combines results but collects all errors instead of stopping at the first one.

    Both support 2-5 arity.

    // zip (stops at first error)
    fun validate(dto: CustomerDto): Result<Customer, DomainMessage> {
        return zip(
            { PersonalNameParser.parse(dto.firstName, dto.lastName) },
            { EmailAddressParser.parse(dto.email) },
            ::Customer
        )
    }
    
    // zipOrAccumulate (collects all errors)
    fun validate(dto: CustomerDto): Result<Customer, List<DomainMessage>> {
        return zipOrAccumulate(
            { PersonalNameParser.parse(dto.firstName, dto.lastName) },
            { EmailAddressParser.parse(dto.email) },
            ::Customer
        )
    }
  9. Use binding for non-linear logic

    master

    When a chain is not linear and later steps need access to intermediate values from earlier steps, use the binding function. This provides an imperative-style block where .bind() unwraps a Result into a named variable. If any .bind() call fails, the entire block short-circuits immediately.

    fun save(id: Long, dto: CustomerDto): Result<Event?, DomainMessage> = binding {
        val customerId = parseCustomerId(id).bind()
        val existing = findById(customerId).bind()
        val validated = validate(dto).bind()
        updateEntity(customerId, existing, validated)
    }
  10. Chain operations with andThen

    master

    Use andThen to chain operations where each step depends on the success of the previous one. This is ideal for linear pipelines where the output of one step is the input for the next.

    val (status, body) = call.parameters
        .readId()
        .andThen(::parseCustomerId)
        .andThen(::findById)
        .map(::entityToDto)
        .mapBoth(::customerToResponse, ::messageToResponse)
  11. Work with collections of Results

    master

    The library provides extension functions for Iterable<Result<V, E>>:

    • combine(): Turns a List<Result<V, E>> into a Result<List<V>, E>, returning early with the first error.
    • partition(): Splits results into a Pair<List<V>, List<E>> (valid values and errors).
    • filterOk(): Extracts only the successful values.
    • filterErr(): Extracts only the errors.
    • Other functions: allOk, anyOk, countOk, countErr, onEachOk, onEachErr.