Closed lucavenir closed 2 years ago
Sounds like a spreadsheet - when one value changes, all computations depending on it are recomputed.
The general idea seems to be that:
§var b = expression;
captures the entire expression, listens for changes to any reactive variables in it, and if any changes, it evaluates the entire expression
again, then stores it in b
, which may again trigger more updates.
That's probably not viable the way it's suggested here.
The example §var d = a + §b;
is very simple. I'm sure we could make that work, somehow.
What about §var d = functionWithSideEffect() + §b;
If b
changes, is functionWithSideEffect()
evaluted again, or did we remember the value from the first time it was evalauted? We probably run it again. That's why §var d = §a + §b;
works.
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.
Take §var d = await something() + §b;
. Will the value coming from b
be delayed until await something()
has completed? What if §b
changes again before the future has completed? It, at a minimum, needs to ensure that the result of the former change isn't written to d
after the result of the latter.
Maybe just prohibit asynchrony inside reactive computations.
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 §a++
not be a cyclic computation depending on itself?)
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 value
getter.)
Something like:
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:
§var b = expression;
captures the entire expression, listens for changes to any reactive variables in it, and if any changes, it evaluates the entireexpression
again, then stores it inb
, which may again trigger more updates.
I came up with a modified version of ValueNotifier
that listens to other ValueNotifier
s:
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, §
would just be syntax that translate §x
into x.value
and §x = 5
to x.value = 5
.
What about
§var d = functionWithSideEffect() + §b;
Ifb
changes, isfunctionWithSideEffect()
evaluted again, or did we remember the value from the first time it was evalauted? We probably run it again. That's why§var d = §a + §b;
works.
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.
It doesn't interact well with asynchrony.
Okay... now I made an AsyncReactive
class. I can probably do better at making a universal version that works with both regular objects and futures but I'm no expert on FutureOr
. (Relatedly, the following is so close to implement
ing Reactive
, but alas, Future<T> Function() computation
is not a subtype of T Function() computation
.)
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();
}
}
Take
§var d = await something() + §b;
. Will the value coming fromb
be delayed untilawait something()
has completed? What if§b
changes again before the future has completed? It, at a minimum, needs to ensure that the result of the former change isn't written tod
after the result of the latter. Maybe just prohibit asynchrony inside reactive computations.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?
By using setValue
instead of set value
, we can return a Future
that can be awaited once all listeners have been notified. That pretty much answers all the questions. It also means that §x = 5
will need to be updated to x.setValue(5)
:
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 (final
, var
, or the type) instead of the variable name, so you don't have to keep using the §
(or whatever) symbol whenever you refer to the variable.
that is kinda what AngularDart do in the background of the Component
.
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 OnInit
etc in AngularDart
. With that you can do your computation when the heartbeat of the framework executes the cycle with all or some variables of a computation changed.
What about
§var d = functionWithSideEffect() + §b;
Ifb
changes, isfunctionWithSideEffect()
evaluted again, or did we remember the value from the first time it was evalauted? We probably run it again. That's why§var d = §a + §b;
works.
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? §var d = function() + §b;
would remember the value of the call, whereas §var d = §function() + §b;
and §var d = function(§a) + §b;
would rerun the function each time the computation is run.
That behavior could extend to both reactive and nonreactive variables in these declarations - If you have reactive variables a
and b
, and normal variables c
and d
, §var e = §a + b + §c + d
would update e
whenever a
changes, but always use whatever values b
and d
had at the time of declaration. It'll use whatever value c
has at the time a
changes, but won't update in response to a change in c
since c
isn't a reactive variable.
It doesn't interact well with asynchrony. Take
§var d = await something() + §b;
. Will the value coming fromb
be delayed untilawait something()
has completed? What if§b
changes again before the future has completed? It, at a minimum, needs to ensure that the result of the former change isn't written tod
after the result of the latter. Maybe just prohibit asynchrony inside reactive computations.
Maybe declaring a reactive variable with a reactive asynchronous component could change the type of the variable to a Future. Something like §var c = await §something() + §b
would create a reactive Future, where when b
changes, c
becomes a new Future running something() again and adding that value for b
. Going by the above approach, to use c
you have to await c
to get the value using the most recent value of b
, or await §c
to make a reactive declaration that continues to update going forward.
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 Reactive
implementation is, there's simply no point in updating a variable until it's read. Instead, a getter or other function that computes its value when needed is better. Or, if you need to update and cache several values, do that all in one function with a name like updateState()
.
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 notifier.updateValue(1)
vs x = 1
). Flutter already has ValueNotifier
and ChangeNotifier
, but their main usage isn't to tie other ValueNotifier
s to them, it's to have widgets that can be rebuilt when the values change. And even those widgets wait for the framework to call build
to actually rebuild. It's all about timing.
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.
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.
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?
c
a + b
(ie, a
and b
)a
and b
(to determine if any of them are reactive)Given my Reactive
implementation above, it could then replace the declaration with the following and get fully reactive variables.
var a = 1;
Reactive<int> b = Reactive(() => 2, []);
Reactive<int> c = Reactive(() => a + b.value, [b]);
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.
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.
Is 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". Indeed, i's easy to see that:
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.
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.
To my understanding, it doesn't look like so.
I want to underline @Levi-Lesches's last comment's code and his question:
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?
- the inferred type of
c
- any identifiers in
a + b
(ie,a
andb
)- the types of
a
andb
(to determine if any of them are reactive)
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 b
is a Reactive<int>
, we have to access its .value
property to inspect its value.
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 b.value
several times. Is that also possible?
Now, one might think that adding @reactive
feels magical (in a bad way) and and actually more verbose than just writing .value
in our code. While the verbosity is similar, there's plenty of chances that can be intercepted by that macro, such as implementing APIs that either accept a Reactive<T>
or just T
. The macro would have the responsibility to unref a value, obtaining an API that is transparent on the use (or lack of) reactives.
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.
Then - and I'm asking out of my ignorance here - what's the difference between defining a macro directive such as @myMacro
and defining a custom operator such as §
? Is it even possible with meta-programming? If yes, then, why not implement all of the above as:
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 var b = §2
(read "var b equals reactive two"), and to then use §b
("reactive b") in our code, whenever it is needed?
Given all the above, I think the details about reactivity's implementation are out of scope now. After reading the feedback, I surfed the internet quite a lot for more intel onto reactivity: to my understanding there's a good chance that reactivity implementation is (not surprisingly) opinionated.
Indeed, even the Dart community offers several implementations to handle reactive state, right now. For example:
ChangeNotifier
, ValueNotifier
and in general Listenable
are kind-of reactive objects.So, the good news is: the front end community is able to write reactive code. The bad news is: it either requires a compilation step to generate code on our behalf (Svelte 👀), or to have quite a lot of boilerplate (even though there's quite the effort - and some very excellent jobs - at minimizing it). In my opinion, this proposal is about being able to write as less boilerplate possible when it comes to reactives.
As a post scriptum, I couldn't resist to implement my own, very naive and with plenty of defects, Ref<T>
class here, to serve as an example of "opinionated":
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:
value
iff it is necessary, i.e. when actually using such reactive variable (via get
). Is this OK? Is this desirable?Are we able to solve this problem with the current state of Dart? I realize I may be redundant here, but while writing composable-like API for Flutter might sound exciting for some, I think that the objective might be to find a "Dartish" way to implement actually reusable and reactive state with a lot less boilerplate (compared to nowadays solutions).
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?
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.