https://kotlinlang.org logo
#compose
Title
# compose
j

jim

10/09/2019, 3:50 PM
@alexsullivan114 Perhaps I can speak to that one, as I was one of the engineers building React.js at Facebook and was also the person who designed it differently when I proposed building Compose at Google. Creating real views during every composition would be prohibitively expensive. Just as creating HTML elements in browsers can be expensive (although this is improving dramatically in recent years), creating Views on Android is not cheap. To recreate a real view hierarchy on every update would be a non-starter. React and Flutter both use a VirtualDOM, and your `render`/`build` functions return VDOM elements. This worked, and was a relatively revolutionary design decision at the time, and their continued success is a testament to this model. I considered a similar approach for Compose. However, there were a few drawbacks to this approach. 1. Performance. Although the VDOM is orders of magnitude faster than creating real DOM/Views, modern view frameworks are highly optimized, and the VDOM allocations and garbage collection become bottlenecks. Furthermore, creating new VDOM elements means the VDOM elements are no longer referentially equal, which makes them more expensive to compare in the common case where the particular node doesn't change, which becomes the second performance bottleneck. Our approach makes it possible to avoid the allocations (present day) and comparisons (future work), which should allow us to achieve a better performance profile once we start optimizing aggressively (we're still just getting everything working at this point). Furthermore, VDOM makes it harder to statically reason about hierarchy invariants, which make it harder to do some more advanced compile-time optimizations. 2. Readability. The VDOM encouraged a bunch of patterns that made the code less maintainable. For example, users would build up VDOM trees, pass them around, and use them as input to other functions that would return modified VDOM. I personally consider these patterns to be antipatterns, and the Compose style takes them completely off the table because you don't get access to the VDOM elements, so you can't abuse them. 3. Control Flow Ergonomics. Because you need to produce/return VDOM elements, doing control flow becomes awkward. If you want to iterate over nested lists, then you either need to have
list.map { it.map { ... }}
or you need to create a mutable list with nested
for
loops that mutate the list. Those approaches are both perfectly doable, but not super ergonomic. The Compose method ends up handling
if
statements and
for
loops in a more natural way. To be clear, other declarative frameworks like React, Flutter, Vue, and Ember are great frameworks, so please don't take the above as a criticism of them. The various frameworks have different constraints and environments, and these decisions are all just a series of tradeoffs. We as an ecosystem are still learning and experimenting and iterating. We opted to do more work at compile time instead of at runtime with a VDOM, which has both pros and cons. Hopefully we will find that the pros outweigh the cons in our environment, but only time will tell.
👍 27
a

alexsullivan114

10/09/2019, 4:01 PM
Fantastic summary, thank you! It sounds like performance was a meaningful part of the reason for choosing the composing paradigm. That's something that I have no insight into and will certainly trust you and the teams decisions on. Considering the readability point, I'm interested in why you consider passing around VDOM trees an anti-pattern. To be clear, I'm not disagreeing, I'm just interested in the pain points you've discovered. I'm mostly familiar with it in the form of passing child elements into a higher order component. I have found it challenging at times to reason about the returned component (that pain is more acute in highly abstract libraries - i.e. the component returned by the
createAppContainer
method in
react-navigation
or the connected component in react-redux), but I've chalked that up to still being relatively new at using React. For the control flow, my knee-jerk (possibly naive) take is that the VDOM approach encourages a more "functional" (I don't like using the term functional since it's so damned overloaded but I don't know what else to use here) approach, whereas the composing method is more tailored around a traditional imperative style of dealing with control flow (I recognize the limits of that statement since Compose is fundamentally built around composable functions). Would you agree with that assessment? What an exciting programming time we live in. It's so fun to watch as the library (and the broader ecosystem) progresses.
j

jim

10/09/2019, 7:28 PM
Great questions! It's a little hard to give a full explanation in this chat, but it might be a good topic for a future blog post. I'll try to give it a quick try though. Regarding readability: The differences are subtle, but the basic idea is to slightly discourage patterns that are hard to read and favor patterns that are easier to read. We make it easier to "emit elements in place" (which tends to be easier to read because the code directly matches the output) and slightly less common to "create elements to be used later" (but still possible via lambdas). Creating/using higher order components can be a pretty "advanced" pattern, but that's not what we're strongly defending against. In fact, Compose supports creating/using Higher Order Components; you can easily write a function that creates a composable lambda from another composable lambda. You can also pass around children as a lambda (aka. "render prop"). Code really starts to become nasty when the code is attempting to read from children or "modify" children via `cloneElement - and this is what Compose strongly prevents. Code which reads/modifies children inherently ends up baking in assumptions about the children, which makes the code hard to reason about, and hard to look at a fragment of code and be sure what it is going to do, because such patterns require you to know what the rest of the system might do with those elements. With regards to control flow, your knee-jerk reaction is the same that most functional developers have when they first see Compose, but it turns out to be an illusion. Emitting values into the Compose runtime is mathematically equivalent to collecting the values within the method and then returning them at the end. The difference is purely syntactic sugar, and it is easy for a compiler to mechanically convert between the two. SwiftUI took a very similar approach but with a more formalized language-level change which makes for a super interesting read ( https://www.swiftbysundell.com/articles/the-swift-51-features-that-power-swiftuis-api/#function-builders ) Because Compose manages the execution of your composable function, those calls can actually be constrained to the function execution (just like Swift function builders) and thus the function remains "functional" or "pure" for lack of a better term. It looks imperative and side-effectful but it is mathematically pure/functional/declarative. The bigger risk is that it trains people to think the code is imperative, and thus to think that imperative/side-effectful code is acceptable within Composables, and then to adopt other non-managed APIs that are imperative/side-effectful, and thus violate best practices.
2 Views