https://kotlinlang.org logo
Title
s

Sam Garfinkel

03/25/2020, 2:32 PM
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):
class Foo(val client: HttpClient) : CoroutineScope by GlobalScope {
  val bar: Deferred<String> by lazy {
    async { 
      client.get<String>("<https://www.example.com>")
    }
  }
}
:yes: 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

streetsofboston

03/25/2020, 2:33 PM
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

Adam Powell

03/25/2020, 2:34 PM
and expose the fetch as a
suspend fun
, not a
Deferred
.
Deferred
is the implementation detail here
👍 1
s

Sam Garfinkel

03/25/2020, 2:35 PM
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

streetsofboston

03/25/2020, 2:35 PM
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

Adam Powell

03/25/2020, 2:35 PM
as only one reason of many, you don't want to expose the
Deferred
because then anyone can
.cancel()
it 🙂
☝️ 2
👍 3
s

Sam Garfinkel

03/25/2020, 2:36 PM
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

Adam Powell

03/25/2020, 2:36 PM
yes it would, but
suspend fun foo() = privateDeferredVal.await()
would not.
s

Sam Garfinkel

03/25/2020, 2:38 PM
Oh make the
val
private? I did that, I think I just forgot that in the example. So updated example:
private val bar: Deferred<String> by lazy {
  GlobalScope.async {
    client.get<String>("<https://www.example.com>")
  }
}
Is this the correct design?
a

Adam Powell

03/25/2020, 2:39 PM
Now extract the
GlobalScope
into a private val passed to the constructor instead
s

Sam Garfinkel

03/25/2020, 2:40 PM
Ah, so that the scope is bound to the parent. Right.
a

Adam Powell

03/25/2020, 2:41 PM
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

Sam Garfinkel

03/25/2020, 2:42 PM
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

Adam Powell

03/25/2020, 2:46 PM
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

Sam Garfinkel

03/25/2020, 2:49 PM
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

Adam Powell

03/25/2020, 2:51 PM
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

Sam Garfinkel

03/25/2020, 2:57 PM
Ah gotcha, kinda like how Retrofit’s adapters return wrapped Pojos. Ok, maybe I’ll try that.
z

zak.taccardi

03/25/2020, 5:49 PM
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

Sam Garfinkel

03/25/2020, 6:28 PM
The point of the lazy call is to cache the Deferred, not to prevent the async from starting at initialization time.
a

Adam Powell

03/25/2020, 7:18 PM
you don't need a lazy to cache a Deferred, a simple private val will do that 🙂
👍 1
s

streetsofboston

03/25/2020, 7:20 PM
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

Adam Powell

03/25/2020, 8:34 PM
Well right, but like @zak.taccardi mentioned, you can do that with async params too