Skip to content
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

Closed
lucavenir opened this issue Jul 13, 2022 · 13 comments
Closed

Built-in Reactivity #2345

lucavenir opened this issue Jul 13, 2022 · 13 comments
Labels
feature Proposed language feature that solves one or more problems state-duplicate This issue or pull request already exists

Comments

@lucavenir
Copy link

lucavenir commented Jul 13, 2022

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:

Dart is a client-optimized language for fast apps on any platform.

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:

  1. Declare a reactive variable of type T;
  2. Append subscriptions: (naively) a list of other reactives that should be notified of changes);
  3. Remove subscriptions, when variables aren't listening anymore (e.g. they are disposed);
  4. Getters and Setters of type T for whatever is inside the Reactive.

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 a Reactive<T>.

Indeed, the § operator would be just syntactic sugar for taking care of the declaration, instantiation, subscriptions, unsubscriptions, getters and setters of a Reactive.
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:

// declaring a normal and a reactive variable
var a = 4;
§var b = 3;

// using such variables
var c = a + §b;
§var d = a + §b;
print(c); // 7
print(§d); // 7

// what's the difference between reactive and normal variables?
§b = 4;
print(c); // 7, since `c` is not reactive
print(§d); // 8, since `d` is reactive

// what happens if we touch `a`?
a = 5;
print(c); // 7, unsurprisingly
print(§d); // should this be 8, since `a` is not reactive (!), or should this be 9, since `d` is reactive? (!!)

// say that `d` is now `8`: what happens if we update `b` now, after we touched `a`?
§b++;  
print(§d); // 10: `d` reacted to `b`, but its expression uses the current value of `a` (so.. as a pointer?)

// The following two lines would throw a compile-time error
§var e = 1;
e = 5; // Error: A value of type 'int' can't be assigned to a variable of type 'Reactive<int>'.
§e = 5; // Works fine.

// Actually, in the above examples we could have used `final` for the reactive variables
var x = 4;
§final y = 3;
§final z = x + §y;
§y++;
print(§z); // 8

Once solved the above-shown ambiguities, this would be a first step towards easy-to-customize reusable stateful logic such as:

// hook/composables inspired naming
useAsync<int>(Future<int> f, {initialState=null, immediate=false}) {
  §final data = initialState;
  §final error = null;
  §final isLoading = false;

  execute() {
    §isLoading = true;
    try {
      §data = await f;
    } catch(e) {
      §error = e;
    } finally {
      §isLoading = false;
    }
  }

  if(immediate) execute();

  return §data, §error, §isLoading, execute;
}

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:

@reactive // Macro
class ReactiveInt extends int {}

// Verbose, not scalable...
final a = ReactiveInt(5);
final b = ReactiveInt(a, dependencies: a);

Here's another:

// If it were possible to write metaprog. annotations on declarations...
@reactive
var a = 5;
@reactive
var b = 1;

// Would the following be enough with just that?
@reactive
var c = a + b;
a++;  // is this even possible?
print(c); // would this print 7?

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:

class _MyState extends State<MyStatefulWidget> {
  @state  // a setState macro shortcut that cuts down more boilerplate
  §final a = 5;

  §final b = §a ** 2;
  
  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: () {
        §a++;
      },
      child: Text("${§b}"),
    );
  }
}

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.

@lucavenir lucavenir added the feature Proposed language feature that solves one or more problems label Jul 13, 2022
@lrhn
Copy link
Member

lrhn commented Jul 13, 2022

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
}

@Levi-Lesches
Copy link

Levi-Lesches commented Jul 13, 2022

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 entire expression again, then stores it in b, which may again trigger more updates.

I came up with a modified version of ValueNotifier that listens to other ValueNotifiers:

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; 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.

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 implementing 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 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?

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.

@jodinathan
Copy link

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.

@Jetz72
Copy link

Jetz72 commented Jul 14, 2022

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.

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 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.

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.

@Levi-Lesches
Copy link

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 ValueNotifiers 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.

@munificent
Copy link
Member

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.

@Levi-Lesches
Copy link

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?

  1. the inferred type of c
  2. any identifiers in a + b (ie, a and b)
  3. the types of 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]);

@rrousselGit
Copy link

rrousselGit commented Jul 15, 2022

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.

@lucavenir
Copy link
Author

lucavenir commented Jul 15, 2022

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 important

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:

  1. When we introduce a reactive variable, we introduce a dependency, even if it's on a constant value;
  2. When we are introducing a dependency, we are introducing a directed graph;
  3. When we are introducing a graph, we are able to observe its current 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:

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?

  1. the inferred type of c
  2. any identifiers in a + b (ie, a and b)
  3. the types of a and b (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?

Reactivity implementation details

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:

  1. @Levi-Lesches offered an implementation example, up above;
  2. @rrousselGit implemented a well-known and wide-used reactivity library that goes beyond Flutter, Riverpod (which actually does more than just reactivity, but this is out of scope);
  3. I think it's safe to say that 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:

  1. Cycles: a good requirement for a reactive library is "no cyclic dependencies". This class has no control over this.
  2. Zones: Should such control, and many others, be allocated in the global scope (like Vue3 does, or should we use a "containerized" approach (like Riverpod does)?
  3. Lazy evaluations: this implementation evaluates value iff it is necessary, i.e. when actually using such reactive variable (via get). Is this OK? Is this desirable?
  4. Asynchronicity: it's unclear to me how to handle async scenarios with reactives at the moment (as you can tell, I lack the knowledge), but the wiki states it is possible to choose between synchronous updates and asynchronous ones;
  5. Deep reactivity: this implementation (should) force deep dependencies re-computation, i.e. everything is lazily re-computed as a cascade, whenever something changed in the meantime. But sometimes, reactivity should be shallow. It depends on the context.

The bottom line

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?

@munificent
Copy link
Member

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.

@lucavenir
Copy link
Author

Alright, thanks 🙏🏽

@lucavenir
Copy link
Author

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.

@munificent munificent added the state-duplicate This issue or pull request already exists label Aug 4, 2022
@lucavenir
Copy link
Author

lucavenir commented Apr 7, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature Proposed language feature that solves one or more problems state-duplicate This issue or pull request already exists
Projects
None yet
Development

No branches or pull requests

7 participants