kittinunf / Result

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

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

What about Result<T?>

danneu opened this issue · comments

commented

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.

Actually, (originally dated back a year ago) when I designed Result, I doesn't feel that null is suitable to represent a successful operation.

One of the reasons for that is when I consume Result at the call-site. I usually use fun get(): V = value after checking that it is a successful result, as it also practically "unwrap" value as denoted with V for client. So I don't have to do ?.let or if (result.get() != null) again to make use of the final value.

However, I got and understood your concerns, that in some situation null is pretty much very legitimate. So, I would say that your argument is also valid in the other end.

Options are 1. Extend the use of Result to V? and perhaps add more function to do what I originally want? 2. Create another type to represent this kind of operation, and perhaps make that as a better abstraction for what it actually is?

What do you think?

commented

Another classic use for null is a database lookup Result<User?, Exception> where record-not-found is a query success case, and deadlocks/serialization errors are query fail cases.

Once again, I'm a Kotlin beginner, so this issue is more question than complaint.

One of the reasons for that is when I consume Result at the call-site. I usually use fun get(): V = value after checking that it is a successful result, as it also practically "unwrap" value as denoted with V for client. So I don't have to do ?.let or if (result.get() != null) again to make use of the final value.

I would have thought that if you changed <V : Any> Result<V, Exception> to <V : Any?> Result<V, Exception> (i.e. removed the constraint), then the callsite would only have to use ?.let or if (result.get() != null) if the user themself defined Result<String?, Exception> instead of Result<String, Exception>.

So, in my head, removing the <V : Any> constraint would be a low impact change that would only increase the versatility of the library.

I forked your library, changed all <V : Any> to <V>, changed value.hashCode() to value?.hashCode() ?: 0, and a few other minor tweaks, and it seems to behave as expected.

data class User(val uname: String)
data class Response(val status: Int, val body: String = "")

fun getUser(): Result<User?, Exception> {
    return Result.Success(null)
}

fun handler(request: Request): Response {
    val result = getUser(request.params.get("user_id"))
    
    // handle query failure
    val user = when (result) {
        is Result.Success -> result.get()
        is Result.Failure -> return Response(500, "Internal Error")
    }
    
    // handle record-not-found
    user ?: return Response(404, "Not Found")
    
    // user found
    return Response(200, "Hello, ${user.uname}")
}

I didn't have to touch my existing Result code that had non-nullable success values.

I would say that you have convinced me. I don't see why relaxing the constraint would undermine the use of this library. Since you care enough to open an issue for this feature, I do feel I want to consider your request.

I forked your library, changed all to , changed value.hashCode() to value?.hashCode() ?: 0, and a few other minor tweaks, and it seems to behave as expected.

Would you love to open a PR for that? And we could make it forward from there. Thanks and really hope that you find this library useful.

commented

I've started experimenting with it on my own project. Here's the diff of me replacing kittinunf/Result with my fork to try it out: danneu/kog@1c343b0

Unfortunately Decoder.kt was one of the first things I wrote when I started learning kotlin, so it needs some love. I also think I misuse Result in some places. I'll continue fleshing it out and writing tests to see how I like it.

Any updates?

commented

I think they built it this way such that they could use Kotlin optionals, like result.get()!!, which I can appreciate but don't think I need beyond tests where I know my result is always successful. It also lets you destructure results (value, error) = result which only makes sense if nulls are not valid values.

I have a quick experiment that's defined:

type Result <ValueType, ErrType> = Ok <ValueType> | Err <ErrType>

since I didn't want to constrain the value type nor the error type. This library of course constrains to not-null and Exception, respectively.

But I'm not convinced this is a change this library needs to make since it's a design preference.

I am running into this as well, my case is exactly the db case, where I might get a nullable object returned when there is no result and it should not count as a failure as this is a completely expected state. I do not see any disadvantage by this as it should not affect the code otherwise and I created #63 to fix it.