-
Notifications
You must be signed in to change notification settings - Fork 205
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
Declaration expressions and declaration promotion #1420
Comments
I like this! I think it would be helpful to include the rule that every composite statement introduces a scope which is enclosing the entire composite statement and nothing else. For example: void main() {
if (b) {...} else {...}
// works like:
{
if (b) {
...
} else {
...
}
}
} such that any variables introduced by Compared to #1210, binding expressions, the main difference to a variable declaration expression is that the former uses The implicit variant (where the name of the new variable is derived from the initializing expression in some way) is orthogonal: Both forms can easily omit that feature or include it, and it would be a non-breaking change to add it later. |
Thanks for the corrections. Using Generally the idea here is to not change the structure of expressions, but allow you to name existing sub-expressions, and then reuse them later. With this, we can desugar the About only allowing |
Can we hide the original field/ variable with new variable like |
@Cat-sushi The scope would not support that. Local variables are added to the surrounding scope, even before their declaration. We could change the way scopes work in Dart, and introduce a new scope for every |
@eernstg About wrapping all composite statements/control flow structures in extra scopes, I'm not sure it's precisely what we want. There is me wanting to be able to do something about the variable set in a failed test, so Apart from that, it should be one scope per iteration in order to allow variable declarations in the test to be a new variable on each iteration. So a For { // Outer scope
var i = 0;
loop: { // Iteration scope
if (!(i < something)) break loop;
$body: { // body scope (if necessary?)
body[continue -> break $body]
}
i++;
restart loop; // pseudocode.
}
} The So, I think it's possible to do something with scopes here, but it's more complicated than just wrapping every composite statement in an outer scope (although that too might be worth it, to contain variables ... as long as I get my |
@lrhn Thank you. I probably understand what you said. |
Fixed the "dag" to use For As described, there is no And yes, restricting to only allowing |
That's not an expression, so no. As for |
@lrhn wrote:
The reason why I'd recommend limiting the scope of the variable If you want the variable to be available after the loop then you'd simply use an old-fashioned local variable declaration outside the while statement. |
@tatumizer |
It is |
The biggest argument (to me) against prolonging the lifetime of variables declared inside a construct is what it would do to existing variables in the surrounding scope. int i = 0;
while (i := iteration.next()) {
.. something(i);
}
use(i); // <--- Which `i`. Here the "Which So, if the All in all, I think I agree with Erik that all constructs should introduce a new scope. while(test(i := next()) {
use(i);
} else {
discard(i);
} (So #171, please!) |
Well, if you're breaking then you're still in the scope of the loop, so you still have access to the loop variable while (test(i := next())) {
if (should_break(i)) {
on_break(i);
break;
}
on_loop(i);
} else {
on_end(i);
} |
Right, that was your complaint -- that you would lose access to
|
It seems there are many proposals for such a feature -- #1191, #1201, #1210, #1514, and this one, #1420, to name a few -- that all do essentially the same thing. Can these be consolidated into one proposal that's more concrete on the syntax? @leafpetersen, I noticed you posted the same challenge in many of these -- do you have any anecdotal thoughts/preferences? |
@Levi-Lesches we've budgeted time in the upcoming quarter to work on getting a consolidated proposal + syntax. I don't want to prejudge more than that. |
It seems the problem is the short-circuiting -- is it possible to separate what gets evaluated at runtime from what is statically analyzed at compile time? Like, this code is invalid: int a = 5;
bool b = true;
String c = a < 42 || b < 30 ? "yes" : "no"; since b is known to be a bool ahead of time, even though the second half of the if ((x := a) > 0) || (y := b) > 0) the compile can (and should, IMO) define |
Yeah, I don't know why I didn't see that.
It seems like it. At a cursory glance, #1191 and #1210 also suffer from this. I'd be in favor of a merge between #1201 and #1514, where you would write: if (shadow maybeNull != null) {
use(maybeNull);
} which would also allow you to use shadow maybeNull; // a field in a class
bool isValid = someOtherCondition && maybeNull != null; // refers to the local version
print(maybeNull); // refers to the local version EDIT: Using a |
You can, but only down-chain: a?.method(x := expr, foo(x)).otherMethod(x.length); It's no different from declaring the variable earlier in the function, and only being able to rely on it having a value after the assignment: List<int> x;
a?.method(x = [1, 2, 3], foo(x)).otherMethod(x.length); This code is currently valid. The assignment makes the variable definitely assigned, but only down-stream from the assignment (only on code dominated by the assignment). If you use So, we already do all the computation needed to figure out where the variable can be used, and you hadn't even noticed. You can just treat |
About if ((x := a) > 0 || (y := b) > 0) {
// x is available here, maybe > 0.
} else {
// x *and* y available here, neither greater than 0.
} then the if ((x := a) <= 0 && (y := b) <= 0) {
// x *and* y available here, neither greater than 0.
} else {
// x is available here, maybe > 0.
} which doesn't look that surprising to me. |
All these comments make me wonder why we didn't stick with good ol' In fact, I think that allowing declaration expressions can encourage messy code, just as these examples illustrate. I'd rather have to read final bool complex1 = a || b && !c && (d || a);
final bool complex2 = b && a || !c || d;
if (complex1 || complex2) { /* ... */ } than have to see if ((complex1 := a || b && !c && (d || a)) || (complex2 := b && a || !c || d)) {
/* ... */
} and option 1 allows/encourages for descriptive variable names, whereas simply including the variables inline in option 2 make it more difficult to read. |
|
Well, promotion is nice |
class Counter {
int? count;
void incrementShadow() {
shadow count;
if (count == null) count = 0;
count++;
}
// A shadow simply "wraps" the local context with its own local variable
void incrementRegular() {
final int _count = this.count;
if (_count == null) _count = 0;
_count++;
this.count = _count;
}
} Both the declaration and saving the value back to |
I've split off "Assignment Promotion" to its own issue (#1844) so I could tag it with the "flow-analysis" label. |
I'm generally supportive of the idea of adding a ConcreteStarting with the concrete, let's consider each of the examples from the original proposal in comparison to a normal expression scoped Example 1foo(v.property.name, v.property.value); With this proposal:
With general
This is, to me, vastly more readable in the second form. Example 2print(1 + (var o = 2) + o * 2); With general print(let var o = 2 in 1 + o + o*2); Again, the second form is more readable to me. Example 3C() : _controller = (var c = StreamController()), stream = c.stream; This isn't handled by a general let. Allowing local variable declarations in initializer lists does solve it, however, and in a much more readable way: C() : var c = StreamController(), _controller = c, stream = c.stream; Example 4BTree<T> buildDag<T>(int depth, T leafValue) =>
depth == 0 ? BTree.leaf(leafValue) : BTree.node(var dag = buildDag(depth - 1, leafValue), dag); Becomes: BTree<T> buildDag<T>(int depth, T leafValue) =>
depth == 0 ? BTree.leaf(leafValue) : var dag = buildDag(depth - 1, leafValue) in BTree.node(dag, dag) end; Again, vastly more readable to me. Example 5T firstWhere(Iterable<T> element) {
var it = elements.iterator;
while (it.moveNext() ? !test(var value = it.current) : throw StateError("no element"));
return value;
} There is no way to write this code with general let. I think I consider that a feature, not a flaw. :) The remainder of the examples I believe focus on using this as a mechanism to bind and promote variables inline in GeneralizingGiven that this does in fact solve the general class C {
int? x null;
void test() {
{
print({var x = 3 : x, x : x}); // What does this print? How do I explain to a user why it prints what it prints?
print(x); // What does this print?
}
{
print( { if(var x = 3) x : x, x : x); // What does this print? How do I explain to a user why it prints what it prints?
print(x); // What does this print?
}
{
// Is this an error?
// If so, this makes the feature inferior to general let, which allows encapsulating local variable names.
// If not.... WAT?
print({var x = 3 : x, var x = 4: x});
print(x); // What does this print?
}
{
print(<Object>["Here's a really long list of things",
"It has stuff in it",
ConstructorCall(text: "It has constructor calls too",
build : () {
print("Maybe even lambdas");
},
stuff: "Also other stuff",
},
"A really long thing" + "Other things" + foo<int>(NestedCall(Something(var x = "Look a squirrel"))),
{"Config" : 3,
"Fooble" : var y = 4,
"I can do this all day", 6
}
]);
// Did you see the assignment above? Would you see it in a code review?
// Could you explain to a user why this doesn't print `null`?
print(x); // WAT?
}
{
print(var y = 3 ?? var x = 4);
print(y); // prints 3
print(x); // prints null. Wait, what?
}
} SummaryTo summarize, it seems to me that this proposal:
I'm open to being convinced otherwise, but as it stands, I'm quite opposed to this approach. |
I'll have to agree to disagree on the readability. I find I remember some language showing me recursive data something like:
That's the same idea: Name the value the first time it occurs and the allow reusing it, rather than name the value first, separately, and then use the name every time. I do agree that scoping being implicit gives it some potentially sharp edges. As for scoping, what I would prefer is that every variable declaration introduces a scope starting at its declaration and ending at the end of the surrounding block/structure (I say "structure" because I want the A It also becomes an error to introduce the same variable name more than once in the same block. With that, the scope examples above becomes: class C {
int? x null;
void test() {
{
print({var x = 3 : x, x : x}); // prints {3: 3}. The `var x = 3` introduces a variable that lasts until `}`.
print(x); // Prints 3
}
{
print( { if(var x = 3) x : x, x : x); // Compile-time error, condition is not a boolean?
print(x);
}
{
print({var x = 3 : x, var x = 4: x}); // Compile-time error, `x` already declared in this scope.
print(x);
}
{
print(<Object>["Here's a really long list of things",
"It has stuff in it",
ConstructorCall(text: "It has constructor calls too",
build : () {
print("Maybe even lambdas");
},
stuff: "Also other stuff",
},
"A really long thing" + "Other things" + foo<int>(NestedCall(Something(var x = "Look a squirrel"))),
{"Config" : 3,
"Fooble" : var y = 4,
"I can do this all day", 6
}
]);
// Did you see the assignment above? Would you see it in a code review?
// Could you explain to a user why this doesn't print `null`?
print(x); // Yes, there is a definitely executed `var x` prior to this code in the same block.
// It's always possible to write obscure code.
}
{
print(var y = 3 ?? var x = 4);
print(y); // prints 3
print(x); // Would be a compile-time error, `x` refers to the prior `var x = 4`, but is not definitely assigned.
}
} It's always possible to write obscure code. I'm not particularly worried about people doing that to themselves, as long as they can avoid it if they want to. I don't want to force people to do something unreadable, but I also don't think it's necessarily disqualifying for a language feature that you can do something obscure with it. |
We have discussed the addition of an extra scope around every composite statement ( I think that's crucial! (.. and I included it in #1210.) If we don't have it then said variable could be in scope any number of lines after the end of the
But I'm not so worried about the unusual scoping as long as the primary use case (as far as I can see) will be the introduction of a new variable in a condition of a composite statement. As @lrhn wrote,
but I do think that it is dangerous to have a scoping behavior that allows these in-expression variable introductions to survive far beyond the, say, Of course, we could have a lint that flags any new variable which is introduced in this manner if it occurs at a column which is higher than 20. And we could ask IDEs to make the variable name blink. ;-) |
Yes, we really, really will have to disagree here. For me to be at all open to this proposal, I would need substantial empirical evidence to support the readability of this, because I find it completely unreadable. Moreover, I would point out that JS function scope variables have a similar property, and as best I can tell, ES6 added block scope for a reason. Finally, I accept that you find
As far as I'm concerned, it's impossible not to write obscure code with this feature. That is, all of the examples are obscure to me. I'm just pointing out that there are no upper bounds on the obscurity that's possible.
No, that isn't the same idea. That's a simple recursive {
print({var x = 3 : x, var x = 4: x}); // Compile-time error, `x` already declared in this scope.
print(x);
} This is a usability footgun for this feature, and is one of the many reasons that expression // Works
Pair<int, int> sumOfSquares(Pair<int, int> fst, Pair<int, int> snd) {
return Pair(let sum = fst.x + fst.y in sum * sum,
let sum = snd.x + snd.y in sum * sum)
// Doesn't work
Pair<int, int> sumOfSquares(Pair<int, int> fst, Pair<int, int> snd) {
return Pair((var sum = fst.x + fst.y) * sum,
(var sum = snd.x + snd.y) * sum)
}
This is, to be clear, one of my most significant concerns with those proposals, and you may notice that there are substantial concerns about this raised in the issue by other people as well. I accept that it may be necessary, and I think in this very limited form it might be ok, but it is still a point against these proposals. |
I'm not proposing that variable declarations escape their containing block - or "structure", if we want declarations inside That's JavaScript did was much worse, it made the variables function scoped, not block scoped. Example: function foo() {
x = 2;
y = 3;
if (x == y) {
var y;
}
var x;
} The function foo() {
let x = 42;
{
console.log(x); //ReferenceError: Cannot access 'x' before initialization
let x = 10;
}
} The That's what I'm proposing: essentially to allow you to write It does mean you cannot reuse the same name later in the same scope (unless we actually allow shadowing inside the scope, but that can get weird very quickly). And I can see how that's annoying, like the The confusion may be based on a prior assumption about what Pair<int, int> sumOfSquares(Pair<int, int> fst, Pair<int, int> snd) {
return Pair((var sum = fst.x + fst.y) * sum,
(var sum = snd.x + snd.y) * sum) the two var sum = fst.x + fst.y;
var prod1 = sum * sum;
var sum = snd.x + snd.y;
var prod2 = sum * sum;
return Pair(prod1, prod2); The second one is not surprising, because obviously we are declaring the same name twice in the same scope. I'm not sure why the prior example was not just as obvious, unless someone was already assuming Pair<int, int> sumOfSquares(Pair<int, int> fst, Pair<int, int> snd) {
return Pair((var sum = fst.x + fst.y) * sum,
(sum = snd.x + snd.y) * sum) // no `var`
} just like they likely would the expanded version. Escaping the current expression, unlike a var list = [1, 2, var x = compute(), x, 4];
Constructor() : controller = (var c = StreamController), stream = c.stream; In both situations, we can't just insert a We can instead introduce a I'll update the original post to make scoping more precise (and drop the "maybe we can also" parts, or at least move them to a separate section). I'm willing to work towards variables not being in scope before their declaration, so you can write |
There is the Binding Expressions proposal that you could do: if (obj.@prop != null)
print(prop); // prop is local and promoted to non null |
Edit 2021-11-24 - Restructure, move "maybe we can also" parts into separate section.
This is inspired by #1091, #1201 and #1210, but is slightly different in approach/scope.
We have an issue with promotion of instance members (or any non-local variable in general).
The #1091 approach is to do a binding at the promotion point. The #1201 approach is to introduce new variables in tests (but do it implicitly in some cases), and #1210 introduces new variables with a new syntax (and also potentially implicitly).
This is a proposal for two features which takes some of #1201 and #1210, but do not introduce any names implicitly, and where the binding can be used independently of the need to promote the variable.
Feature: Assignment Promotion
(Note: discussed in #1844)
First, allow a local variable assignment of the form
id assignmentOp expression
(potentially parenthesized) to act like the variable itself, when used in a test. We currently allowif (x != null) ...
to promotex
. This change would also allowif ((x = something) != null) ...
to promotex
.It only affects the left-most variable of an assignment, so
if ((x = y = something) != null) ...
will only promotex
, not bothx
andy
(although that's also an option if we really want it - treat bothx
and the assigned expression as being tested)If you do
if ((x += 1) != null) ...
that still works, but there aren't that many operators which return a nullable result, so the usefulness is limited.This is a very small feature, but it allows you to do “field promotion” as:
Feature: Variable declaration expression
Allow
var x = e
andfinal x = e
as expressions. No types, onlyvar
andfinal
. Must have an “initializer expression”.The expression introduces a new variable named
x
in the current block scope, and it’s a compile-time error to refer to that variable prior to its declaration.For statement-level declarations, the “prior to declaration” is anything prior to the source location of the declaration. For a declaration with an initializer expression, that’s equivalent to hoisting the declaration to the top of the current scope block, but keeping the assignment at the original declaration point, and saying you must not refer to the variable where it’s not definitely assigned.
These expression variables, which must have initializer expressions, has the same behavior:
=>
function body).For type inference, the context type of the declaration becomes the context type of the RHS, the static type of the RHS becomes the declared type of the variable.
The construct is an
<expression>
:An
<expressionsStatement>
expression also cannot start withfinal
orvar
, just like it currently cannot start with{
.The new constructs, being
<expression>
s, need to be parenthesized in most places, including before anis
check, but also that it can contain any expression as a RHS, including a cascade.Usage
This feature can introduce a variable at the first point where you need its value, rather than having to go back and declare it further up, even though that’s effectively what it does.
If you have an expression where a sub-expression is repeated twice, you can name it the first time, and then refer to that name the second time:
foo(v.property.name, v.property.value);
Can become:
The variable declaration works anywhere there is a surrounding scope (which is any expression), even if you can’t normally have statement-level variable declarations there.
As opposed to
let x = e1 in e2
-like constructs, where the scope is onlye2
, the variable uses the same scoping as other local variables, which is “until the end of the current scope”. Because of that, it can also be used where the expressions do not share a common (or at least not close) parent expression, like lists (potentially deeply nested inside another expression):or where we can’t technically have variable declarations, like initializer lists:
(which would currently be implemented using an extra helper constructor), or
=>
function bodies:(where we would currently use a
{}
body to be able to declare the variable up-front.)Promotion
The
var id = e
orfinal id = e
counts as assignments for assignment promotion (above), soif ((var c = this.capacity) != null) { ... }
would both readcapacity
into a local variable, then promote that local variable to non-null if possible.This is the proposed solution to promoting non-local-variable expressions: Introduce a new local variable with the same value, then promote that, and do it in a single expression. Only, unlike #1191, the binding feature is generally useful and not restricted to checks or promotion, and it doesn't clash with a potential pattern syntax for
is
checks. The binding is not linked to the test, the features are orthogonal, and should therefore also work if we introduce more tests (like pattern matching) in the future.Alternatives and similar features
There is no implicit assignment of a name to an expression, unlike #1201 and #1210. I personally found those hard to read. We can introduce those as well, so, for example,
var foo.bar.fieldName
as an expression would be equivalent to(var fieldName = foo.bar.fieldName)
. Basically:var selector.chain.last
, not followed by=
and in an expression position, is equivalent tovar last = selector.chain.last
(and similar forfinal
).Since this is based on the same logics as the current definite-assignment analysis, it can make variables available in only some continuation of the declaration.
For example:
Control flow statement scopes
Currently there is no special scope for the conditions of an
if
orwhile
statement. The condition expression belongs to the surrounding scope. (Or rather, there is no way to tell since you cannot introduce variables inside the condition expressionFor a
for (;;)
statement, there is a new scope introduced for thefor
statement itself, separate from the block scope of the body (it’s the parent scope of the body scope). The variables declared in the initializer part of thefor (;;)
loop are declared in that scope.We could introduce a condition-scope for
if
andwhile
statements, so that variable declarations in the test belongs only to that scope, and not the surrounding block scope.Example:
It would be consistent to introduce a wrapper scope for the control flow statement itself, like for
for (;;)
. It’s not necessary, but it means that the variable belongs to the outer scope, and may conflict with other variables in that scope.On the other hand, it also prevents constructs like:
which is a logical extension of the same pattern that we support for promoting local variables.
I’d recommend not introducing that scope.
That means that a variable unconditionally declared in
test
ofwhile (test) { ... }
is available in the body.Feature: Shorter syntax for local declaration
Alternatively, maybe preferably, introduce
x := e
as an in-line final declaration, equivalent to the abovefinal x = e
, and do not introduce any way to declare non-final local variables.That means that locally declared variables will all be final. I think that's a good thing. It prevents some use case, like:
but I'm not sure such uses are really that essential.
If the local variable declaration expressions can only introduce final variables, it means closures over them can just capture the value, and not worry about getting the correct variable. (Also
x := e;
can still be used as an expression statement as a short declaration of a final local variable).Examples (using
:=
only).Summary
This is really three features:
(x = e) is T
and(x = e) == null
to promotex
.var x = e
andfinal x = e
are expressions. They introduce variables into the current block (just like the similar statement-level declarations), but must not be used where the variable is potentially unassigned. Both are also subject to assignment promotion.x := e
is an expression. It introduces a final variable into the current block (likefinal x = e
above, but nothing similar tovar
). Also subject to assignment promotion.The text was updated successfully, but these errors were encountered: