dart-lang / language

Design of the Dart language
Other
2.68k stars 205 forks source link

Allow referring to assigned variable within its initializer. #542

Open natebosch opened 5 years ago

natebosch commented 5 years ago

Cascade syntax helps avoid redundant references to the same variable, but it breaks down once you need to reference that variable within a cascade call.

The following are logically equivalent:

var foo = bar();
foo.baz();
var foo = bar()
    ..baz();

However the same equivalence can't be made with:

var foo = bar();
foo.baz(() {
  foo.blub();
});
var foo = bar()
    ..baz(() {
      foo.blub(); // Error, "Local variable 'foo' can't be referenced before it is declared"
    });
lrhn commented 5 years ago

Dart does not allow access to a variable inside its own initialializer. (This is unrelated to cascades, so I've updated the title to the more general situation).

It is usually hit when a closure contains a reference to the variable, and the author knows that the function won't be called immediately.

Other examples include:

var subscription = stream.listen((o) {
   if (o == null) subscription.cancel();
   ...
});
var port = rawReceivePort((o) {
  if (o == null) port.close();
  ...
});

The problem is that if we allow access to the yet-uninitialized variable, we also have to give it a semantics. Dart has steadfastly refused to give access to final variables in a way where you can see them prior to initialization. With NNBD, this becomes even more essential because they might not even have the value null.

The options are to throw on access prior to initialization, or to give "some value". In either case (and the latter is unlikely, especially for NNBD), it requires an extra check on each access to the variable, which might be an unnecessary overhead.

natebosch commented 5 years ago

This is unrelated to cascades

I agree that solving the general problem for an initializer most likely solves it for cascades, however I do think the problem of cascades is a bit narrower and could potentially be solved more easily.

From a user perspective the most straightforward desugaring of a cascade would make the reference to the variable happen outside of its initialization.

lrhn commented 5 years ago

You are suggesting that an assignment to a cascade expression would perform the assignment to the receiver value prior to evaluating the cascade sections? That is possible, but somewhat inconsistent. In all other situations the cascade in total before the value becomes available.

It would still expose the object in a state prior to the cascade, which may be an initialization. E.g., var x = Foo()..init(()=>x)..finalize();. Here x is bound to Foo() pre-finalization, and init has access to that x variable, so it is possible that the pre-finalized Foo() leaks. That's a tricky case to debug.

If it's only variable initialization, and not any assignment, then it's inconsistent. It would mean that var x = Foo()..init(()=>x)..finalize(); and var x; x = Foo()..init(()=>x)..finalize(); would have different bindings for x if init calls its argument synchronously. That's ... badly inconsistent. But if we are talking all assignments, then x = Foo()..something()..finalize(); where x is a setter would see the pre-finalized Foo and might try to do something with it.

All in all, I think changing the evaluation order for cascades is too inconsistent and error-prone to be really viable.

natebosch commented 5 years ago

I see what you mean about the assignment to something other than a variable initializer. I think I was implicitly treating variable initialization as a different desugaring to other usages of cascades.

It feels sensible to me to treat var x = different from any other x =, but I can see the arguments against that as well.

dnfield commented 4 years ago

The duplicated bug I linked is basically this one, although I found it even more confusing because I wasn't trying to refer to the variable explicitly - but this code fails:

const Foo foo = Foo()..baz();

I would have expected that to desugar into:

const Foo foo = Foo();
foo.baz();

which is allowed - and

final Foo foo = const Foo()..resolve();

is also allowed.

eernstg commented 4 years ago

Just FYI: The notion of an anonymous method was proposed with, among other things, this particular kind of situation in mind. You could say that it works as a "better cascade". Here's the blub example expressed using an anonymous method:

// Desired actions.
var foo = bar();
foo.baz(() {
  foo.blub();
});

// Same thing using (proposed, not yet implemented) anonymous methods.
var foo = bar()..{ baz(() => blub()); };

The point is that the .{ /*statements*/ } or ..{ /*statements*/ } construct contains regular code where the receiver (obtained by evaluating bar()) can be denoted by this. In particular, we can call blub() as shown, as opposed to the situation with a cascade. Of course, this may be omitted for member accesses (so this.baz() can be written baz()).

These properties make the code in .{ /*statements*/ } or ..{ /*statements*/ } similar to the code in an instance method of the receiver bar(), hence the name 'anonymous methods'.

Reprevise commented 6 months ago

Came across this today with WebViewController via webview_flutter.

final WebViewController controller = WebViewController()
  .. // ...
  ..setNavigationDelegate(
    NavigationDelegate(
      onPageFinished: (url) {
        controller.runJavascript(); // "Local variable 'controller' can't be referenced before it is declared" error
      },
    ),
  );

Looks like "anonymous methods" are just Kotlin's apply scope function? Those functions are super useful! Guessing I'd just be able to do:

final WebViewController controller = WebViewController().{
  setNavigationDelegate(
    NavigationDelegate(
      onPageFinished: (url) {
        controller.runJavascript(); // no error?
      },
    ),
  );
};
lrhn commented 6 months ago

You can implement something like that yourself, as:

extension WithSelf<T> on T {
  R apply<R>(R Function(T) onSelf) => onSelf(this);
}

Then you can write:

final WebViewController controller = WebViewController()..apply((controller) {
  setNavigationDelegate(
    NavigationDelegate(
      onPageFinished: (url) {
        controller.runJavascript(); // no error?
      },
    ),
  );
});

You'd have to name the controller twice (if you need it outside at all).

It would be nice to have a block with bindings that doesn't introduce a new function body. Doesn't have to bind the value to this, but binding it to something without that being a function parameter.

(Personally I'd like declaration expressions (#1420 or similar), which would allow something like (final controller = WebViewControler())..setNavigariontDelegate(...);. The scope that declaration would be the rest of the block, any code dominated by the declaration expression, but the parentheses makes the assignment happen before the cascade.)