2

I'm trying to convert a CompletableFuture<Optional<T>> to a Flow<T?>. The extension function I'm trying to write is

fun <T> CompletableFuture<Optional<T>>.asFlowOfNullable(): Flow<T?> =
    this.toMono().map { (if (it.isPresent) it.get() else null) }.asFlow()

but it fails because asFlow() doesn't exist for nullable types, AFAICT based on its definition.

So, how do I convert CompletableFuture<Optional<T>> to Flow<T?>?

Edit 1:

Here's what I've come up with so far. Feedback appreciated.

import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import java.util.Optional
import java.util.concurrent.CompletableFuture

fun <T> Optional<T>.orNull(): T? = orElse(null)

fun <T> CompletableFuture<Optional<T>>.asFlowOfNullable(): Flow<T?> = flowOf(this.join().orNull())

FYI, in my case, which is using Axon's Kotlin extension queryOptional, I can now write this:

inline fun <reified R, reified Q> findById(q: Q, qgw: QueryGateway): Flow<R?> {
    return qgw.queryOptional<R, Q>(q).asFlowOfNullable()
}

I'll defer for a while creating a comment with the above pattern as the answer to allow for feedback.

Edit 2: Since it was pointed out below that asFlowOfNullable in Edit 1 would block the thread, I'm going with this from @Joffrey for now:

fun <T> Optional<T>.orNull(): T? = orElse(null)

fun <T> CompletableFuture<Optional<T>>.asDeferredOfNullable(): Deferred<T?> = thenApply { it.orNull() }.asDeferred()

Edit 3: credit to both @Tenfour04 & @Joffrey for their helpful input. :)

Matthew Adams
  • 2,059
  • 2
  • 21
  • 31
  • In Kotlin, we usually don't use `Flow` to represent a single item. It's more natural to use a simple suspend function that returns the value. Why do you need a flow here? – Joffrey Jul 27 '21 at 07:47
  • Because I'm using Axon's Kotlin extension to call `QueryGateway.query(query:Q): CompletableFuture` where there is only a single item or null. – Matthew Adams Jul 27 '21 at 12:30
  • @Joffrey see https://docs.axoniq.io/reference-guide/extensions/kotlin#querygateway – Matthew Adams Jul 27 '21 at 12:40
  • 1
    That explains why you have a CompletableFuture, but not why you want to convert it to a Flow instead of a suspend function or Deferred. – Tenfour04 Jul 27 '21 at 12:58
  • 1
    Your code in Edit 1 will block the calling thread until completion because of the `join` call. – Tenfour04 Jul 27 '21 at 13:34
  • @Tenfour04 Regarding Edit 1: yeah, that's what I was afraid after I thought about it for a while. – Matthew Adams Jul 27 '21 at 14:54
  • Regarding your edit 2, it would be interesting to see how you're using this helper function. If you're not passing around the deferred value, you will most likely `await()` it almost right away (suspending the coroutine), which means you could simply have used a suspending `await()` function on the `CompletableFuture` directly instead converting to `Deferred`. – Joffrey Jul 27 '21 at 15:15

1 Answers1

2

To use the below extensions, you need the jdk8 coroutines library:

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$1.5.0"

I'm not sure where the asFlow() function comes from that you're using, but here's a way I think would work without it. It seems a little odd to me to have a Flow of a single item, because it could just be a suspend function or if you need it as an object to pass around, a Deferred, which is intended for returning a single result and is therefore more analogous to a Future than a Flow.

fun <T> CompletableFuture<Optional<T>>.asFlowOfNullable(): Flow<T?> =
    flow { emit(await().orElse(null)) }

As a suspend function:

suspend fun <T> CompletableFuture<Optional<T>>.awaitNullable(): T? = 
    await().orElse(null))

As a deferred:

fun <T> CompletableFuture<Optional<T>>.asDeferredNullable(): Deferred<T?> =
    thenApply { it.orElse(null) }.asDeferred()
Tenfour04
  • 83,111
  • 11
  • 94
  • 154
  • See my reply above for motivation. I tried both of your samples, and got the following compile error: `Kotlin: Unresolved reference: await`. Am I missing some import? – Matthew Adams Jul 27 '21 at 12:35
  • `asFlow()` is coming from `import kotlinx.coroutines.reactive.asFlow` – Matthew Adams Jul 27 '21 at 12:41
  • @MatthewAdams You need the `kotlinx-coroutines-jdk8` library. – Tenfour04 Jul 27 '21 at 12:45
  • What are your thoughts about using `Deferred` versus a `Flow` that will only ever have a single value? – Matthew Adams Jul 27 '21 at 14:18
  • 1
    The Flow is awkward. The only case I can think of where it would make sense is you are passing it to some API that only accepts Flow. Even the Deferred has limited cases where it would make more sense than simply exposing a suspend function. You only need Deferred if you have multiple asynchronous actions going on at once that you need to coordinate. – Tenfour04 Jul 27 '21 at 14:19
  • 1
    @MatthewAdams `Deferred` is in essence a single deferred value, while a `Flow` doesn't tell much about whether it will produce 0, 1 or more values. I would seriously refrain from using `Flow` for a single value unless you want to use this flow in a place where you want to also accept or return other flows with potentially more values. – Joffrey Jul 27 '21 at 14:22
  • @Joffrey's explanation is good. When you have a Flow, you don't know how many results it will produce. It might be 0 or it might be theoretically infinite, in which case calling `collect()` on it will prevent any subsequent code in a coroutine from ever being reached. – Tenfour04 Jul 27 '21 at 14:26
  • Ok, as a result of this discussion, I've filed issue https://github.com/AxonFramework/extension-kotlin/issues/139. I hope this goes somewhere. – Matthew Adams Jul 27 '21 at 14:52
  • @Joffrey How would you rewrite your "As Deferred" function as a `suspend fun`? – Matthew Adams Jul 27 '21 at 14:55
  • 2
    @MatthewAdams I'm not the author of this answer, but the suspending version is already provided by @Tenfour04. It's called `awaitNullable` in this answer. It does not return a `Deferred`, it just returns `T?` because the "asynchrony" is already expressed by the `suspend` modifier. On the other hand, if you convert the `CompletableFuture` to a `Deferred`, you don't need to suspend because you're just converting a future type into another immediately - there is nothing to wait for. – Joffrey Jul 27 '21 at 15:02
  • 1
    @MatthewAdams regarding the filed issue, it would be more appropriate for the API to expose `suspend` functions returning plain values (or Unit) rather than exposing functions with `Deferred` return types. – Joffrey Jul 27 '21 at 15:18
  • @Joffery agreed. I must've missed @Tenfour04's `suspend` version -- I understand now. I'll mention the use of `suspend` in the Axon github issue. Thanks! – Matthew Adams Jul 27 '21 at 18:10
  • @Tenfour04 The maintainers of https://github.com/AxonFramework are open to improving https://github.com/AxonFramework/extension-kotlin based on issue https://github.com/AxonFramework/extension-kotlin/issues/139. They've asked me if I'm willing to submit a PR for the changes. I am, however, I still consider myself a Kotlin noob. Would y'all be willing to help me with the PR so that I ensure that the Kotlin idioms are actually idiomatic? You can email me at matthew@matthewadams.me to discuss. Thanks! – Matthew Adams Jul 28 '21 at 13:39
  • @Joffrey StackOverflow wouldn't let me tag you also in the above comment. Same question above is for you, too. :) – Matthew Adams Jul 28 '21 at 13:40