jeremiep
Science is fantastic to dig into areas it can already see, and terrible at seeing new areas from the greater unknown.
The problem usually isn't react, but that most projects using it directly use it wrong.
This seems to be my biggest pet peeve with the JS ecosystem as a whole; people chase things that are easy to learn rather than simple to reason about once learned and the result is almost always a convoluted mess of a codebase, regardless of the project.
I used jQuery, then Underscore, and later lodash; now I'm using ClojureScript and wondering why I ever could enjoy any of those crippled FP alternatives in the past.
"Someone who doesn't know about X will create its own worse version" is true, but it also applies to you and everyone else :) We don't know what we don't know (thats meta-ignorance) and we tend to assume what we do know is the state of the art, it quite usually is very far from it!
Not everything on the web has to use the normal stacks. I used TypeScript in the past but didn't enjoy the complexity of most JS libs, even with typings. Clojure was a breath of fresh air, and still is.
Taxation is already high for the rich, but the top bracket is usually quite low. I don't think the solution is more taxes, but more tax brackets; they should scale to accommodate the extra rich.
I lived in poverty for years after dropping out of college, sometimes with roommates that were way beyond toxic, worked multiple jobs 70 hours a week just to pay rent and food and still ended up middle class with a job I love; I've been through hell to find heaven. What I learned on the way I now use every day, its made me a stronger and better person and I can now help others do the same.
If someone had given me what I have today, just for the sake of equality, I would not have learned responsibility, discipline, I would hardly have developed most of the skills I now have and probably would've lost all of it by now. I would basically still be an angsty teen in an adult's body, which is what kept me poor in the first place.
I forgot exactly where I got that number, but it's been a pretty good metric so far.
In a nutshell; the compiler is great a micro-optimizations and absolutely terrible at macro-optimizations. The former will get you a few percent of perf boosts while the later usually results in orders of magnitudes of performance gains.
Its near impossible to apply macro-optimizations at the end of a project without massive refactors.
Architecture, data structures, batching operations, memory locality, and a bunch of other metrics are all concepts the compiler can't really help you with whatsoever and they have a much larger impact on performance than the 10% the compiler is actually able to optimize.
The problem is that either programmers don't care, or they can't make the distinction between premature optimizations and architecture planning.
Have you read your own comment?
Where I have issues with that is it usually feels like an afterthought rather than a first-class feature.
If you instead model your application after an event log then your current state is just a fold over these events. You have the same list of actions you previously had to manage undo, but now its positioned at the very center of the architecture.
And then you start seeing emergent features of that design:
- you can time travel the UI's state, which is probably the best debugging tool you can have for UIs.
- you can replay the event log over fixed application code, which greatly decrease iteration times.
- it moves logic outside of the UI components, making the app easier to unit test.
- you're storing a sequence of intents rather than results; its effectively free to audit such a design, its convenient to report the last X actions performed with a bug report and more precise than asking the user what they were doing.
The list goes on and on. I believe most of the complexity in applications is generated from the fact there is very little synergy between the different features of these applications. If everything requires a different abstraction and set of indirections, then you're just accreting complexity over time.
If you have a design where features can emerge from it, its usually a safe-bet to say you're on to something :)
Thats how I've been building UIs for the last few years. `re-frame` in ClojureScript for one is a fantastic framework built on top of these concepts.
> I don't see the point of having a debate about the merits of functional programming vs. OOP in the middle of your post
I made that distinction to highlight the differences between retained-mode UIs versus immediate-mode UIs. There are more similarities to OOP vs FP than differences here.
Think of it this way; the application itself already contains all the state it needs. A retained-mode UI will deep copy that state into the UI components while an immediate-mode UI will make the UI state a function of the application state. There is huge value in doing that because it dramatically simplifies the UI stack, this translates into shorter iteration times, simpler reasoning and less bugs. Its no good thinking about higher level tooling if the fundamental API is overly complex.
> unless you build a system that's easy to understand on that end as well
We shouldn't care about whether something is easy or difficult; we're only going to learn it once after all. We should instead care about whether its simple or complex, because that is a direct function of our ability to reason about things once they are learned. Most things that are easy to learn generate more complexity than simplicity and I believe this is harmful in the long term.
> I don't think it's unreasonable to expect that the low-level rendering engine (ie. an implementation in UIKit) maintains some level of UI state.
You're right, its perfectly reasonable and precisely what React does under the hood. Where I disagree is that I don't want the cached UI state to leak into the components. Just like the current state of the DOM is irrelevant when rendering the virtual DOM. Its purely an optimization mechanism and one which should be allowed to evolve independently from the higher level components.
> Things generally aren't immutable
Then why are so many things moving towards immutability? Modern video game renderers push really hard to be stateless, to be completely decoupled from the game threads, to not leak cached state into entities and a bunch of other FP-inspired concepts.
The end result is incredibly more parallelism. The rendering pipelines are much simpler and performant. There are quite a few advantages to this approach. It also generates optimal memory layouts and accesses, these are simply emerging from such a design. Its much easier to have linear memory accesses if you constantly recreate the display lists in fresh memory. Updating a state tree is much more complex than creating a display list and it greatly cripples the potential optimizations.
> higher level libraries can easily remove the pain of UI state management
That is not the feeling I get when I look at the average React project; most components use getState/setState, need to touch the virtual DOM for almost every operation, are needlessly maintaining two views of the same state (app and UI) and whatnot. Or they go full Functional Components and the result is a huge stack of component layers from hell.
> It seems to me the appropriate abstraction is build the protocol around a UI state tree
I believe this is fundamentally wrong; the appropriate abstraction should be built around the application state. You can derive UI state from that, and doing so liberates you from having to maintain the UI state independently.
A hashmap of nodes on screen is still very high level; you're basically replacing the DOM with data, yet keeping the exact same structure. But now you lose static types and everything is slower.
Redrawing doesn't happen at the node level either; that would be terribly inefficient. Things are instead batched together.
You don't have to compile your styles to CSS currently; just write them as JS objects and call the CSS constructors yourself from code. You'll quickly find its not productive for most styling.
I simply don't believe the vast majority of developers to be able to properly handle rendering at that level. No offence but all your points about low-level rendering is not how it actually works. Its incredibly easy to fool yourself into thinking you wrote a well-designed, efficient piece of code and learn years later every single piece of it was far from optimal.
Thats why we have layers of abstractions.
For one, there's a lot to be learned from immediate-mode UIs (I'm thinking Dear IMGUI, not Unity3D's GUI); ie state inside components generates complexity, which is a bad thing. Your views should be nothing more than a pure FSM transforming app state to UI directives. IMGUIs have a ton of tricks to simplify UI development. With these in mind, FP becomes a much more interesting paradigm than OOP for this. The same logic applies whether you render the entire UI 60 times a second or you cache the previous render to only do the minimum amount of updates.
A good abstraction is one where features can emerge from its design. For example you rarely have to test your views when they're just a simple pure transform. You don't need an entire framework and command-patterns to implement undo when your app state is immutable and modelled as a sequence of actions. With each emergent feature you remove the need for tons of code and tooling; that yields agility.
I still believe most web application are much more complex than they should be, especially on the UI level, regardless of the framework used. And until we solve that problem, I don't think the web stack is the way to go for native. We're just replacing one set of problems for another one.
I strongly believe we shouldn't make things easier, but simpler.
React only gets complicated when you use states and mutable props, or when logic is put inside components.
As a view backend it just becomes a FSM translating app state into pure views and nothing more.
> Much like you know that control flow will come back to the function you're reading after calling another function.
What about continuations? Exceptions? Aborts? setjmp()? I can think of many cases where control doesn't return to the caller that are perfectly valid.
Basically, either the code is so simple theres obviously no bugs, or the code is so complex theres no obvious bugs. I feel a nursery is closer to the later than the former.
Implementing a promise as a monad will yield all the same benefits while also keeping the ability to compose and be immutable; and then you have an abstraction and the result is simple.
I agree restrictions make the code better, this just isn't one of these cases to me.
Still, there's value in unification and generality, at least when the result is simple, even if that covers only 80% of cases.
For example you generally run a lot more composable tasks than actual threads and syncs. You get a general, and powerful abstraction for the general case yet threads are still there for more exotic needs.
What conventions? Its not realistic to assume the world will change to fit your views of software :p
> At least in C/C++ it doesn't.
Sure does; it adds more build targets, gets you to maintain shared code across executables, and plan deployment for multiple executables instead of one. Thats all before even coding the support for that.
> Why not?
Because these depend on shared memory and ownership transfer for performance; you'll drastically drop performance just for the sake of isolation.
> No reason for it to stay that way.
I will literally stop using the web if pages can spawn processes :)
> I won't pretend to know all those words :P
Hehe, basically immutability makes it so nobody can mutate data, thus making it safe to be shared across threads. Uniqueness will transfer ownership such that only one thread has references to a mutable piece of memory at any time. Deterministic parallelism means its impossible to have race conditions or deadlocks.
> I just think the article's proposal has some merit and we should consider it.
Agreed, I'm still having a hard time seeing it however :p