Giter Site home page Giter Site logo

kittinunf / result Goto Github PK

View Code? Open in Web Editor NEW
898.0 16.0 51.0 618 KB

The modelling for success/failure of operations in Kotlin and KMM (Kotlin Multiplatform Mobile)

License: Other

Kotlin 100.00%
kotlin result functional optional kotlin-multiplatform-library kotlin-multiplatform

result's Introduction

Result

Kotlin MavenCentral Run Gradle Codecov

This is a tiny framework for modelling success/failure of operations in Kotlin. In short, it is a model in type of Result<V: Any?, E : Throwable>.

Ideology

Result<V: Any?, E: Throwable> is to provide higher abstraction of operation that can be ended with result either success or failure. Result.Success represents value in case of success, and Result.Failure represents error in case of failure which is upper bounded with Throwable type.

Installation

Gradle

repositories {
    mavenCentral()
}

dependencies {
    // if you are working on JVM or Android only project
    implementation("com.github.kittinunf.result:result-jvm:«version»") //for JVM support
    
    // if you are working in KMM project
    implementation("com.github.kittinunf.result:result:«version»") //for Kotlin Multiplatform support
}

TL;DR

This model is highly inspired by "Railway Oriented Programming" concept.

Result allows one to express series of success/failure operations in Kotlin as;

Result.of<T, Throwable>(doOperation())
      .flatMap { normalizedData(it) }
      .map { createRequestFromData(it) }
      .flatMap { database.updateFromRequest(it) }

Work with Result is easy

//multi-declaration
val (value, error) = result

//get
val value: Int = result.get() // throw exception if error

//terminal operator
//success
result.success {
}

//failure
result.failure {
}

//fold is there, if you want to handle both success and failure
result.fold({ value ->
    //do something with value
}, { err ->
    //do something with err
})

Why

Result is suitable whenever there is a need to represent an operation that has the possibility of failure. Error handling can be cumbersome to work with. Result helps process the operations in a nice, functional way, while maintaining readability to your code.

Let's consider a need to read data from foo, and to perform some further validation

fun process(): String {
    try {
        val foo = File("/path/to/file/foo.txt").readText()
        val isSuccessful = processData(foo)
        if (!isSuccessful) {
            return "Data is corrupted and cannot be processed"
        }
    } catch (e: Throwable) {
        //do something if error
        Logger.log(ERROR, e.message())
    }
}

However, things start getting ugly when we have chain of operations being run sequentially, such as

fun process(): String {
    try {
        val foo = File("/path/to/file/foo.txt").readText()
        val isSuccessful = normalizedData(foo)
        if (!isSuccessful) {
            return "Data cannot be processable"
        }
        val request = createRequestFromData(foo)
        try {
            val result = database.updateFromRequest(request)
            if (!result) {
                return "Record in DB is not found"
            }
        } catch (dbEx: DBThrowable) {
            return "DB error, cannot update"
        }
    } catch (e: Throwable) {
        //do something if error
        Logger.log(ERROR, e.message())
    }
}

Ouch, it looks pretty bleak.

Let's see how Result can help us.

First, we break things down into a small set of model in Result.

  • Read a file
val operation = { File("/path/to/file/foo.txt").readText() }
Result.of { operation() }  // Result<String, FileThrowable>
  • Normalize a data
fun normalizedData(foo): Result<Boolean, NormalizedThrowable> {
    Result.of { foo.normalize() }
}
  • Create a request from data
fun createRequestFromData(foo): Request {
    return createRequest(foo)
}
  • Update DB with Request
fun database.updateFromRequest(request): Result<Boolean, DBThrowable> {
    val transaction = request.transaction
    return Result.of { 
        db.openTransaction {
            val success = db.execute(transaction)
            if (!success) {
                throw DBThrowable("Error")
            }
            return success
        }
    }
}

The whole operation can be chained by the following;

Result.of { doOperation() }
      .flatMap { normalizedData(it) }
      .map { createRequestFromData(it) }
      .flatMap { database.updateFromRequest(it) }

The creates a nice "happy path" of the whole chain, also handle error as appropriate. It looks better and cleaner, right?.

Never Fail Operation

In some case, one wants to model an always successful operation. Result<V: Any?, NoException> is a good idea for that. NoException is to indicate that there is no exception to throw. E.g.

// Add operation can never be failure
fun add(i: Int, j: Int) : Result<Int, NoException>

Nice thing about modelling in this way is to be able to compose it with others "fail-able" operations in Result.

High Order functions

Success

map and flatMap

map transforms Result with given transformation (V) -> U. As a result, we are able to transform V into a new V in the case where Result is Result.Success. When Result is Result.Failure, error is re-wrapped into a new Result.

flatMap is similar to map, however it requires transformation in type of (V) -> Result<U, ...> .

Failure

mapError and flatMapError

mapError ((E) -> E2) and flatMapError ((E) -> Result<E2, ...>) are counterpart of map and flatMap. However, they are operate on Result.Failure. It is quite handy when one needs to do some transformation on given Throwable into a custom type of Throwable that suits ones' need.

More features

Please check out more features in the ResultTest

Railway Oriented Programming

If interested, here are more articles that one might enjoy.

Credit to Scott Wlaschin

Credits

Result is brought to you by contributors.

License

Result is released under the MIT license.

result's People

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

result's Issues

Does not work with kotlin 1.3.20

The other fuel libraries have been updated to 2.0.1 and they support kotlin 1.3.20, the update hasn't come for this result library yet.

Provide an idiomatic way to flatten List<Result>

From kittinunf/fuel#805:

It'd be nice to able to do the following:

  1. Transform List<Result<Foo, FuelError>> into Result<List<Foo>, FuelError>. If any of the Results is an error, the final Result is an error too, sort of an AND.
  2. Transform List<Result<Foo, FuelError>> into Result<List<Foo>, FuelError>. If any of the Results is a success, the final Result is a success too, sort of an OR.

Add support of result. isSuccess()

Hi @kittinunf

Thanks for an awesome library 👍
It is really useful and that's why I even created Java version of it https://github.com/amatkivskiy/resultforjava .

There are multiple things that I found really useful when I was creating Java version of it:

What do you think about this?
If this is ok, I can create a pull request with mentioned changes.

[Idea] Suspending function support

Result currently does not support suspending functions.

The following example won't compile:

suspend fun aSuspendingFunction() : Something

val result = Result.of { aSuspendingFunction() } //-> "Suspension functions can be called only within coroutine body"

Since Result is nice, for instance to work with web service calls, it would be very cool to support this new Kotlin feature right out of the box.

I'm currently using this workaround to work with Result in conjunction with coroutines:

val result = resultOf { aSuspendingFunction() }

private suspend fun <V : Any> resultOf(f: suspend () -> V): Result<V, Exception> = try {
    Success(f())
  } catch (e: Exception){
    Failure(e)
  }

What do you think? If you like the idea I could prepare an according PR.

Support Javascript Backend

I am planning to implement Result on upcoming Fuel's MPP project.

  1. JS(IR) with Browser
  2. Fixing Test by removing whitespace

Map that throws?

Hi, I was surprised to find that this code would not print "Error":

val result = Result<Int, Exception>.of(0)
result.map {
    transform(it)   // transform throws
}.failure {
    print("Error")
}

fun transform(i: Int) {
    throw Exception()
}

It looks like map does not expect to handle errors. What I had to do instead was:

val result = Result<Int, Exception>.of(0)
result.flatMap {
    Result.of {
        transform(it)
    }
}.failure {
    print("Error")
}

which looks less elegant. More importantly, in the first case, there is no error shown anywhere so it's difficult to debug.

`flatMapError` should take value type (V) to consideration

Lib version com.github.kittinunf.result:result-jvm:5.1.0

flatMapError returns * type for value that makes it impossible to use in chains. Original implementation was useless for me, I had to provide my own:

inline fun <V, reified E : Throwable, reified EE : Throwable> Result<V, E>.rightFlatMapError(
    transform: (E) -> Result<V, EE>
): Result<V, EE> =
    try {
        when (this) {
            is Result.Success -> Result.success(value)
            is Result.Failure -> transform(error)
        }
    } catch (ex: Exception) {
        when (ex) {
            is EE -> Result.failure(ex)
            else -> throw ex
        }
    }

Just compare to original:

inline fun <reified E : Throwable, reified EE : Throwable> Result<*, E>.flatMapError(transform: (E) -> Result<*, EE>): Result<*, EE> = try {
    when (this) {
        is Result.Success -> Result.success(value)
        is Result.Failure -> transform(error)
    }
} catch (ex: Exception) {
    when (ex) {
        is EE -> Result.failure(ex)
        else -> throw ex
    }
}

Btw, thanks for the lib! Fix it, please.

Artifact result-coroutines not found

Hi,

Firstly I would like to congratulate you for the awesome project!

I'm trying to use result-coroutines in my project, but Gradle cannot download the artifact. Any idea?

I have the following repositories:

repositories {
    google()
    jcenter()
    maven { url "http://kotlin.bintray.com/kotlinx" }
}

Update to kotlin 1.3

Kotlin 1.3 has been recently released and coroutines have been promoted to stable now. Coroutines API before Kotlin 1.3 is deprecated

Opinion on stdlib `kotlin.Result<T>` in kotlin 1.3

Kotlin 1.3 introduces a new built in type called Result<T> (docs) in the standard library (stdlib) and in the kotlin package.

Do you have an opinion on it? It seems to have taken a lot of inspiration from this library, but with one big difference: It doesn't have an equivalent to flatMap for operations that return a new Result<T>. It also doesn't place any constraints on errors, which are typed as Throwable.

The immediate inconvenience after upgrading my project to kotlin 1.3 is that I now need to explicitly add the import to use com.github.kittinunf.result.Result.

The KEEP can be seen here: https://github.com/Kotlin/KEEP/blob/master/proposals/stdlib/result.md

Dealing with unexpected exceptions more elegantly

Whenever an unexpected exception gets thrown while evaluating a function with Result it leads to an unintuitive ClassCastException. Here is a simple example:

import com.github.kittinunf.result.Result
import org.junit.jupiter.api.Test
import kotlin.test.fail

internal class ResultExceptionClassCastTest {
    class MyException: Exception()

    @Test
    fun `Unexpected exception leads to unintuitive ClassCastException`() {
        Result.of<String, MyException> {
            operation("something")
        }.fold({}, {
            fail("Not reached")
        })
    }

    private fun operation(it: String): String {
        throw IllegalArgumentException()
        return "never reached $it"
    }
}

Is there something that I as a library user can do to handle this better? Is this an issue with the library?

The simple solution would be to expect an Exception (or Throwable!), and then to cast to the expected type, if it indeed is that type, but this doesn't seem like a very good solution.

What about adding a label to each validation?

Hello guys,

I've some validations like:

    val factoryIdExists: Result<Long, Exception> = Result.of(productDto.factory.factoryId)
    val factoryExists: Result<FactoryDto, Exception> = 
    Result.of(factoryService.getFactory(factoryIdExists.get()))
    val validations = Validation(factoryIdExists, factoryExists)

What if we could add a label to each validation, so, it would be easier to present the validation that failed to an API client, for example. Do you think that it would be worth? If so, I can design it.

Thanks!

Android Studio always tried to import kotlin.Result

This is more a question.
Probably somebody figured out how to tell to Android Studio that I'm using com.github.kittinunf.result.Result instead of the default one.
There is no issue if I'm typing Result symbol by symbol - then I can just choose appropriate import.
However, if I insert code from browser or other place (not AS), then AS always uses default Result.
Is there a way to substitute it for all the cases by com.github.kittinunf.result.Result?

Result.map {} can cause a ClassCastException if an exception that isn't of the right type is thrown inside it

It would be useful to add a function, let's say for example mapSafe that could handle an exception of another type, for example:

suspend fun <V : Any, U : Any, E : Exception, D : Exception> SuspendableResult<V, E>.mapSafe(transform: suspend (V) -> U): SuspendableResult<U, D> = try {
    when (this) {
        is SuspendableResult.Success -> SuspendableResult.Success(transform(value))
        is SuspendableResult.Failure -> SuspendableResult.Failure(error)
    }
} catch (ex: Exception) {
    SuspendableResult.error(ex as D)
}

Haven't tested this. Thoughts ?

getOrNull

Hey, it would be nice if I could do result.getOrNull or result.getOrElse(null) or similar as sometimes nulls are used to show there is an error instead of an exception being thrown.

Error Response Handling

With Previous Version I was successfully getting error response via response.data in code like this

Fuel.post(path, keyValues)
                        .responseJson { request, response, result ->
when (result) {
                                is Result.Failure -> {
                                    //TODO: Need attention to fix response error
                                   val errorMessage = parse(String(response.data))?.get("error")
                                
                                }
                                is Result.Success -> {
                                    val myResult = result.value.obj()

                                }
}

But with latest version of Fuel it not working in android. I tried following code to get error code on Result Failure but it not working. Kindly guide me. Thanks

result.fold(success = { json ->
                                callback.onCompleted(ConstantData.APICallJSONResult(json.obj()))
                            }, failure = { error ->

                                Log.e("Failure", error.toString())
                                Log.e("Failure", error.localizedMessage)
                                Log.e("Failure", error.message)
                                Log.e("Failure", error.response.data.size.toString())


                            })

My json response is when Result Failure with status code 400 or 422

{
    "status": false,
    "error": "Something Wrong!"
}

Question: Proper usage of SuspendResult?

I'm migrating away from using ArrowKt's Either type so I don't have such a large dependency for a single purpose. However, I'm struggling to find a purpose for SuspendResult. I'm using coroutines in my application with no issues just using Result. For instance, I've got:

managedLaunch {
    Try     //typealias for Result to prevent clashing
        .of(fetchProfessionsUseCase.execute())     //execute is suspending
        .apply {
            failure {    //chain failure clause
                requireState()
                    .copy(error = it)
                    .publish()
            }
        }
        .map(::prepareFetchedCategories)  //non-suspending method reference
        .map(::showCategories)  //non-suspending method reference
}

If I use SuspendResult, calling .map requires another suspend function, which I find a bit unnecessary. If I use a normal Result, I am free to call a suspend function like execute(), as long as we're inside a CoroutineScope, and use non-suspending functions in my .map calls. Am I missing something or using these constructs incorrectly?

Edit: Actually, my failure clause never gets triggered when I use a suspendCoroutine and resumeWithException, though try/catch blocks do catch the error, so I know it is thrown.

[Feature request] add or() that takes other Result as argument

Please consider adding the following function:

/**
 * Take this if it is [Result.Success] or [fallback] if this is [Result.Failure]
 */
infix fun <V : Any, E : Exception> Result<V, E>.or(fallback: Result<V, E>): Result<V, E> {
    return when (this) {
        is Result.Success -> this
        else -> fallback
    }
}

/**
 * Take this if it is [Result.Success] or calculate [fallback] if this is [Result.Failure]
 */
infix fun <V : Any, E : Exception> Result<V, E>.or(fallback: () -> Result<V, E>): Result<V, E> {
    return when (this) {
        is Result.Success -> this
        else -> fallback()
    }
}

As this is quite useful when your fallback has to be calculated as well, and implementation is too cumbersome to be implemented on the spot.

What about Result<T?>

I'm new to Kotlin, so I may be missing something obvious, but it seems to me that Result values are constrained to Any rather than Any? because of <V : Any>.

I believe this means that I can't have a result of Result<String?, Exception>.

What should we do when null is a legal value?

I've been writing a JSON decoder combinator where Decoder<T> means that the decoder will return Result<T, Exception> when applied to a JSON string. Since JSON can contain nulls, I want to implement a null combinator such as Decoder.nullable(Decoder.string) which returns Result<String?, Exception>.

However, I wasn't sure how to make this work beyond bringing in some sort of Maybe<T> : Just<T> | None monad so that I could return Result<Maybe<String>, Exception>. I'd prefer to just use Kotlin's optional here since it already exists.

Can't find coroutines 2.1.0 on jcenter

Hi,

com.github.kittinunf.result:result-coroutines:2.1.0 doesn't exist on jcenter. I can only find version 2.0.0 on a mulesoft repo. Did you forget to publish it?

Thank you.

Enforced nullability on get()'s return type

I just migrated from 2.2.0 to the current latest version (5.1.0) and the main diference I saw was the nullable success, which I found just great!

But, one thing that I didn't get was the signature change on the get() method. Given that Result.Success<V>'s V can now be a nullable type, why does the Result's abstract get() has to have an "enforced" nullable return (: V?).

This feels somewhat counter intuitive to me, given that, even if I have a non-nullable Result value (e.g.Result<String, Exception>), its get() will always return a nullable one (String?), which just includes an (apparently) unnecessary complexity to handle the return.

Is there a particular reason for this design? Perhaps some use case that I'm not seeing right now? If no, I would like to propose a change removing this enforced nullability on get()'s return type: #91

Provide functions to unwrap value or error

It would be interesting to provide functions to unwrap value or error.

inline fun <V, E> Result<V, E>.unwrap(failure: (E) -> Nothing): T
inline fun <V, E> Result<V, E>.unwrapError(success: (V) -> Nothing): E

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.