Hmm. You can often see the first one in docs, but ...
# codingconventions
g
Hmm. You can often see the first one in docs, but I'm concerned that it doesn't scale well. Consider this:
Copy code
fun main(){
    runBlocking{
        with(First()){
            with(Second()){
                with(Third()) {
                    foo().zip(bar()).zip(foo()) // fails because of name conflict
                }
            }
        }
    }
}

class First(){
    fun CoroutineScope.foo():ReceiveChannel<Int>{}
}
class Second(){
    fun CoroutineScope.bar(): ReceiveChannel<Int>{}
}
class Third(){
    fun CoroutineScope.foo(): ReceiveChannel<Int>{}
}
Deep nesting plus name conflicts. How do you tell the difference between the two methods with the same name?
g
If you have such extreme cases, just use parameter instead, it's just matter of style
I believe mostly people vote for the first one because it consistent with other coroutines builders.
Also, I'm not sure that general approach when each method of class is coroutine builder is so common. Maybe if you have class you can pass scope to constructor, this is not universal solution, but on practice I see it often, when if I have class with coroutine builders, class itself has some state inside, exposed with channels, so class should have own scope to handle lifecyle, so it solves code problem, because you don't need scope anymore. Of course it's not something universal, but maybe if your class doesn't have internal state and each method is standalone buildrr, better just use top level functions for those builders?
g
a concrete usecase: viewmodel consumes data from repository. So, all these classes that provide channels are repositories. Having this, combining 3 streams from a repo is not an extreme case. The intention was pretty clear: viewmodel has a scope and lifecycle. It pulls all the data, delivers to main thread, and knows when to clean. It works, but as you can see, the API gets pretty messy... As you mentioned, I was thinking of having repository implement it's own
CoroutineScope
but there is no lifecycle attached to it. What about the cleanup in this case? Do you still have to do anything about the Job?
d
Was I mentioned here or something? I'm a bit confused
Anyway, I think if you're calling 3 coroutine builders in one line you are probably doing something wrong anyway
Oh you linked it, nvm
g
@Dico the code is here, it's written according to docs and it's a few simple lines. I gave the actual usecase in every second android app a few seconds ago.
Something wrong anyway
is not very constructive.
d
Okay, sorry. I didn't intend it as criticism, it just struck me as an edge case.
🤝 1
d
Maybe surround each
produce
in a
coroutineScope { }
to use the callers scope? @ghedeon
g
This doesn’t make sense, coroutineScope will never return if there is some active produce coroutine
What about the cleanup in this case? Do you still have to do anything about the Job?
Just pass coroutine scope as argument
you also can do some trick, like return some ChannelProvider and subscribe already in your scope, but not sure that this additional abstraction make sense
d
Good catch @gildor 😊, in the end, it looks like he's right that rx leads to a cleaner api in this regard... I wonder if cold channels would solve this?
g
it looks like he’s right that rx leads to a cleaner api in this regard
It’s not true. Rx by default do not force you to handle lifecycle. You can do exatly the same with Kotlin if you want:
fun bar() = GlobalScope.produce()
- this is exactly what RxJava does in terms of lifecycle
forcing user to handle lifecycle explicitly is good and this is why structured cconcurrency exists
fun bar(scope: CoroutineScope) = scope.produce()
is not worse than
fun CoroutineScope.foo() = produce()
This is just a matter of style and use case
d
Good point, but in rx you handle the lifecycle at subscription using composite subscriptions, coroutines force you to handle it in the repositories and in all usages, you can't just delegate handling to the usage side like in the viewmodel, so you end up with scopes all over the place. That's why I think cold channels might solve this... @gildor
g
but in rx you handle the lifecycle at subscription using composite subscriptions
Copy code
val compositeChannel: MutableList<ReceiveChannel<T>>
compositeChannel = GlobalScope.produce { } // do some subscription
compositeChannel.forEach { it.close() }
coroutines force you to handle it in the repositories and in all usages
And this is good, not bad
This is not a problem of channel at all. This problem because
produce
runs coroutine
you right with cold streams it may be something different, but the only thing that changed is that you pass scope to some
subscribe()
method
d
That's the current advantage of rx... cold channels seem to be blocked for some reason, I think it's a pity...
g
what?
I mean cold and hot streams are diffrent
and yeah, rxjava supports also cold
I just don’t understand how this related to discussion
d
I mean they're not implementing it, there's lots of talks in the issue on github... the relationship is that it would be a cleaner solution to the original question.
g
Are we talking about lifecycle handling and code style of coroutines builders or hot vs cold?
because it’s completely not related for me
g
@gildor when you say it's a matter of style, does it mean there is a way to distinguish same function names
foo/foo
in my example? Because I didn't find it yet. Only the last one wins.
d
Well, you can use this@ syntax, and I believe it's only a problem with signature clash
➖ 1
g
no, unfortunately you cannot use this@
You can do something like: with(Second()) { with(First()) { foo() }.zip(bar()).zip(with(Third()) { foo() }) }
And this is ugly for sure. But it really depends on real code. Your particular example can be rewritten
Copy code
val firstFoo = with(First()) { foo() }
val bar = with(Second()) { bar() }
val thirdFoo = with(Third()) { foo() }
firstFoo.zip(bar).zip(thirdFoo)
g
oh, well, I think that's where I draw the line, it's not a matter of style anymore 🙂. One is working and another is mental gymnastic.
g
You just need somehow call function
Someone may say it’s fine
someone will just rewrite to:
Copy code
val firstFoo = First().foo(scope)
val bar = Second().bar(scope)
val thirdFoo = Third().foo(scope)
firstFoo.zip(bar).zip(thirdFoo)
👍 1
is it much better? Maybe in this case yes
But code does exactly the same
g
come on, it IS better. Like objectively.
g
But if you have class and use a lot of methods:
Copy code
class First {
    fun CoroutineScope.foo() = produce {}
    fun CoroutineScope.bar() = produce {}
    fun CoroutineScope.baz() = produce {}
}
Than usage with sccope is quite nice:
Copy code
with(First()) {
 foo().zip(bar()).zip(baz())
}
it’s better but for this particular extereme case
with
is just a way to bring extension to scope, nothing more
why not just use param for scope if it works better for your use case and for your style preferences
g
I will, but I'm here to understand what I can do with channels, what are the good practices, so I can give answers to my team, not to solve a boring repository request. I'd kindly ask you stop pushing for "extreme case". It's a day-to day case and a corner stone of Rx usage. It's basically every second Interactor in your app: take a few repos, combine channels, give it to ViewModel. The earlier we aknowledge this, the faster we can adapt and maybe address it in official documentation, because now it's only top level functions there.
needless to say, that I appreciate your contribution to this community, it's a titanic effort.
g
okay, not “extreme”, synthetic case that doesn’t work well with current code
I would just choose style on particular use case
It’s basically every second Interactor in your app: take a few repos, combine channels, give it to ViewModel
And all your sources of observables have name conflicts? Not every ViewModel has so many separate sources, of course it’s possible, I just want to say that it not something that you have all the time
And I don’t want argue that extension function is better, for repository case it’s not, this is why I’m talking about style. That there is no universal answer, same way as with any other function vs extension usage
b
Afaik, given a function
class Foo { fun CoroutineScope.bar() = ... }
can be invoked as
foo.bar(scope)
without rewriting the extension function signature
d
I don't believe that's correct. I think you can do it with lambdas with a receiver, but not functions with receivers.
➕ 1
e
It is not possible and there are other use-case where it might be desired, though. See also discussion on this issue: https://youtrack.jetbrains.com/issue/KT-14504