Coder Social home page Coder Social logo

michaelbull / kotlin-result Goto Github PK

View Code? Open in Web Editor NEW
977.0 17.0 56.0 909 KB

A multiplatform Result monad for modelling success or failure operations.

License: ISC License

Kotlin 100.00%
kotlin result monad either type class functional-programming functional fp ios

kotlin-result's Introduction

kotlin-result

Maven Central CI License

badge badge badge badge badge badge badge badge badge badge badge badge badge badge

A multiplatform Result monad for modelling success or failure operations.

Installation

repositories {
    mavenCentral()
}

dependencies {
    implementation("com.michael-bull.kotlin-result:kotlin-result:2.0.0")
}

Introduction

In functional programming, the result Result type is a monadic type holding a returned value or an error.

To indicate an operation that succeeded, return an Ok(value) with the successful value. If it failed, return an Err(error) with the error that caused the failure.

This helps to define a clear happy/unhappy path of execution that is commonly referred to as Railway Oriented Programming, whereby the happy and unhappy paths are represented as separate railways.

Overhead

The Result type is modelled as an inline value class. This achieves zero object allocations on the happy path.

A full breakdown, with example output Java code, is available in the Overhead design doc.

Multiplatform Support

kotlin-result targets all three tiers outlined by the Kotlin/Native target support

Read More

Below is a collection of videos & articles authored on the subject of this library. Feel free to open a pull request on GitHub if you would like to include yours.

Mappings are available on the wiki to assist those with experience using the Result type in other languages:

Getting Started

Below is a simple example of how you may use the Result type to model a function that may fail.

fun checkPrivileges(user: User, command: Command): Result<Command, CommandError> {
    return if (user.rank >= command.mininimumRank) {
        Ok(command)
    } else {
        Err(CommandError.InsufficientRank(command.name))
    }
}

When interacting with code outside your control that may throw exceptions, wrap the call with runCatching to capture its execution as a Result<T, Throwable>:

val result: Result<Customer, Throwable> = runCatching {
    customerDb.findById(id = 50) // could throw SQLException or similar
}

Nullable types, such as the find method in the example below, can be converted to a Result using the toResultOr extension function.

val result: Result<Customer, String> = customers
    .find { it.id == id } // returns Customer?
    .toResultOr { "No customer found" }

Transforming Results

Both success and failure results can be transformed within a stage of the railway track. The example below demonstrates how to transform an internal program error UnlockError into the exposed client error IncorrectPassword.

val result: Result<Treasure, UnlockResponse> =
    unlockVault("my-password") // returns Result<Treasure, UnlockError>
    .mapError { IncorrectPassword } // transform UnlockError into IncorrectPassword

Chaining

Results can be chained to produce a "happy path" of execution. For example, the happy path for a user entering commands into an administrative console would consist of: the command being tokenized, the command being registered, the user having sufficient privileges, and the command executing the associated action. The example below uses the checkPrivileges function we defined earlier.

tokenize(command.toLowerCase())
    .andThen(::findCommand)
    .andThen { cmd -> checkPrivileges(loggedInUser, cmd) }
    .andThen { execute(user = loggedInUser, command = cmd, timestamp = LocalDateTime.now()) }
    .mapBoth(
        { output -> printToConsole("returned: $output") },
        { error  -> printToConsole("failed to execute, reason: ${error.reason}") }
    )

Binding (Monad Comprehension)

The binding function allows multiple calls that each return a Result to be chained imperatively. When inside a binding block, the bind() function is accessible on any Result. Each call to bind will attempt to unwrap the Result and store its value, returning early if any Result is an error.

In the example below, should functionX() return an error, then execution will skip both functionY() and functionZ(), instead storing the error from functionX in the variable named sum.

fun functionX(): Result<Int, SumError> { ... }
fun functionY(): Result<Int, SumError> { ... }
fun functionZ(): Result<Int, SumError> { ... }

val sum: Result<Int, SumError> = binding {
    val x = functionX().bind()
    val y = functionY().bind()
    val z = functionZ().bind()
    x + y + z
}

println("The sum is $sum") // prints "The sum is Ok(100)"

The binding function primarily draws inspiration from Bow's binding function, however below is a list of other resources on the topic of monad comprehensions.

Coroutine Binding Support

Use of suspending functions within a coroutineBinding block requires an additional dependency:

dependencies {
    implementation("com.michael-bull.kotlin-result:kotlin-result:2.0.0")
    implementation("com.michael-bull.kotlin-result:kotlin-result-coroutines:2.0.0")
}

The coroutineBinding function runs inside a coroutineScope, facilitating concurrent decomposition of work.

When any call to bind() inside the block fails, the scope fails, cancelling all other children.

The example below demonstrates a computationally expensive function that takes five milliseconds to compute being eagerly cancelled as soon as a smaller function fails in just one millisecond:

suspend fun failsIn5ms(): Result<Int, DomainErrorA> { ... }
suspend fun failsIn1ms(): Result<Int, DomainErrorB> { ... }

runBlocking {
    val result: Result<Int, BindingError> = coroutineBinding { // this creates a new CoroutineScope
        val x = async { failsIn5ms().bind() }
        val y = async { failsIn1ms().bind() }
        x.await() + y.await()
    }

    // result will be Err(DomainErrorB)
}

Inspiration

Inspiration for this library has been drawn from other languages in which the Result monad is present, including:

Improvements on existing solutions such the stdlib include:

  • Reduced runtime overhead with zero object allocations on the happy path
  • Feature parity with Result types from other languages including Elm, Haskell, & Rust
  • Lax constraints on value/error nullability
  • Lax constraints on the error type's inheritance (does not inherit from Exception)
  • Top level Ok and Err functions avoids qualifying usages with Result.Ok/Result.Err respectively
  • Higher-order functions marked with the inline keyword for reduced runtime overhead
  • Extension functions on Iterable & List for folding, combining, partitioning
  • Consistent naming with existing Result libraries from other languages (e.g. map, mapError, mapBoth, mapEither, and, andThen, or, orElse, unwrap)
  • Extensive test suite with almost 100 unit tests covering every library method

Example

The example module contains an implementation of Scott's example application that demonstrates the usage of Result in a real world scenario.

It hosts a ktor server on port 9000 with a /customers endpoint. The endpoint responds to both GET and POST requests with a provided id, e.g. /customers/100. Upserting a customer id of 42 is hardcoded to throw an SQLException to demonstrate how the Result type can map internal program errors to more appropriate user-facing errors.

Contributing

Bug reports and pull requests are welcome on GitHub.

License

This project is available under the terms of the ISC license. See the LICENSE file for the copyright information and licensing terms.

kotlin-result's People

Contributors

05nelsonm avatar avently avatar berikv avatar bitpogo avatar deryeger avatar dimsuz avatar globegitter avatar gregoryinouye avatar gsteckman avatar jhabkin avatar jvanderwee avatar kevinherron avatar kirillzh avatar mguelton avatar michaelbull avatar mrbergin avatar munzey avatar nimelrian avatar pablisco avatar peter-cunderlik-kmed avatar yuitosato avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

kotlin-result's Issues

Assertion helpers for tests

Would you be open to adding helper functions for using Results in tests?

For example:

inline fun <reified V, reified E> Result<V, E>.assertOk(): V {
    assertIs<Ok<V>>(this)
    return value
}

Usage:

fun doSomething(): Result<String, Exception> {
    // ...
}

@Test
fun test() {
    val success: String = doSomething().assertOk()
}

Struggle to run Gradle Check locally

I've tried to run gradlew check locally but I'm encountering some odd issue that doesn't seem to happen on CI:

https://scans.gradle.com/s/xz4bgfv3rbzty/console-log?task=:kotlin-result:linkDebugTestIosX64

:kotlin-result:linkDebugTestIosX64 FAILED
e: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/ld invocation reported errors
Please try to disable compiler caches and rerun the build. To disable compiler caches, add the following line to the gradle.properties file in the project's root directory:
    
    kotlin.native.cacheKind.iosX64=none
    
Also, consider filing an issue with full Gradle log here: https://kotl.in/issue
The /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/ld command returned non-zero exit code: 1.
output:
ld: warning: ignoring file /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/13.0.0/lib/darwin//libclang_rt.ios.a, missing required architecture x86_64 in file /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/13.0.0/lib/darwin//libclang_rt.ios.a (4 slices)
Undefined symbols for architecture x86_64:
  "___cpu_model", referenced from:
      polyHash_x86(int, unsigned short const*) in libstdlib-cache.a(result.o)
ld: symbol(s) not found for architecture x86_64

Could there be something I'm missing? I've tried opening the iOS emulator as I saw people with a similar error had seemed to work. Although I think native here is not necessarily targeting iOS.

Consider adding zipOrAccumulate?

Have you considered implementing a zipOrAccumulate method, which unlike zip, takes all of the Err from the Result arguments and returns them as Result<T, List<Err>>?

zip returns the first Err from the Result arguments, which can be inconvenient when we want to return multiple errors during validation.
For instance, when creating a user instance, if both the name and email address are invalid, we would want to return two errors.

class User(val name: Name, val email: Email) {
  companion object {
    fun create(name: String, email: String): Result<User, List<UserCreateError>> {
      ...
    }
  }
}

Arrow's Either has a zipOrAccumulate method that can solve this problem.

https://arrow-kt.io/learn/typed-errors/working-with-typed-errors/#accumulating-different-computations
https://github.com/arrow-kt/arrow/blob/cb033637ab6b6f1f134b1924c948d6638cc5acf4/arrow-libs/core/arrow-core/src/commonMain/kotlin/arrow/core/Either.kt#L1499

As the implementation is not overly complicated, would you consider including it in kotlin-result?

Here's an example implementation: ZipOrAccumulate.kt

/**
 * Apply a [transformation][transform] to ten [Results][Result], if both [Results][Result] are [Ok].
 * If not, the all arguments which are [Err] will propagate through.
 */
public inline fun <T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, E, Z> zipOrAccumulate(
    producer1: () -> Result<T1, E>,
    producer2: () -> Result<T2, E>,
    producer3: () -> Result<T3, E>,
    producer4: () -> Result<T4, E>,
    producer5: () -> Result<T5, E>,
    producer6: () -> Result<T6, E>,
    producer7: () -> Result<T7, E>,
    producer8: () -> Result<T8, E>,
    producer9: () -> Result<T9, E>,
    producer10: () -> Result<T10, E>,
    transform: (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10) -> Z,
): Result<Z, Collection<E>> {
    contract {
        callsInPlace(producer1, InvocationKind.EXACTLY_ONCE)
        callsInPlace(producer2, InvocationKind.AT_MOST_ONCE)
        callsInPlace(producer3, InvocationKind.AT_MOST_ONCE)
        callsInPlace(producer4, InvocationKind.AT_MOST_ONCE)
        callsInPlace(producer5, InvocationKind.AT_MOST_ONCE)
        callsInPlace(producer6, InvocationKind.AT_MOST_ONCE)
        callsInPlace(producer7, InvocationKind.AT_MOST_ONCE)
        callsInPlace(producer8, InvocationKind.AT_MOST_ONCE)
        callsInPlace(producer9, InvocationKind.AT_MOST_ONCE)
        callsInPlace(producer10, InvocationKind.AT_MOST_ONCE)
        callsInPlace(transform, InvocationKind.AT_MOST_ONCE)
    }

    val result1 = producer1()
    val result2 = producer2()
    val result3 = producer3()
    val result4 = producer4()
    val result5 = producer5()
    val result6 = producer6()
    val result7 = producer7()
    val result8 = producer8()
    val result9 = producer9()
    val result10 = producer10()

    return if (
        result1 is Ok &&
        result2 is Ok &&
        result3 is Ok &&
        result4 is Ok &&
        result5 is Ok &&
        result6 is Ok &&
        result7 is Ok &&
        result8 is Ok &&
        result9 is Ok &&
        result10 is Ok
    ) {
        Ok(
            transform(
                result1.value, result2.value, result3.value,
                result4.value, result5.value, result6.value, 
                result7.value, result8.value, result9.value,
                result10.value,
            ),
        )
    } else {
        Err(
            listOf(
                result1, result2, result3, result4,
                result5, result6, result7, result8,
                result9, result10,
            ).mapNotNull { (it as? Err)?.error },
        )
    }
}

public inline fun <T1, T2, E, Z> zipOrAccumulate(
    producer1: () -> Result<T1, E>,
    producer2: () -> Result<T2, E>,
    transform: (T1, T2, T3) -> Z,
): Result<Z, Collection<E>> { ... }

public inline fun <T1, T2, T3, E, Z> zipOrAccumulate(
    producer1: () -> Result<T1, E>,
    producer2: () -> Result<T2, E>,
    producer3: () -> Result<T3, E>,
    transform: (T1, T2, T3) -> Z,
): Result<Z, Collection<E>> { ... }

public inline fun <T1, T2, T3, T4, E, Z> zipOrAccumulate(...

Extensions for Kotlin Flow?

Have you considered providing extensions for Kotlin Flows, specifically, Flow<Result<V, E>>? I'm making heavy use of these "flows-of-result" and I have some helpers of my own. However, I feel that it would be much nicer to have binding-style utilities that know about Flow. Here are some examples of helpers that we have in our project (admittedly, some of these can have better names!):

Some variants of Flow's combine operators that know about Flow<Result<V, E>>

/**
 * Variant of Kotlin's [combine] that makes it easier to work with flows of [Result].
 *
 * Use this if [transform] never returns an error. If your transform might return an error, consider using [combineResultWithErr] instead.
 */
fun <T1, T2, E, R> combineResult(
    flow: Flow<Result<T1, E>>,
    flow2: Flow<Result<T2, E>>,
    transform: suspend (T1, T2) -> R
): Flow<Result<R, E>> {
    return combine(flow, flow2) { r1, r2 ->
        binding {
            val s1 = r1.bind()
            val s2 = r2.bind()
            transform(s1, s2)
        }
    }
}

/**
 * Variant of Kotlin's [combine] that makes it easier to work with flows of [Result].
 *
 * Use this if [transform] might return an error. If your transform never returns an error, consider using [combineResult] instead.
 */
fun <T1, T2, E, R> combineResultWithErr(
    flow: Flow<Result<T1, E>>,
    flow2: Flow<Result<T2, E>>,
    transform: suspend (T1, T2) -> Result<R, E>
): Flow<Result<R, E>> {
    return combine(flow, flow2) { r1, r2 ->
        binding {
            val s1 = r1.bind()
            val s2 = r2.bind()
            transform(s1, s2).bind()
        }
    }
}

Variant of andThen that can be applied to a Flow<Result<V, E>>

/**
 * Apply [transform] on each element in this [Flow] if the element is an [Ok] otherwise return the
 *  error as-is into the flow.
 */
fun <V, E, U> Flow<Result<V, E>>.andThen(transform: suspend (V) -> Result<U, E>): Flow<Result<U, E>> {
    return map {
        when (it) {
            is Ok -> transform(it.value)
            is Err -> it
        }
    }
}

Variant of flatMapLatest that makes it simpler to work with Result<V, E>

/**
 * Like [andThen] but allows the [transform] to return a `Flow<Result>`. This method applies the
 *  [flatMapLatest] operator allowing you to return a flow from your transform
 */
fun <V, E, U> Flow<Result<V, E>>.andThenFlow(transform: suspend (V) -> Flow<Result<U, E>>): Flow<Result<U, E>> {
    return flatMapLatest {
        when (it) {
            is Ok -> transform(it.value)
            is Err -> flowOf(it)
        }
    }
}

As you can see, these utilities work, but they can be improved a lot. Specifically, they can benefit from the equivalent of binding {} (perhaps something like flowBinding {})

flatMap

I can't see a flatMap function or its equivalent which is typical for monads. It should work like Haskell's >>=.

My suggested implementation:

infix inline fun <V, E, U> Result<V, E>.flatMap(transform: (V) -> Result<U, E>) : Result<U, E> {
    return when (this) {
        is Ok -> transform(value)
        is Err -> this
    }
}

Also flatMapError function could be implemented in simillar way. If you like the idea, I will implement them along with unit tests and create a pull request.

Thanks!

Binding doesn't support coroutines

I was excited to try out the binding block in our codebase but it doesn't support suspending functions. We currently use Result heavily with coroutines so this was a bit of a bummer. Is there any likelihood of this being added in the future? I'm not sure if it's out of scope for this project. I'm happy to raise a PR if you think it would be a worthwhile addition.

Mapping a failure to success given a predicate

While wrapping calls to an external api, one pattern emerged - recoverIf. So it would be similar to toErrorIf and toErrorUnless, and would allow us to selectivly recover from known errors.

Is there any reason that this is not implemented? If not, I'll try to provide a PR during the weekend.

Problem with using kotlin-result via maven

Hello

I am trying to add kotlin-result to my maven project, however strange things starts to happen then. After adding

<dependency>
  <groupId>com.michael-bull.kotlin-result</groupId>
  <artifactId>kotlin-result</artifactId>
  <version>1.1.11</version>
</dependency>

Dependency is added, jar is downloaded , however I cannot import any class - maven just doesn't see classes from com.github.michaelbull package.

My properties for kotlin and java are:

<java.version>11</java.version>
<kotlin.version>1.4.31</kotlin.version>

Do you know what can cause such problems? I have never had such problem with any other library.

Can't find artifact

First of all, thanks for the great work!

As far as I understand I should be able to find the current version (com.michael-bull.kotlin-result:kotlin-result:1.1.8) at maven central.
Unfortunately when looking there I only find the POM and checksum files, but not the JAR. Same for 1.1.7.
Only for 1.1.6 I'm able to find the actual JAR on mane central.

Is this a misunderstanding from my side or where can I find the current JAR?

Multiplatform ArtifactNotFouundException

I'm trying to import com.michael-bull.kotlin-result:kotlin-result:1.1.8 in the commonMain source set in a multiplatform project, but the gradle build is failing with this stacktrace:

Gradle import errors
project ':web': Unable to build Kotlin project configuration
Details: org.gradle.internal.operations.BuildOperationQueueFailure: There was a failure while populating the build operation queue: Could not find kotlin-result-1.1.8-samplessources.jar (com.michael-bull.kotlin-result:kotlin-result:1.1.8).
Searched in the following locations:
https://repo.maven.apache.org/maven2/com/michael-bull/kotlin-result/kotlin-result/1.1.8/kotlin-result-1.1.8-samplessources.jar
Caused by: org.gradle.internal.resolve.ArtifactNotFoundException: Could not find kotlin-result-1.1.8-samplessources.jar (com.michael-bull.kotlin-result:kotlin-result:1.1.8).Searched in the following locations:
https://repo.maven.apache.org/maven2/com/michael-bull/kotlin-result/kotlin-result/1.1.8/kotlin-result-1.1.8-samplessources.jar

I'm using the same dependency in another (JVM-only) project and it works perfectly. Is there something I'm missing with multiplatform configuration that will solve this? Thanks!

getOrThrow?

This can be useful at boundaries, like a rest or graphql handler. I've added an extension function, but this might be useful in the core library. I did a quick search and didn't find any related issues.

fun <V, E> Result<V, E>.getOrThrow(transform : (E) -> Throwable): V = getOrElse { error -> throw transform(error) }

edit: right now I'm using all of these.

fun <V, E> Result<V, E>.getOrThrow(transform: (E) -> Throwable) = getOrElse { throw transform(it) }

fun <V: Any?> V.success() = Ok(this)

fun <E: Any?> E.failure() = Err(this)

inline fun <V: Any, reified E: Throwable> trying(noinline block: () -> V) = trying(E::class, block)

fun <V, E: Throwable> trying(klass: KClass<E>, block: () -> V): Result<V, E> = try {
    block().success()
} catch (e: Throwable) {
    @Suppress("UNCHECKED_CAST")
    when {
        klass.isInstance(e) -> e.failure() as Err<E>
        else -> throw e
    }
}

ensureNotNull-alike binding scope extensions

Arrow has a handy extension for eagerly returning from effect with an error if a value is null - ensureNotNull. Would be very helpful to have a similar extension here, right now need to rely on fairly nested flatMap:

val value = maybeGetValue()
  .flatMap {
    when (it) {
      null -> Err(ValueMissingError)
      else -> Ok(it)
    }
  }
  .bind()

vs

val value = maybeGetValue().bind()
ensureNotNull(value) { ValueMissingError }

Next release?

First of all, thanks for the nice result library you provide!

I would like to use the recoverIf function, which was introduced in one of the last PRs.

Is there a release due in the near future?

Best practices for integrating with Flow<T>

I'm not really sure the best way to achieve this. It could very well be ignorance on my part.

I have an Android room table i want to Flow on. Something like: Flow<Vehicle>
First i need to get the userVehicleId from the User Repository. That isn't a flow, it returns Result<Int, Unit>

The function i have atm looks like this:

suspend fun getUserVehicle(): Flow<Result<Vehicle, Unit>>{ 
   return userRepo.getUserVehicle()              // returns `Result<Int, Unit> `
        .andThen{ id ->
            Ok(vehicleDao.flowOnUserVehicle(id)) // returns Flow<Vehicle, Unit>
        }
}

This isn't valid and won't compile. Since I'm returning Result<Flow<Vehicle, Unit>

Thanks in advance!

Gradle

Copying the following lines:

maven { url "https://jitpack.io" }
compile 'com.github.michaelbull:kotlin-result:1.1.0'

into the repositories and dependencies respectively generated a build error in Gradle.

Build error is as follows:
Warning:root project 'my_project': Unable to build Kotlin project configuration
Details: java.lang.reflect.InvocationTargetException: null
Caused by: org.gradle.api.artifacts.ResolveException: Could not resolve all dependencies for configuration ':compileClasspath'.
Caused by: org.gradle.internal.resolve.ModuleVersionNotFoundException: Could not find com.github.michaelbull:kotlin-result:1.1.0.
Searched in the following locations:
https://repo1.maven.org/maven2/com/github/michaelbull/kotlin-result/1.1.0/kotlin-result-1.1.0.pom
https://repo1.maven.org/maven2/com/github/michaelbull/kotlin-result/1.1.0/kotlin-result-1.1.0.jar
https://jitpack.io/com/github/michaelbull/kotlin-result/1.1.0/kotlin-result-1.1.0.pom
https://jitpack.io/com/github/michaelbull/kotlin-result/1.1.0/kotlin-result-1.1.0.jar
Required by:
project :

Proposal: add getErr() function to UnwrapException

When creating a domain entity with more than five fields (so zip is not available), the best pattern I've found goes like this:

data class T(val a: A, val b) {
  companion object {
    fun create(x1 : String?, x2: String?) : Result<T, DomainMessage> =
      try {
        Ok(T(
          A.create(x1).unwrap(), 
          B.create(x2).unwrap()));
      } catch (e:Exception) {
        Err(ParseError("error: ${e.message}");
      }
}   

What I'd like to be able to do is return the original DomainMessage from the catch block:

      } catch (e: UnwrapException) {
        e.getErr()
      }

Does this seem reasonable to you?

---- Edit: in proposed catch block, change Exception to UnwrapException

`orElse` for Error Type Transformation

Hello.

Currently, Result.orElse has the following signature:

<V, E> Result<V, E>.orElse(transform: (E) -> Result<V, E>): Result<V, E>

However, it does not allow for changing the type of the error. Rust's or_else can change it, so how about modify it to align with that capability?

<T, E, F> Result<T, E>.orElse(transform: (E) -> Result<T, F>): Result<T, F>

thanks!

Move benchmarking to separate sub project

I tried to add a benchmark test for suspendable binding but ended up with cyclical dependencies trying to have kotlin-result jvmBenchmark source set depend on kotlin-result-coroutine project (which already depends on kotlin-result).

Would make life a lot easier to just move benchmarking to its own subproject. It would also simplify the current kotlin-result gradle file which has a bunch of tricks in it already just to get the benchmarking plugin to play nice inside an mpp build.

This would also help delineate dependencies if we decided to benchmark against other implementations (arrow's Either, kotlin's own result type, kittinunf's result type).

Once completed we can look to move the work in #27 to there.

Use of runCatching and coroutines breaks structured concurrency

Since runCatching catches all exceptions, it catches CancellationException which prevents any coroutines launched within the runCatching block from being cancelled when their enclosing CoroutineScope is cancelled.

This is not, strictly speaking, an issue with kotlin-result but it caught me by surprise so I thought it would probably be good to warn others of the potential gotcha.

I propose:

  1. A warning in the kdoc comment, pointing out the problem or,
  2. 1 + a new function in kotlin-result-coroutines called something like runSuspendCatching() which rethrows CancellationException.

I've already written a version of runSuspendCatching() and a test case that demonstrates the behaviour in my own project so I'm more than happy to submit a PR if you want.

The same issue has been raised for the stdlib runCatching at Kotlin/kotlinx.couroutines#1814 but it doesn't seem like the Kotlin devs thinks it's much of a problem. I disagree and thought maybe we could at least improve things here!

Kotlin multi-platform support

This question was already raised (here #5), but still, I am not able to use this library in the Multiplatform project. I think this is due to differences between the android library's build.gradle and the multiplatform one. In the MP library you need to specify kotlin("multiplatform") plugin and divide code into source sets (for that specific library you will need only common source set). You can read more about [https://kotlinlang.org/docs/tutorials/mpp/multiplatform-library.html](Multiplatform Kotlin library) at kotlintlang.org

I've tested this library with MP "structure" and it seems to work, so if you are interested in supporting MP I can create PR for this and you will check out code.

Consider using consistent "error" naming over "failure"?

I've noticed that certain APIs use "failure" terminology (onFailure, fold(failure = ), mapBoth(failure =)), whereas others use "error" terminology (mapError, getError, toErrorIf and more).

It looks like it would be more consistent to use "error" terminology across the board: onError, fold(error = ), mapBoth(error = ) + any other APIs that I might be missing.

runCatching catches Throwable?

Hi, in Java, it is generally consider a bad practice to catch Throwable instead of Exception due to Throwable being the super class of Error which represents lower level errors like OutOfMemory etc.
May I know is there any reason why runCatching is catching Throwable instead of Exception?

Please Refactor to Use Kotlin Result For 1.5+

So with Kotlin 1.5+, the Result type is finally "fully featured". I say that because, with the ability to use it as the return type for methods, it is truly viable as a functional error handling solution. However, having used Kotlin's Result and your Result, I must say that I like your API and feature set far better. However, a lot of the signatures class with Kotlin's (ie, their Result vs your Result) clash, so to use your library I typealias everything (ie, KtResult).

I think it would be wonderful if you were to refactor this to be built on top of the default Kotlin result, with all your great features (ie, flatMap, orElse, etc) as extension functions to it. I can see some challenges, namely that Result is a final class that cannot be extended (for Ok/Err), but I think that the outcome would be worth the effort.

Let me know your thoughts on this. I may be able to find some time to help you if you are interested.

PS. Obviously, given the scale of the breaking changes, you would have to either bump the major version, provide legacy API support, or create a separate 1.5+ artifact.

Discuss: Use coroutines instead of exceptions for binding

Hi!

I would've posted this in discussion if it was available, but I was curious if it had ever been considered to use coroutines instead of exceptions for the binding blocks.

Using exceptions has always seemed slightly error prone to me since all it takes is for someone to catch a generic exception and you now potentially have unexpected behavior. In addition stacktraces can be slightly expensive to produce.

On the flip side, coroutines have their drawbacks as well. For example, the stack traces can sometimes be confusing and there are limitations for where you can actually use bind.

For example I made a proof of concept and tried it out in a repo that made decent usage of binding. The issues are that any function that is not inline or suspend will produce a compilation error because you need to be within the custom coroutine context.

A map function is okay because it's inline, but a sequence function is not

listOf(1, 2, 3).asSequence().map { 
    provideX().bind()
}
Suspension functions can be called only within coroutine body

The other issue I discovered is when the binding context is put into the receiver of another function it fails as well (this is only an issue when restricting the scope of what suspend functions can be called from your scope).

run { 
    provideX().bind()
}

doesn't work but

kotlin.run { 
    provideX().bind()
}

In conclusion, I'm not convinced yet whether one solution is better than the other. I think the coroutine based approach has some benefits over the exception based approach, but it's slightly more restrictive in how you use it. I'm curious to know what you and others think! Thanks for your time.

Here's a link to my POC if you want to try it out. I made both a pure kotlin.coroutines version and a kotlinx.coroutines version.

dronda-t/kotlin-result@master...dronda-t:kotlin-result:dronda/kotlin-coroutines

Implement method into_ok_or_err for Result<T, T>

I think it'd be nice to be able to write something like

val a = res.map { it.joinToString("\n") }.into_ok_or_err()

instead of

val a = when (res) {
    is Ok -> res.unwrap().joinToString("\n") // -> String
    is Err -> res.unwrapError() // -> String
}

This feature already exists in Rust Nightly.

Add a function combining andThen with runCatching

Hello,

First of all, sorry for not fixing that typo in the PR i sent you 😅

A common pattern I see when wrapping library code is the following;

.andThen {
	runCatching {
		// lib code
	}
}

The following creates an annoying typewarning, and looks wonky;

.map {
   // code
}.runCatching {
	if(this is Err) {
		throw this.error
	} else {
		// lib code
	}
}

Would you be willing to accept a PR for something like this? Name TBA, fire if you have any suggestions.
Also, would you place such a function in And.kt or Factory.kt?

.andThenCatching {
	// lib code
}

Missing kotlin-result-mingwx64 on maven

Hello, I'm trying to build a KMP lib but I can't build the native variant (I've been able to build the other variants fine)

Execution failed for task ':compileKotlinNative'.
> Could not resolve all files for configuration ':nativeCompileKlibraries'.
   > Could not resolve com.michael-bull.kotlin-result:kotlin-result:1.1.11.
     Required by:
         project :
      > No matching variant of com.michael-bull.kotlin-result:kotlin-result:1.1.11 was found.
      ...

From what I understand, the mingwx64 variant of kotling-result seems to be missing for maven
https://search.maven.org/search?q=g:com.michael-bull.kotlin-result

Do you plan to add the mingwx64 artifact on maven in the future ?

Mapping a failure to success

This is probably not an issue. Its more of a query i have. I very well could be misusing, misunderstanding or not aware of something.

The use case is if the api call fails i want it to successfully return the cache.

Much like andThen allows the transforming of one thing to another contained withing the Result type. What i feel i am looking for is a way to "andThen the error". So i can transform the E into a success using Ok(SomeType)

I can't find the function though i could just be using it wrong/this is not the correct practice.
For now i'm letting my Error contain any type so it can optionally return the result i want.

Thanks in advance, really loving using kotlin-result so far.

Artifact hosting

Would you consider hosting artifacts not on Jitpack? Jitpack is convenient, but also a convenient mechanism for malicious code injection: artifacts are built somewhere, by some computer neither you (the author) nor me (the consumer) have anything to do with. This is more of a "Trust us, it's fine, probably" model than I'd prefer for artifact delivery.

Hosting release artifacts is pretty straightforward with Bintray, which I (and anyone else with an interest in verifiable artifacts) would prefer.

Multi-platform support?

Hi there, would you provide us any instruction on how to use the library in a multi-platform (JVM+JS) project?

About component1() and 2

Hi,

I was looking at the code and it seems that the component1() and component2() methods in the Result class are not really used anywhere.

I was wondering if they have any use that I'm failing to see here or if they could be removed.

Thanks.

iosSimulatorArm64 and macOsArm64 support

Are there any plans to include iOS Simulator and macOS running on Apple Silicon as kotlin native targets (iosSimultorArm64() and macOsArm64() targets)? This would greatly improve experience of KMM projects developers using this great library :)

e: Could not load module <Error module>

I am getting an obscure error message when attempting to build my Android application after refactoring a Repository interface to return Result: e: Could not load module <Error module>

It's so vague that I can't think of where to look for what could be causing the error. I found this thread which suggests the error is caused by type inheritance failing.

Documentation of order of elements in Iterable.kt

Question rather than an issue: the docs in Iterable.kt for e.g. getAll() mention that the order is preserved. Other methods like combine() don't mention anything about the order.

Is the order guaranteed and it's just that the docs are a bit inconsistent between methods or the order for some methods is not guaranteed?

Should we offer a binding that can bind results of any Error type?

i.e. should we change or offer the following as part of the api:

fun <V> binding(block: ResultBinding.() -> V): Result<V, *>

This would allow 2 things:

  1. no more specifying the types when declaring a binding block:
val result = binding {
  val x = doSomething().bind()
  val y = doSomethingElse().bind()
  x + y
}
  1. doSomething and doSomethingElse can have error types that dont extend from the same type.
    The downside of this is that now the ide will see the type of result in the above example as Result<Int, *>
    But is that so bad?

If we wanted to add this, would it make more sense to replace the current binding with this? or offer a second function. What would they be named? bindingAny? bindingExplicit?

Rename Result to Either

This is a great library and I'm using it for back- and frontend Kotlin code likewise!
One issue I have with it is that the IDE usually picks kotlin.Result as a default for the import and I have to manually add the com.github.michaelbull.result.Result import. Using Either would be closer to the standard for what Result represents (not just closer, exactly spot on ;-). Thanks for considering.

Using fun doSomething(): Result<String, MyException> generates an error

I have a method with this signature:

class MyException(override val message: String?, override val cause: Throwable?) : Exception(message, cause)

  fun doSomething(): Result<String, MyException> {
    val x: Result<String, MyException> = Result.of {
      try {
        "34"
      } catch (e: Exception) {
        throw MyException("some message", e)
      }
    }
  }

I get a compilation error saying:

Error:(22, 19) Kotlin: Type inference failed. Expected type mismatch: inferred type is Result<String, kotlin.Exception /* = java.lang.Exception */> but Result<String, MyException> was expected

is it possible the problem is the definition of Result?

sealed class Result<out V, out E> {
    companion object {

        /**
         * Invokes a [function] and wraps it in a [Result], returning an [Err]
         * if an [Exception] was thrown, otherwise [Ok].
         */
        inline fun <V> of(function: () -> V): Result<V, Exception> {
            return try {
                Ok(function.invoke())
            } catch (ex: Exception) {
                Err(ex)
            }
        }
    }
}

It should not be something like:

inline fun of(function: () -> V): Result<V, out Exception> {

or perhaps I am missing something? I am new to kotlin-result and kotlin. So I cannot say...

Recovery extension

More of a general question rather than an issue.

We use andThen extension quite a lot in our project to chain logic events.
I'm not very experienced in the functional world but If I would explain it in words is sort of "on a success execute this operation and transform into a new Result type"

Is there already any extension that would basically accomplish the reverse? Execute on an error and transform the result?

I'm facing a scenario where I want to try to recover from a failure. But the recovery itself could also still fail. So the code we have atm looks something like this:

Using these types as an example

fun foo(): Result<Int, String> = Err("FooError")
fun recoverFoo(): Result<Int, Int> = Ok(1)

We have this fold:

foo().fold(
    success = { Ok(it) },
    failure = {
        recoverFoo()
            .mapError { "FooRecoveryError" }
    },
)

And I was hoping there would be a cleaner way to write it as maybe something like this:

foo().recoverError {
    recoverFoo()
        .mapError { "FooRecoveryError" }
}

And I guess if the method existed it would look something like this:

public inline infix fun <V, E, U> Result<V, E>.recoverError(transform: (V) -> Result<V, U>): Result<V, U> {
    contract {
        callsInPlace(transform, InvocationKind.AT_MOST_ONCE)
    }

    return when (this) {
        is Ok -> this
        is Err -> transform(value)
    }
}

So I guess my actual question is, with the existing extensions, is there a cleaner way to write this recovery scenario instead of using fold? I looked at mapError but it forces the outcome to also be an Error, and that's not really the idea here, and I wasn't able to see any other alternatives.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.