GregoryConrad / rearch-dart

Re-imagined approach to application design and architecture
https://pub.dev/packages/rearch
MIT License
88 stars 4 forks source link

Internal error when using inherited types #36

Closed joranmulderij closed 9 months ago

joranmulderij commented 9 months ago

An example is the easiest:

class A {}
class B implements A {}

B myCapsule(CapsuleHandle use) {
  final (value, setValue) = use.state(B());
}

void later() {
  final valueA = use(myCapsule as A);
  final valueB = use(myCapsule); // Will throw error
}
#0      CapsuleContainer._managerOf (package:rearch/rearch.dart:133:7)
rearch.dart:133
#1      _CapsuleManager.read (package:rearch/src/impl.dart:35:36)
impl.dart:35
#2      _CapsuleHandleImpl.call (package:rearch/src/impl.dart:111:20)
impl.dart:111
#3      CapsuleContainer.onNextUpdate.tempCapsule (package:rearch/rearch.dart:184:47)
rearch.dart:184
#4      _CapsuleManager.buildSelf (package:rearch/src/impl.dart:52:30)
impl.dart:52
#5      new _CapsuleManager (package:rearch/src/impl.dart:9:5)
impl.dart:9
#6      CapsuleContainer._managerOf.<anonymous closure> (package:rearch/rearch.dart:132:13)
rearch.dart:132
#7      _LinkedHashMapMixin.putIfAbsent (dart:collection-patch/compact_hash.dart:543:23)
compact_hash.dart:543
#8      CapsuleContainer._managerOf (package:rearch/rearch.dart:130:22)
rearch.dart:130
#9      CapsuleContainer.onNextUpdate (package:rearch/rearch.dart:185:25)
rearch.dart:185
#10     _WidgetHandleImpl.call (package:flutter_rearch/src/widgets/consumer.dart:145:31)
consumer.dart:145
GregoryConrad commented 9 months ago

Hi! 👋

If I’m understanding your error correctly, it’s because you must read a capsule with its correct type (and not let it get upcasted, either implicitly or explicitly). Thus, if reading myCapsule (from your example), you must either do:

final data = use(myCapsule); // data is inferred to be B
final A data = use<B>(myCapsule); // explicitly write type if we’re upcasting

use<SomeType>(someCapsule) is also handy when writing use in a list initializer with some super type.

This issue has been asked before, so I’ll keep this open until I can update the docs

GregoryConrad commented 9 months ago

Fixed by https://github.com/GregoryConrad/rearch-docs/pull/4/

joranmulderij commented 9 months ago

I kind of feel like this should not happen, because the program fails on line 2 because of line 1. Also the resulting error is really obscure and it took me quite some time to figure out what was going on.

void later() {
  final valueA = use(myCapsule as A); // If I would remove this line, the program will run correctly.
  final valueB = use(myCapsule); // Will throw error.
}
GregoryConrad commented 9 months ago

final valueA = use(myCapsule as A); // If I would remove this line, the program will run correctly.

If this is copy-pasted directly from your code, this is where the error is originating from, not the next line. You cannot cast myCapsule to A (as it is a B Function(CapsuleHandle), aka a Capsule<B>). You probably meant final valueA = use(myCapsule) as A; (note the differing parentheses location).

If, however, you instead had use(myCapsule as Capsule<A>); in your code, well then that would be an issue with how Dart assumes types and would be the same reason you only have to do someNullableVariable! once. Take this code:

void typeTest(int? i) {
  int b = i as int;
  int c = i;
}

Notice how this compiles without needing a ! or another as int after the second i; this is because Dart uses its program analysis to prove that i must be an int after it is casted explicitly with as. However, I think this is unlikely and just that you had the as A in the wrong spot (inside the use() instead of after).

joranmulderij commented 9 months ago

Sorry, I wrote that code and did not test if it was correct.

This is a working example. The code below will throw an error on the valueB line. When commenting the valueA line it will complete successfully.

import 'package:rearch/rearch.dart';

class A {}

class B implements A {}

B myCapsule(CapsuleHandle use) {
  final (value, setValue) = use.state(B());
  return value;
}

void main() {
  final container = CapsuleContainer();
  final valueA = container.read(myCapsule as Capsule<A>); // Try commenting this line.
  final valueB = container.read(myCapsule); // Will throw error
  print(valueB);
}

Again, sorry for the confusion.

GregoryConrad commented 9 months ago

Thank you for pressing this a bit more! You unintentionally just enabled some new patterns and ergonomics that weren't possible before. I've got a fix locally and I also just realized that the fix will also what will enable cool patterns like [someCapsule, anotherCapsule].map(use).toList() to work reliably.

Here's what's happening. When you container.read(myCapsule as Capsule<A>), the container is internally building an upcasted copy of your data and is storing that internal state with its upcasted type. When you go to read the same capsule out of the container, but this time with the actual type, it gets the cached internal state (already present in the container at this point since it was previously built) and then attempts to downcast it to the actual type, but this fails, since the internal state itself was made with the upcasted type.

You can reproduce this with the following test:

  test('containers store capsules completely untyped (issue #36)', () {
    // Re-reading the capsule under a different type should not throw.
    () capsule(CapsuleHandle use) => ();
    useContainer()
      // ignore: unnecessary_cast
      ..read(capsule as Capsule<Object>)
      ..read(capsule);
  });

Fix should be out 🔜