When to use coroutine vs. Result type?

Deferred represents a pending operation’s result.”
Result [represents] a finished operation’s result.”

Yes, I understand.

Consider the case where you invoke a background operation, such as a REST call. The request is either going to succeed or fail (i.e. return a value, or produce an exception). Either way, you want to be notified when the operation is complete (a Result). You’re not going to be continually checking the status while the request is outstanding (a Deferred).

Swift 5’s new Result type serves a similar purpose:

If your function returns a Result, it means your function must be a synchronous function or behave like one. If you really want something asynchronous and be “notified” with a Result value, then you have to express that with some async mechanism like Future<Result<T>> (or Deferred equivalent), or coroutines or callbacks, and this choice is independent from the choice of using Result in the first place.

So indeed, when calling your async function, you could register a callback with Result as param type, or you could design your function to return a Deferred immediately and then choose to wait for the value whenever you need it later down the line (this includes the possibility of converting the Deferred to a Result by the way).

Are these the 2 “options” you were comparing in your original question? I guess this has more to do with callbacks VS coroutines than the Result type itself then.

Side note: of course, continually checking/polling is out of the question here, Deferred is not meant for this, but rather to keep a handle on a background task in order to query and wait for its result when you actually need it.

" If your function returns a Result , it means your function must be a synchronous function or behave like one."

That’s not entirely correct. For example, this is an extension function I add to the Activity class in Android to support asynchronous execution. Result is used as the argument to the callback, because it encapsulates either the successful result or the exception that was thrown by the background task:

fun <A: Activity, R> A.doInBackground(task: () -> R, resultHandler: (activity: A?, result: Result<R>) -> Unit) { 
   ... 
}

For example:

doInBackground({
    // return result of long-running operation (e.g. REST call)
}) { activity, result ->
    result.onSuccess { value ->
        // handle success
    }.onFailure { exception ->
        // handle failure
    }
}

Further, Result can’t actually be used as a return type:

Since we can’t return a Result from a method, it can only be used as an argument, which makes it seem like it was designed largely for use in asynchronous callbacks like the one above.

So yes, my question is about callbacks vs. coroutines, but specifically about Result as a callback argument. Why would the language designers introduce both in Kotlin 1.3, when it seems like coroutines are the preferred way of implementing asynchronous behavior?

Well your function clearly does not return a Result (or a collection of Results), since this is an argument to the callback, not in the return type, so I don’t see how this example makes my statement incorrect.

It is not possible to directly return Result as you pointed out, but I believe it is possible to return something like List<Result>. My point only holds for the valid cases of course, but I was strictly speaking about return types.

Anyway, this part of my reply is really besides my main point.

Why? There are plenty of synchronous examples in the design document. I believe the main concern is aggregating exceptions without breaking the “flow of the code”. This can be applied in any callback-like structure, like functional pipelines handling collections of computation results:

fun readFilesCatching(files: List<File>): List<Result<Data>> =
    files.map { 
        runCatching { 
            readFileData(it)
        }
    }

readFilesCatching(files).map { result: Result<Data> -> // type explicitly written here for clarity
    result.map { it.doSomething() } // Operates on Success case, while preserving Failure
}

Please note that this part really is independent from how you got the data in the first place, async or not doesn’t really matter. What I meant in my initial sentence is that the asynchronous mechanism in your code is almost independent from the usage of Result.

You may use Result with a callback mechanism like you did, or you may use Result in the various ways mentioned in its design document, including when converting collections of Deferred values.

I agree that coroutines are the preferred way of implementing async behavior. The reasons for the existence of Result are mentioned in its design document, and I don’t think it mentions the usage as param in a callback for an async operation (or at least not directly).

“I don’t see how this example makes my statement incorrect.”

Fair enough. I read your statement as “if [you are using a] Result, it means your function must be a synchronous function or behave like one”, which clearly isn’t correct. Sorry if that’s not what you meant.

“The reasons for the existence of Result are mentioned in its design document, and I don’t think it mentions the usage as param in a callback for an async operation (or at least not directly).”

I haven’t reviewed the document thoroughly yet. However, the Kotlin and Swift Result types are nearly identical, and the Swift version was definitely created to support returning a success/failure union from an asynchronous operation. I have to imagine that the Kotlin version was at least partially intended to address a similar requirement, but maybe not. I’ll have to dig into the docs and presentations a bit more.

No worries at all, I should have worded that sentence better.

Anyway, as for my answer, this is really just my opinion after I briefly read the design document. I feel like Result is more an answer to get rid of exception handling when not convenient, and it happens to be used in particular in the contet of handling async tasks results after the fact, so there is indeed some kind of link.

However, I don’t believe having the Result type is meant to orient people towards the callback approach, that’s why it doesn’t look like a contradiction with the introduction of coroutines to me.

Actually, it turns out that the first use case in the document is exactly what I am describing:

Continuation and similar callbacks

The primary driver for inclusion of this class into the Standard Library is Continuation<T> callback interface that should get invoked on the successful or failed execution of an asynchronous operation. We’d like to be able to have only a single function with “success or failure” union type as its parameter:

In my example, I’m using a lambda, but the intent is identical.

Indeed, but I rather read that use case as directly useful to implement coroutines Continuation interface, for instance when building coroutine wrappers around existing callback-based implementations.

So this use case, IMO, really goes in the coroutines direction, rather than supporting the use of callbacks in applicative code, but maybe I misinterpreted this.

Based on the document, it does seem like Result could have been created largely to provide an argument to resumeWith(). Maybe I simply assumed that it had a larger purpose because it is so similar to the Swift version.

Thanks for the discussion, by the way. Really appreciate the insights.

1 Like