-
Notifications
You must be signed in to change notification settings - Fork 204
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Built-in Reactivity #2345
Comments
Sounds like a spreadsheet - when one value changes, all computations depending on it are recomputed. The general idea seems to be that: That's probably not viable the way it's suggested here. The example What about That means that an expression, inside a function body, can get evaluated multiple times, triggered from pretty much anywhere. (Maybe even inside itself, unless we can statically see and reject cyclic dependencies.) It doesn't interact well with asynchrony. What if the computation throws, where does the error end up? It shouldn't be able to interrupt the propagation of a value to other listeners. So it probably ends up as an uncaught asynchronous error somewhere? The idea should be able to work just as well for instance variables and static/top-level variables. (And would It seems like you can implement something like that as a library feature, just with some extra syntactic overhead (it's not a variable, it's an object with a import "dart:async";
class Reactive<T> {
final List<WeakReference<Reactive>> _listeners = [];
final T Function()? _computation;
late T _value;
Reactive.value(T value) : _computation = null {
_value = value;
}
Reactive(T Function() computation) : _computation = computation {
_value = runZoned(computation, zoneValues: {#_ReactiveInit: this});
}
T get value {
var currentInitializer = Zone.current[#_ReactiveInit];
if (currentInitializer != null) {
_addListener(currentInitializer);
}
return _value;
}
set value(T value) {
if (!identical(value, _value)) {
_value = value;
_notifyListeners();
}
}
void _addListener(Reactive listener) {
_listeners.add(WeakReference(listener));
}
void _notify() {
var preValue = _value;
var newValue = runZoned(_computation!, zoneValues: {#_ReactiveInit: null});
if (!identical(preValue, newValue)) {
_value = newValue;
_notifyListeners();
}
}
void _notifyListeners() {
for (var listener in _listeners) {
var l = listener.target?._notify();
_listeners.removeWhere((l) => l.target == null);
}
}
}
void main() {
var a = Reactive<int>.value(1); // No dependencies.
var b = Reactive<int>.value(2);
var c = Reactive<int>(() => a.value + b.value);
print(c.value); // 3
a.value = 5;
print(c.value); // 7
b.value = 6;
print(c.value); // 13
} |
From reading the proposal literally as @lrhn summarized:
I came up with a modified version of typedef VoidCallback = void Function();
class Reactive<T> {
final T Function() computation;
final List<Reactive> dependencies;
final List<VoidCallback> _listeners = [];
Reactive(this.computation, this.dependencies) : _value = computation() {
for (final Reactive dep in dependencies) {
dep.addListener(_listener);
}
}
void _listener() => value = computation();
void dispose() => _listeners.clear();
void addListener(VoidCallback callback) => _listeners.add(callback);
void removeListener(VoidCallback callback) => _listeners.remove(callback);
void notifyListeners() {
for (final callback in _listeners) { callback(); }
}
T _value;
T get value => _value;
set value(T val) {
_value = val;
notifyListeners();
}
} And with the examples from @lucavenir's comment: void main() {
var a = 4;
var b = Reactive<int>(() => 3, []);
var c = a + b.value;
var d = Reactive<int>(() => a + b.value, [b]);
b.value = 4; // causes d to update
print(c); // still 7 since c is not reactive
print(d.value); // updated to 8 because b changed
a = 5; // no updates since it's not reactive
print(c); // still 7 since c is not reactive
print(d.value); // still 8 since a is not a reactive dependency
b.value++; // causes d to update
print(d.value); // updated to 10 since a is now 5
} With this implementation,
Recomputing the entire expression would be the least magical thing to do. It would be obvious where side effects are coming from if undesired but otherwise allows the user to intentionally add side effects.
Okay... now I made an typedef AsyncCallback = Future<void> Function();
/// Create an instance like `await AsyncReactive(asyncFunc, deps).init()`
class AsyncReactive<T> {
final Future<T> Function() computation;
final List<AsyncReactive> dependencies;
final List<AsyncCallback> _listeners = [];
AsyncReactive(this.computation, this.dependencies) {
for (final AsyncReactive dep in dependencies) {
dep.addListener(_listener);
}
}
/// MUST call this to initialize the value.
Future<AsyncReactive<T>> init() async { setValue(await computation()); return this; }
Future<void> _listener() async => setValue(await computation());
void dispose() => _listeners.clear();
void addListener(AsyncCallback callback) => _listeners.add(callback);
void removeListener(AsyncCallback callback) => _listeners.remove(callback);
Future<void> notifyListeners() async {
for (final callback in _listeners) { await callback(); }
}
late T _value;
T get value => _value;
Future<void> setValue(T val) async {
_value = val;
await notifyListeners();
}
}
By using Future<T> delayed<T>(T value) async {
await Future.delayed(Duration(seconds: 1));
return value;
}
void main() async {
final a = await AsyncReactive(() async => 2, []).init();
final b = await AsyncReactive(() async => await delayed(1) + a.value, [a]).init();
print(b.value); // 3
await a.setValue(3); // change to 4
a.setValue(4); // change to 5... in 1 second
print(b.value); // It's still 4!
await a.setValue(4); // be sure to await the change
print(b.value); // Now it's 5
final c = await AsyncReactive(() => throw "Error!", [a]).init();
try { await a.setValue(5); }
catch (error) { print("By awaiting, you can catch the error"); }
a.setValue(5); // but by not awaiting, it's an uncaught error
} Also, as an aside, any symbols or keywords should be moved to the declaration ( |
that is kinda what AngularDart do in the background of the Spread reactive stuff like in this proposal are certainly bad regarding performance. Most of times you don't need to rebuild everything because one variable changed, but when a group of them did so. Hence why you have |
Perhaps there could be a syntax to specify which parts of a declaration update reactively, and everything else (that isn't dependent on a reactive part) is remembered as it was? That behavior could extend to both reactive and nonreactive variables in these declarations - If you have reactive variables
Maybe declaring a reactive variable with a reactive asynchronous component could change the type of the variable to a Future. Something like Still not sure what to do with exceptions (throw when the reactive variable is changed for synchronous, complete the future with an error for async?), and I'm not quite sure about the practical applications yet. But I find the idea here interesting. |
As @jodinathan said, there are better ways to do this from a performance and organization perspective. As fun as designing a working Variables in our code are not seen by anyone (until a framework puts them on screen) so they don't have to stay real-time. Having a bunch of values updating just for the sake of being up-to-date is usually unnecessary and misleads readers as to just how much work is being done (think |
People have been exploring handling dataflow automatically at the language level since the 1960s. The world's most popular programming language (Excel) works this way. There are some successful domain specific languages built around it like Pure Data for audio or Blueprints for Unreal game scripting. But despite many attempts, so far no one has successfully integrated it deeply into a general purpose language that I'm aware of. Elm is the latest best attempt. They may pull it off since the language was designed around it from day one. I think it would be really hard to integrate reactivity directly into Dart a decade after its introduction. There are so many places where the way it interacts with imperative code would just get weird and gnarly. I mean, Dart doesn't even have a concept of a lifetime. Also, it's really hard to pick a specific set of reactivity semantics that works for a sufficiently large subset of users to justify blessing it in the language. My impression is that in practice, each reactive system works kind of differently in ways that really matter for some users. Things like where recomputation happens eagerly or on demand, etc. But I could see us exploring what sort of hooks the language would need to expose to static metaprogramming to enable a macro to add support for reactivity. This overlaps a lot with stuff that @rrousselGit is interested in for freezed. I'm not sure if anything would pan out, but it's worth thinking about. |
Given the following line: var a = 1;
@reactive var b = 2;
@reactive var c = a + b; Could a macro (as currently planned) determine the following?
Given my var a = 1;
Reactive<int> b = Reactive(() => 2, []);
Reactive<int> c = Reactive(() => a + b.value, [b]); |
Freezed isn't quite the use-case. My use-case is flutter_hooks and Riverpod, which are about reactivity too. Statement macro and the ability to inspect the code of functions both seems like necessary steps. |
Hi there, I'm excited that this issue actually got some attention (I honestly didn't expect that). Therefore thank you in advance for reading my verbose message and taking it seriously. I've read all of the comments above and, as far as I can contribute, I think we should slightly redirect this thread. Why Reactivity is so importantIs all of this worth such headaches? I want to stress a little more about why this concept, in my opinion, is really important for a client-oriented language. When thinking about reactivity, @lrhn immediately thought about it as a spreadsheet: this is correct, as this well-known Rich Harris talk basically speaks the same vibe. But I think it's way more than that. Reactivity is very much linked to the concept of "state".
Sure, there's a few leaps in beneath, but I think it's clearer now that when we're discussing reactivity, we are inherently discussing how to easily track, update and handle state within Dart. Then, why is state important? Because Dart is widely used to build client interfaces, which (as stated previously) just love state. Furthermore, I could speculate that even back end development could use a "simple and easy" way to write declarative code to create asynchronous processing pipelines. Is this possible?I think we should shift the conversation elsewhere. I think @munificent comment is a pivoting point, as it shifted focus onto the concept of reactivity, which is close to a declarative approach, which just may not be compatible with a general purpose language like Dart. The comment made me realize that "declarativeness" might be achieved by an abstraction built on top of what we have today (OO + Functions + Imperative approaches), exactly like Flutter does. So, maybe this issue shouldn't be about a built-in feature, but rather about the ability of a Framework / Library to clean as much boilerplate as possible when it comes to reactiveness, state, etc. Is Meta Programming enough, as it is now?To my understanding, it doesn't look like so. I want to underline @Levi-Lesches's last comment's code and his question:
I want to add that, in my opinion these three pre-conditions might not be enough for a really concise syntax. Let's add two more lines of code to that. Let's also add a conceptual correction: notice how, since var a = 1;
@reactive var b = 2;
@reactive var c = a + b.value;
// Maybe as an alternative to `b.value+`?
@reactive b++;
print(c); // 4 Here, the objective is to have such macro alter the code so that we are not obligated to access Let's go even further: could such macro also be applied to statements such as with the following (imaginative) example? @reactive var drinks = 1;
@reactive if(@reactive drinks > 5) {
print("Woah, way too many drinks tonight.");
}
nightOut(); // This might print `Woah, way too many drinks tonight.` at some point. Say that, miraculously, all of the above questions have a "yes!" answer. var a = 1;
§var b = 2;
§var c = a+§b;
§b++;
print(c); // 4
§var drinks = 1;
§if(§drinks > 5) {
print("Woah, way too many drinks tonight.");
}
nightOut(); // This might print `Woah, way too many drinks tonight.` at some point. Finally, one last point: maybe it is more readable / doable to have something like Reactivity implementation detailsGiven all the above, I think the details about reactivity's implementation are out of scope now. Indeed, even the Dart community offers several implementations to handle reactive state, right now. For example:
So, the good news is: the front end community is able to write reactive code. As a post scriptum, I couldn't resist to implement my own, very naive and with plenty of defects, class Ref<T> {
Ref(this.computation, {required this.dependencies})
: _value = computation(),
_isDirty = false;
final Set<Ref> dependencies;
final T Function() computation;
bool _isDirty;
T _value;
T get value {
for (final d in dependencies) {
if (d.isDirty) d._reCompute();
}
_value = computation();
return _value;
}
set value(T newVal) {
if (newVal != _value) {
_value = newVal;
_isDirty = true;
}
}
get isDirty => _isDirty;
void _reCompute() {
computation();
_isDirty = false;
}
} This implementation shows several problems:
The bottom lineAre we able to solve this problem with the current state of Dart? Finally, a question: should this thread be kept alive, or does it make sense to close it and move these questions to the metaprogramming thread? |
It's good to keep this thread separate since the main metaprogramming issue is already huge and this is discussing a specific well-defined use case for metaprogramming. |
Alright, thanks 🙏🏽 |
While browsing this repo, I just bumped into this issue. I'm sorry, I didn't know about that proposal (if I did, I wouldn't have wasted anyone's time); to me, at this point, this issue has basically become a duplicate of #1874. |
Related to this, in the meantime, javascript is experimenting with a built-in reactivity system directly in the language, testifying how the "frontend world" is at least considering an explicit, fine-grained reactive API as part of the language itself; I've also read some positive feedback about this from Evan You itself. I don't like js, and I realize Dart/Flutter is a whole different thing, but I think it's great to be aware of how other ecosystems solve this problem, or that at least it's not such a crazy idea |
Hi there,
This proposal is about reactivity as a built-in feature of Dart; much of what I am about to write is inspired from what I've taken from the Meta-Programming thread(s), from its discussions (such as #1482, etc.), from this issue, from #450, and from other solutions out there (Svelte, Vue3).
Motivation:
Dart's website states:
As that, in my opinion, Dart should naturally have built-in reactivity.
Indeed, reactivity is something that is closely related to clients and UIs: some final user click something --> state changes --> UI reacts to that. That is why us "front enders" write code that react to state change and/or asynchronous operations or events.
This is no news in the front end world, see Svelte or Vue3.
I think it's safe to say that reactivity as a base language feature in Dart would be loved.
Proposal
A good reactivity model should be searched; such model should fit Dart well.
Reactivity is about having some sort of class, say
Reactive<T>
, that allows the developer to:But this goes beyong having a built-in
Reactive<T>
class.While such class could be intelligently implemented (and this happened already: there are plenty of libraries out there doing exactly that), this proposal asks Dart to introduce a Reactive operator that cuts down opinionated approaches and boilerplates. This operator should be synchronous.
I'm not sure which symbol would fit right now, so let's use something that isn't used such as
§
(silcrow symbol) in the below examples.The
§
operator under the hood would create and handle aReactive<T>
.Indeed, the
§
operator would be just syntactic sugar for taking care of the declaration, instantiation, subscriptions, unsubscriptions, getters and setters of aReactive
.This would reflect other front end approaches in which the developer is lifted from doing such delicate work and can focus on the actual logic.
Here's some examples:
Once solved the above-shown ambiguities, this would be a first step towards easy-to-customize reusable stateful logic such as:
I'm aware that multiple returns isn't a thing yet (#68): this is just a pure imaginative example.
Static metaprogramming?
I'm not sure if the current proposal of static metaprogramming could achieve something similar; well, obviously I am limited to my own imagination and experience.
Here's a trial:
Here's another:
As you can see I'm lost when it comes of what really meta programming is able to unlock for us.
I know, the following is very imaginative, and kinda out of scope, but in Flutter it would be awesome to handle state as the following:
This would allow Flutter to narrow down the state management solutions to just scoping state in the right place, distinguishing ephemeral vs global (or sub-global) state (as opposed to handle reactful state and scoping state in the right place).
I'm hopeful that static metaprogramming will address these problems, but in my opinion it just really feels safe and easy to use a simple operator to handle reactivity.
Final notes
Out there, React hooks marked the way years ago, Vue3 improved it a lot and Svelte showed that cutting boilerplate is possible.
While the marketing-like sentence above might be true, scope of this proposal isn't "Svelte/Vue/Whatever is awesome, let's just do the same and see what happens".
As stated above, reactivity works well withing the front end / client context, but to be fair it is handy even for general purpose code (or backend) and shouldn't be limited to Flutter or another Framework.
While it is pretty clear that Static Metaprogramming tackles several pain points, it is unclear to me if it will be possible to alter or introduce new operators like above.
As a wrap up: the main objective, for me, is to have as less boilerplate as possible when using reactive / stateful logic; at the same time, such logic should be easily reusable.
The text was updated successfully, but these errors were encountered: