Skip to content

Deferred has no map function #342

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
svd27 opened this issue Apr 21, 2018 · 11 comments
Closed

Deferred has no map function #342

svd27 opened this issue Apr 21, 2018 · 11 comments
Labels

Comments

@svd27
Copy link

svd27 commented Apr 21, 2018

there should be a function

Deferred<R>.map(map:(R)->T) : Deferred<T>

it is easy to implement yourself, but it is a bit confusing that it is missing.
there might be other functions to consider, like

Deferred<Deferred<T>>.flatten() : Deferred<T>

or

List<Deferred<T>>.combine() : Deferred<List<T>>
@jcornaz
Copy link
Contributor

jcornaz commented Apr 23, 2018

The coroutines philosophy is to design the software architecture around suspend functions and avoid creating function returning Job or Deferred.

If you follow this philosophy, you'll never need to map a Deferred as the Deferred will be created at the most top level and used with await.

Example:

// don't do:
fun foo(): Deferred<Int> = TODO()
fun bar(arg: Int): Deferred<Int> = TODO()
val deferred = foo().flatMap { bar(it) }.map { it + 1 }

// do:
suspend fun foo(): Int = TODO()
suspend fun bar(arg: Int): Int = TODO()
val deferred = async { bar(foo()) + 1 }

I believe this is the reason why there is no promise-like operators in kotlinx.coroutines.

Note that one may not follow this philosophy and create Deferred.map as well as other operators (then, flatMap, etc.). But isn't this losing the benefit of coroutines? In this case what would be the point of coroutines over Futures/Promises?

@jcornaz
Copy link
Contributor

jcornaz commented Apr 24, 2018

Le me add a note explaining why suspending function are preferable to functions returning Futures (incl. Deferred, Job, and others):

Marking a function suspend in Kotlin is semantically the same as making the function returning a future in Java. A suspending function IS a callback function. The only difference, is that the callback mechanism is hidden by the compiler, and we don't actually see the callback, which make the code clearer. But it is a callback.

So, the whole point of Kotlin coroutines is to not use callback and future anymore. Instead we use suspending functions which lets us write code like sequential code while keeping the benefits of future and callbacks.

Therefore declaring a function fun foo(): Deffered<Int> instead of suspend fun foo(): Int is basically loosing the whole point of Kotlin coroutines. This could be done with any future library.

Consider this Java code:

public CompletableFuture<Integer> foo() { ... }
public CompletableFuture<Integer> bar(int arg) { ... }
public CompletableFuture<Object> usage() {
	return foo().thenCompose(x -> bar(x)).thenApply(it -> it + 1);
}

In this code we have to use Future, combine, map etc. or, because there is no better choice.

But the point of kotlin coroutines is to provide the suspend keyword in the language which hide all the complexity.

So here is the Kotlin idiomatic equivalent of the Java code above:

suspend fun foo(): Int { ... }
suspend fun bar(arg: Int) { ... }
suspend fun usage() = bar(foo()) + 1

@elizarov
Copy link
Contributor

elizarov commented Jun 7, 2018

Closing this issue. Thanks to @jcornaz for quite a thorough explanation.

@elizarov elizarov closed this as completed Jun 7, 2018
@SolomonSun2010
Copy link

@jcornaz @elizarov thanks.

Dart has a advanced Asynchrony support:
https://www.dartlang.org/guides/language/language-tour#asynchrony-support

Dart suggest Handling Futures,Handling Streams.
For instance, we could :

foo() async { ... }
bar(arg: Int) async { ... }
usage() async => (await bar((await foo()))) + 1

@elizarov
Copy link
Contributor

@SolomonSun2010 Thank. We've studied Dart design while working on Kotlin coroutines and we had put quite a lot of thought into making sure we don't fall into the same futures trap as Dart did. As a result, the Dart example you've given looks much simpler (and nicer) in Kotlin:

suspend fun foo(): Int { ... }
suspend fun bar(arg: Int) { ... }
suspend fun usage() = bar(foo())

@dalewking
Copy link

dalewking commented Jul 15, 2021

I completely disagree with the notion that one should favor suspend functions and avoid async and Deferred. There is a huge disadvantage of a suspend function is that it is blocking while an async function is not. Async does not block until you call await.

So let's say you have two functions for a call to REST server and we need to call both of them and then combine them. So imagine that we do it with suspend:

    fun suspend restCall1(): String { }
    fun suspend restCall2(): String { }

    val result1 = restCall1()
    val result2 = restCall2()

    return listOf(result1, result2)

or we can define them as async functions that return Deferred:

    fun restCall1(): Deferred<String> { }
    fun restCall2(): Deferred<String> { }

    val result1 = restCall1()
    val result2 = restCall2()

    return listOf(result1.await(), result2.await())

If the 2 API calls take about the same amount of time, the suspend version will take twice as long to execute. In the async case the 2 calls can happen simultaneously instead of sequentially.

So there are very valid reasons to favor async over suspend. So Deferred SHOULD have map

@jcornaz
Copy link
Contributor

jcornaz commented Jul 15, 2021

@dalewking We said there is no need for map. We didn't say it is wrong to use async.

Running stuff in parallel is hard to get right. That's why kotlin coroutines wants us to be explicit about it and use async at the topmost level, making the parallelization explicit.

suspend fun restCall1(): String { delay(1000) ; return "Hello " }
suspend fun restCall2(): String { delay(1000) ; return "world!" }

suspend fun callBoth(): List<String> = coroutineScope {
    val result1 = async { restCall1() }
    val result2 = async { restCall2() }

    listOf(result1.await(), result2.await())
}

@jcornaz
Copy link
Contributor

jcornaz commented Jul 15, 2021

There is a huge disadvantage of a suspend function is that it is blocking while an async function is not.

Also, just to be clear the point of suspend is to NOT block the thread. In my latest example, the delay does not block.

The following code:

suspend fun restCall1(): String { delay(1000) ; return "Hello " }

Is compiled to something that is roughy equivalent to (but simplified):

fun restCall1(callback: (String) -> Unit) {
  delay(1000) { callback("Hello ") }
}

If we were to use Deferred instead it would look like:

fun restCall1(): Deferred<String> {
  return delay(1000) // <-- Assuming delay would return a deferred
    .map { "Hello " }
}

But, thanks to coroutines, we don't need map and it we can write something much clearer:

suspend fun restCall1(): String {
  delay(1000) // <-- delay is a `suspend` function, which does not block
  return "Hello " // <-- No special operator! If something must be done after, then simply do put it on the next line.
}

That's the core idea behind kotlin coroutines, and the reason why suspend exist. If you don't like it, then it means you don't like the very purpose of kotlin coroutines. That's fine, we all have opinions. You can use something different ;-).

@dalewking
Copy link

Also, just to be clear the point of suspend is to NOT block the thread. In my latest example, the delay does not block.

When I said blocking, I did not mean blocking the thread. I simply meant blocking in terms of that the second call is "blocked" from executing until the first call finishes.

@existentialtype
Copy link

Example:

suspend fun foo(): Int = TODO()
suspend fun bar(arg: Int): Int = TODO()
val deferred = async { bar(foo()) + 1 }

In this example, how should I structure it if I have another function, baz, that also needs to receive the value computed by foo()?

suspend fun baz(arg: Int): Int = TODO()

Let's say that foo() is very expensive and I only want to compute it once. And suppose also that the decision to invoke bar() and baz() are made separately at different times, so when bar() is invoked, it is not yet known whether baz() needs to be invoked. That is to say, it is not possible to do:

val deferred = async { 
  val fooResult = foo()
  bar(fooResult)
  baz(fooResult)
}

Because at the point in time when we invoke bar() we don't yet know if baz() should also be called. How do I structure this situation with suspend functions? This is of course a very common scenario with deferred values, where a single value computed at one point in time may be needed by multiple other deferred computations each of which may spring into existence at separate points of time in the future.

@dkhalanskyjb
Copy link
Collaborator

@existentialtype, from later in the thread:

We said there is no need for map. We didn't say it is wrong to use async.

With that in mind, here's how your use case could be covered:

val fooResult = async { foo() }
val barResult = async { bar(fooResult.await()) }
val bazResult = async { baz(fooResult.await()) }

If we provided map, this code would be possible to write:

val fooResult = async { foo() }
val barResult = fooResult.map { bar(it) }
val bazResult = fooResult.map { baz(it) }

But what does this code really do? You can't tell just by reading it without knowing how map is implemented. Some important questions that are hidden by this abstraction:

  • On what dispatcher is this map executed?
  • What scope do I need to cancel to interrupt bar?
  • How many times is bar going to be invoked?

If we take the signature proposed in the original issue request literally, then bar can only be executed directly by the one calling await on barResult, and then it's possible that bar will be called more than once...

So, better not to hide the asynchronous control flow behind higher-order functions that have unclear semantics and save a few characters at most.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

7 participants