Is it an anti-pattern to mixin a `CoroutineScope` ...
# coroutines
s
Is it an anti-pattern to mixin a
CoroutineScope
(with a delegate) on a class so that you can use
Deferred
as the value of its properties? This is kind of useful for creating lazily computed properties that are backed by network I/O (I’m using Ktor):
Copy code
class Foo(val client: HttpClient) : CoroutineScope by GlobalScope {
  val bar: Deferred<String> by lazy {
    async { 
      client.get<String>("<https://www.example.com>")
    }
  }
}
👌 2
I’m not really sure how else to cache the result of this call asynchronously. It may be expensive and is not something I want to compute multiple times. I have separate suspendable functions that depend on
bar
but I only want to compute
bar
when needed.
s
I would put CorousineScope as a private val inside the Foo class, not have it implement it.
☝️ 3
But the general idea of lazily caching a async property-value is a good idea (depending on your app/use-case)
a
and expose the fetch as a
suspend fun
, not a
Deferred
.
Deferred
is the implementation detail here
👍 1
s
Can you provide an exemplar? The problem is a trivial
suspend fun
would compute the result each time. How would I implement the lazy functionality?
s
A
suspend fun
can call
await()
on the deferred
☝️ 1
The first time it may actually suspend, the other times it will return immediately
a
as only one reason of many, you don't want to expose the
Deferred
because then anyone can
.cancel()
it 🙂
☝️ 2
👍 3
s
suspend fun foo() = client.get<String>("<https://www.example.com>")
would compute it each time.
I want to cache the result of the
client.get
since it’s expensive.
a
yes it would, but
suspend fun foo() = privateDeferredVal.await()
would not.
s
Oh make the
val
private? I did that, I think I just forgot that in the example. So updated example:
Copy code
private val bar: Deferred<String> by lazy {
  GlobalScope.async {
    client.get<String>("<https://www.example.com>")
  }
}
Is this the correct design?
a
Now extract the
GlobalScope
into a private val passed to the constructor instead
s
Ah, so that the scope is bound to the parent. Right.
a
Assume that any time you type
GlobalScope
it's probably wrong and you'll be on the right track more often than not 🙂 you also generally want your callers to be able to provide scopes to work in rather than hardcoding them
s
The main challenge here is I’m trying to create an API that wraps a REST API, but I need this to be usable from Java, potentially without direct coroutine support so users of the API don’t need to worry about coroutines. I guess maybe I should map everything into futures.
a
that's one way to do it, you can offer static utilities that do the wrapping for you so that you can keep that goop out of your actual classes and their logic 🙂
java code can also pass `CoroutineScope`s around without calling suspend functions
s
Do you have any examples of the static utilities part of that answer? The reality is that it’s significantly easier to serve all of the properties that are derived from the Deferreds, like
bar
in that example, from suspendable functions.
The REST API I’m using is pretty poorly formed, so I’m using JsonPath a lot to to extract only important information.
Which is why I have all of these derived properties (using that term loosely obviously since they’re actually suspendable functions)
a
you could wrap the request result in an object with non-suspending derived property accessors. Use the suspend function (or a wrapper future) to obtain that result object, then perform normal gets on the derived properties
☝️ 1
s
Ah gotcha, kinda like how Retrofit’s adapters return wrapped Pojos. Ok, maybe I’ll try that.
z
you also don’t need
by lazy { .. }
. Try
async(start = CoroutineStart.LAZY) { .. }
instead This will avoid executing the coroutine until
.await()
is called
2
s
The point of the lazy call is to cache the Deferred, not to prevent the async from starting at initialization time.
a
you don't need a lazy to cache a Deferred, a simple private val will do that 🙂
👍 1
s
Unless you want to wait kicking off the
async
until the
bar
is accessed for the first time, not kick it off immediately during the initialization of
Foo
👍 1
a
Well right, but like @zak.taccardi mentioned, you can do that with async params too