Any tips on why Compose functions are structured v...
# compose
t
Any tips on why Compose functions are structured via
@Compose
annotation rather than something more common in Kotlin dsl’s
Compose.foo()
extensions on some context object? It seems to me that it could make this more extensible and user-friendly than annotations and compiler plugins I’ve read about it somewhere that devs went out from traditional “build-view-tree -> diff-view-tree -> dispatch-changes” way to improve performance. Are there any studies on how much actual performance gained?
1
💯 2
👍 2
r
We thought about using a receiver but we didn't want to take it away from the user.
l
There were also some other unintended side effects that we didn’t like. It’s really close to being what we want but ultimately decided it wasn’t the right move. You can imagine all suspend functions instead being extensions on
Continuation
, but they didn’t do that either.
👍 2
t
@Leland Richardson [G] can you name these side effects? Other than taking receivers away from user?
l
Sure, I can try. I don’t have the notes from that discussion handy, but: 1. It means that every component needed to be an extension function, which makes it difficult to do certain patterns, like add components as part of a receiver scope of a lambda that is the child of a component. This is hard to clarify in slack, but let me know if you want a more concrete example. Moreover, patterns like putting composable functions as functions of an
object
, or as methods of a class, don’t make sense anymore. It made us realize that what we wanted was similar to a receiver scope, but not semantically identical. 2. We still wanted to do some code generation around invocations for many reasons, and the threading of the composer parameter didn’t get rid of all of those reasons. If we still need to do code gen, then we need an indication that the function is “special”. The receiver isn’t enough, because we also needed to have some functions that were just actual normal functions on the Composer object that is passed around. It made it hard to build a world where we didn’t also need another annotation, even if we were passing the Composer around as a receiver parameter. Having both seemed strictly worse. 3. With an annotation, it becomes realistic for us to label everything on the Composer as ~private APIs. We can make changes in how we do things, optimizations, etc. without the fear that user-land code is using these APIs improperly, if even accidentally. 4. We have built Compose to be generic to the type of tree it is emitting. Doing this properly is still being worked on (and is the reason we have to hackily import
composer
right now). By exposing the context parameter as a receiver parameter, we have to participate in the type system in all the ways that kotlin receivers already work… doing this in a way that was compile-time safe meant that we would have to add an open generic type parameter with constraints to a lot of components, which adds significant cognitive overhead to the definition of a lot of components. For all of the other components, the type parameter would be specified… but it would feel redundant to a lot of people and probably promote type aliases and things like that which start to make the component ecosystem feel bifurcated and broken. 5. Mentioned already by Romain, but this meant that if you wanted to have components that were defined as receiver scopes to something else (for instance, a UserProfile component could be defined as an extension to a
User
data type, really cleaning up the implementation. 6. Using receivers, for some pretty nuanced technical reasons, would mean that we wouldn’t be able to unify “Effects” and “Composable Functions”, which is something we are exploring currently. 7. Something I might explain in a blog post in the future is the fact that we had played around with the notion of “Class Components”. (These still exist in the repo right now). We gradually realized that class components are mathematically equivalent to composable lambdas that are memoized. This equivalence doesn’t work anymore if you start to use Composer as a receiver. This point isn’t so important that it would decide it on its own, but having the design of something actually remove concepts from the vocabulary of Compose is a good sign that it’s something more general IMO. I think there were a few more, but that’s what I have off the top of my head. It might be worth mentioning that I was a big proponent of the receiver scope approach for a while, and was very much arguing for them, but after doing dilligent research on the effects of going this way, I think
@Composable
is the right way, especially if we can come up with a general purpose language spec for what that means, and turn it into a keyword similar to
suspend
instead. (Not saying that will happen necessarily, but it is something we want to explore)
👍 4
👏 13
t
@Leland Richardson [G] Thanks a lot for this huge piece of info about design process behind compose, it clarifies many things that I've had concern with.
r
I was also on the camp of the receiver scope, mostly because it felt more natural than using an annotation. We had many discussions about this as you can imagine based on Leland’s message above (Leland changed my mind :)
BTW you can see our proposal for multiple receivers: https://github.com/Kotlin/KEEP/pull/176
t
@romainguy I know about this proposal and I’m a big fan of it, mostly because it can cover usecases of KEEP-87 (Typeclasses) without introducing Scala’s implicits 😉
g
Just by curiosity about compiler plugins, could you have the annotation implemented as a keyword? Something like
Copy code
compose fun someComponent {

}
l
that is my long-term hope
🙏 4
r
Ask @elizarov :p
😂 1
e
Plugins cannot contribute keywords nowadays (even soft keywords) and it is likely to stay this way for a foreseeable future. You can get some hint as to why it is so by digging a bit into Kotlin’s history: https://blog.jetbrains.com/kotlin/2015/08/modifiers-vs-annotations/
👍 5
g
@elizarov Interesting!
l
@elizarov one thing that I would like to write up formally at some point is for a small (i think) parsing change to have a lookahead for annotations that prevent the ambiguity where you have an annotation annotating a lambda type. For instance,
@Composable () -> Unit
doesn’t parse, you have to write
@Composable() () -> Unit
. It seems to me we might be able to disambiguate if the parens are followed by
->
. This would help the annotation’s ergonomics a bit 🙂
1
since the parsing currently fails, i think it would be a safe change because we wouldn’t be able to break existing working code
e
Makes sense.
s
@themishkun I came across this when reading back in Slack history. KEEP-87 works nothing like implicits in Scala. As proposed it’ll only work for interfaces, similar to protocol extension in Swift. Additionally, it’ll only be possible to provide a single impl per interface which is globally available. Instead if you want to override that, you can manually pass the interface or have an
internal
override. There are more similar restriction which makes it much more user friendly and Kotlin idiomatic compared to Scala implicits. Sorry to re-ignite this old thread 🙂
l
I read over keep 87 a while ago, but need to do so again I think. I like protocol extensions in swift and wish we had a similar capability in kotlin. I found the syntax proposed to be difficult to follow though. will try and take another look.
👍 1
s
There is some confusion around keep-87 since it's changed quite a lot since the original proposal, based on the discussions in the keep. If you have any questions feel free to DM me or swing by in #arrow or #arrow-contributors. The people that have worked on it and proposed it should be easy to get a hold of there.
👍 1