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

Allow referring to assigned variable within its initializer. #542

Open
natebosch opened this issue Aug 23, 2019 · 8 comments
Open

Allow referring to assigned variable within its initializer. #542

natebosch opened this issue Aug 23, 2019 · 8 comments
Labels
request Requests to resolve a particular developer problem

Comments

@natebosch
Copy link
Member

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 lrhn added the request Requests to resolve a particular developer problem label Aug 26, 2019
@lrhn
Copy link
Member

lrhn commented Aug 26, 2019

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.

@lrhn lrhn changed the title Allow referring to assigned variable within cascade Allow referring to assigned variable within its initializer. Aug 26, 2019
@natebosch
Copy link
Member Author

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
Copy link
Member

lrhn commented Aug 26, 2019

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
Copy link
Member Author

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
Copy link

dnfield commented Feb 14, 2020

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
Copy link
Member

eernstg commented Feb 17, 2020

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
Copy link

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
Copy link
Member

lrhn commented May 28, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
request Requests to resolve a particular developer problem
Projects
None yet
Development

No branches or pull requests

5 participants