-
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
Binding expressions #1210
Comments
It's a very interesting concept. It is necessarily limited to selectors because it has to link to an identifier to make the name optional. I'm not sold on the syntax. The |
Thanks, it builds on nice sources of inspiration, too. ;-)
That's actually not true: Any binding expression can be named ( The construct is deliberately limited to "small" constructs (
That would be handled as
I think it would be difficult to parse a
Yes, that is definitely an issue. I think we can at least milden the reading difficulty in a few ways: We could use a standard formatting where I'm also thinking that
It would be interesting to study the implications of that design. However, I tend to like the fact that
I don't think we should do that: I prefer your approach from the binding type checks/tests, where the new variable is added to the current scope. I think it's going to be considerably more error prone to have two variables named |
@eernstg I would like to explore what some actual code looks like under these various proposals. Here's an example piece of code that I migrated, which I think suffers from the lack of field promotion. This is from the first patchset from this CL - I subsequently refactored it to use local variables, the result of which can be seen in the final patchset of that CL. This was a fairly irritating refactor, and I don't particularly like the result. Would you mind taking a crack at showing how this code would look under your proposal? // The type of `current` is `Node`, and the type of the `.left` and `.right` fields is `Node?`.
while (true) {
comp = _compare(current.key, key);
if (comp > 0) {
if (current.left == null) break;
comp = _compare(current.left!.key, key);
if (comp > 0) {
// Rotate right.
Node tmp = current.left!;
current.left = tmp.right;
tmp.right = current;
current = tmp;
if (current.left == null) break;
}
// Link right.
right.left = current;
right = current;
current = current.left!;
} else if (comp < 0) {
if (current.right == null) break;
comp = _compare(current.right!.key, key);
if (comp < 0) {
// Rotate left.
Node tmp = current.right!;
current.right = tmp.left;
tmp.left = current;
current = tmp;
if (current.right == null) break;
}
// Link left.
left.right = current;
left = current;
current = current.right!;
} else {
break;
}
} |
Sure, here we go: // Context, based on CL:
// Comparator<K> get _compare: Instance getter of enclosing class.
// K key: parameter to enclosing function.
// int comp: local variable.
// Node current: local variable.
// It seems that `right` and `left` are local variables (the code won't work if it's the fields).
while (true) {
comp = _compare(current.key, key);
if (comp > 0) {
if (var oldLeft: current.left == null) break; // Snapshot `current.left` as `oldLeft`.
comp = _compare(oldLeft.key, key); // `current.left!` -> `oldLeft`.
if (comp > 0) {
// Rotate right.
current.left = oldLeft.right; // Replace `tmp` by `oldLeft`: it already has that value.
oldLeft.right = current;
current = oldLeft;
if (current.left == null) break; // NB: Not `oldLeft`, it's based on another `current`.
}
// Link right.
right.left = current; // Not sure how `right` was initialized?
right = current;
current = current.left!; // Snapshot of `current.left` not helpful here.
} else if (comp < 0) {
if (var oldRight: current.right == null) break;
comp = _compare(oldRight.key, key);
if (comp < 0) {
// Rotate left.
current.right = oldRight.left;
oldRight.left = current;
current = oldRight;
if (current.right == null) break;
}
// Link left.
left.right = current; // Again, not sure how `left` was initialized.
left = current;
current = current.right!;
} else {
break;
}
} It could have been slightly more concise if I had used the existing names I can see that I think it's worth noting that this code is updating the field that we're snapshotting, which is basically the worst case for using a snapshot. I used the names So there would presumably be lots of situations where we are actually just using the value of a field (and not updating the same field), and we just want to remember the outcome of null tests and type tests, and there are no name clashes. In that case we could of course use shorter names like |
Honestly, I think this syntax is far more versatile than the one in #1201 . Since To address @lrhn 's concerns about getter binding expressions such as Also, @eernstg, instead of typing out oldLeft in your example, wouldn't it make more sense to use $Old? Isn't the point of the $ syntax to demarcate the similarity in meaning while preserving sanity for later usages? |
I think the parentheses still separates the If we allow We probably need the |
I do think that there's value in having a distinct binding operator (e.g. colon) as opposed to equals, because binding assignments are clearly different from a normal assignment and should have a visual distinctifier. On the other hand, it might also be syntactically confusing, because : in other languages is used to denote a type. It might be easier to stick with an equals sign because of the confusion factor, or perhaps use something like := instead.
As to the whole parsing types thing, I agree that it's an issue that likely can't be solved without making distinct class and property names mandatory, which is definitely a verbose anti-pattern. It might take some advanced contextual analysis to distinctly allow It might (?) be more feasible if |
@tatumizer see #1201 for a better idea of what this is meant to solve. It is an enhanced assignment operator, but it has potential to be a lot more. |
Ah, okay I think I misunderstood your original point. So my understanding is that the whole point is to allow adding var in more places; there is disagreement over how to do that. The argument for some syntactical distinction is that this binding kind of assignment is actually different; it also represents an object, where normal assignment usually represents nothing. String baseData = "";
var x = baseData.(var y:)length > 0; //This version
var a = baseData.var b = length > 0; //Using current assignment syntax Take this example. If you use a separate operator, you can nest binding assignments virtually anywhere; the first pretty clearly displays what's going on. The second uses the current assignment syntax, which brings up a few issues:
Of course, that's not to say that this syntax doesn't have some issues. I personally like it better just for clarity's sake. |
Okay... but that's not the point of the issue. The idea is to not have to say The two are strictly different, as well: |
yes it does, but... why do more work and introduce parsing complexities to make code harder to read? |
True. It's a judgement call and probably personal preference. Ultimately, it'll be up to the Dart devs, not us; I think that we've probably exhausted this particular argument now. |
@tatumizer wrote:
Right, that is the topic of this discussion: Can we find a good way to introduce local variables which is more flexible than
That's what basically all these proposals (#1201, #1191, and this one) do. This proposal uses The use of
void main() {
String baseData = "";
var x = (var length=baseData.length).bitLength > 0;
print (length);
}
Indeed. However, this proposal also aims to allow concise forms where the name of the new variable is taken from the initializing expression or selector. For example, the above could be written as follows: void main() {
String baseData = "";
var x = baseData.var:length.bitLength > 0;
print(length);
} So the point is not that it is impossible to allow for
I'm proposing to use I would expect the named form to be used for whole expressions (with selectors I'd expect the nameless form to be much more common), like It might look slightly more "normal" if we were to use |
@AKushWarrior wrote:
I think the grammar could rather easily be adjusted to allow parentheses along these lines, and it might improve the readability in some cases. However, it's not obvious that they would appear to be very "natural" if they apply to the selector syntactically: @lrhn wrote:
That would eliminate the complexities associated with parentheses around a non-singleton sequence of selectors. But I still suspect that it's equally easy to learn to look further for the selector if the eye hits
True, but that only works when I think it makes sense to use |
If the inline The latter seems to be what you are suggesting, but it feels inconsistent (but then, if we have inline prefix operators, then they should probably all work that way, and then it is consistent. Also, why let So, I'd still prefer a suffix binding to an inline prefix binding, if we want to avoid parentheses, or just a prefix declaration if we don't care about that: 1 + (var x = foo.bar()).baz(x) // general declaration-as-an-expression, no selector special-casing
// or
1 + foo.bar().var x.baz(x) // and then `1 + foo.bar() as<var x>.baz(x)` is very close. |
As specified,
True, there's no specialized support for doing that. Again, I'm prioritizing a likely useful choice and conciseness, so there is no new syntactic device for specifying less common choices. But we could use Of course, there's a need to use a more verbose rewrite if we have null-shorting in the part that we wish to bind to the new variable. As usual, I gave priority to the interpretation that I found most likely to be useful, so the binding selector participates in null-shorting and the type of the new variable is nullable when its selector can be null shorted. |
One thing I'm worried about here is the use of But that does not seem to be the case. Would |
Why not? I don't see anything contradicting this. |
|
The latter is an antipattern; the latter also represents an object, where as the former represents void. |
Ah. I think that maybe |
@AKushWarrior wrote:
I chose |
@tatumizer wrote:
I agree with @AKushWarrior's remarks on this, but also: The binding expression is really intended to be used with "small" initializing terms (expressions or selectors), inside a bigger expression, and if you want to create a variable and bind it to a "big" expression then a |
@lrhn wrote, about suffix forms:
It's definitely an interesting variant of the proposal to use a suffix form. If we allow But we'd need some extra disambiguation in order to bind the suffix form to any other expression. Perhaps it would just require parentheses (in which case it's still a selector, so we don't have any other syntactic forms at all than the selector), like It should be possible. It's not obvious to me that one is much better than the other: class C {
int a, b;
// Usages; assume different scopes, hence no name clashes.
// Introduce a new name, apply to expression.
(a + b) as<var x> + x;
var x:(a + b) + x;
// Reuse name of identifier from enclosing scope.
a as<var> + a;
var:a + a;
// Use 'default' name, apply to selector.
a.bitLength as<var> + bitLength;
a.var:bitLength + bitLength;
// Use new name, apply to selector.
a.bitLength as<var b> + b;
a.var b:bitLength + b;
}
} |
@tatumizer Fewer parentheses! Linear writing! When you are writing and have already written That's the reason we've been considering an inline cast to begin with, and are considering an inline |
@AKushWarrior wrote (about
@lrhn wrote (about why a suffix form would be desirable):
I'd usually give a higher priority to readability than writability, because it's likely that code needs to be understood more frequently than it is modified, and for that it seems useful to announce the variable introduction just before the name of the variable: a.bitLength as<var> + bitLength; // Search back to see which `var` that was.
a.var:bitLength + bitLength; // Aha, we're creating a variable named `bitLength`. I think the most tricky part is the readability of the verbose cases where it makes a selector much bigger than usual: a.bitLength as<var newNameForBitLength> + newNameForBitLength;
a.var newNameForBitLength:bitLength + newNameForBitLength; It's worth considering a rewrite to use a normal I think the most important benefit that the suffix form brings is that it easily allows us to include any desired number of non-identifier selectors: a.foo(16)<int>(true) as<var x>.bar + x;
(var x: a.foo(16)<int>(true)).bar + x; |
Unreadable code is unreadable. Having side effects inside a Same issue could be argued for: class C {
int? i;
void foo() {
var veryLongAndNotParticularlyInterestingNameForCompleteness = 42, andMore = 37, i = 1;
// ...
// Lots of stuff.
// ...
var j = i + 1; // Would everybody know that `i` is not `this.i` here?
}
} which is unreadable without using any fancy features. I guess the point is that in some places, one use of a variable is a logical continuation of another, and we want to extend the scope to also cover the following use, and in other cases it's just incidental, and we don't want to extend the scope too much, unnecessarily. We want the scope to be predictable. (The current Dart variable declaration scope is "in scope in the current block, can only use after declaration". That's predictable, even if it's occasionally annoying. Since statements dominate the rest of their block, there is no question about what is "after", syntactically after the declaration, including after its initializer expression, is the same as semantically after). In this case, the scope of the variable is the current, nearest enclosing, statement (or declaration, I guess, for (For declarations, we don't want to wrap them in a new scope, not if that means Can you do: var y = this.var:y, sqry = y * y; ?) Can you write |
+100! I think the keyword My conclusion is that we need to protect ourselves from variables like the one in the binding expression I mentioned ( Inside an expression statement it's more manageable, because we would presumably read all of it in order to understand it, or we'd just skip over it because we know/think that we don't have to understand it right now. In any case, the new variable which is in scope inside the expression statement causes no particular dangers. When it comes to declarations,
we could make it an error to have a binding expression at all. Just use this instead: var y = this.y, sqry = y*y; The point is that a local variable declaration already allows us to introduce additional variables as needed. Alternatively, the binding expression variable could have a scope which is limited to the local variable declaration itself. This could be helpful if the variable is only created because it's needed in that declaration itself, and it's simply namespace pollution to allow it to be in scope after the declaration. In that case we'd specify the effect on the scoping structure directly (rather than giving a syntactic sugar based specification). But that's a known technique already (cf. initializing formal parameters of generative constructors), so that's not a big problem per se. About reachability and null shorting:
I'd just stick to the rules that I've already proposed, that is, it is a compile-time error to evaluate a variable which is introduced by a binding expression unless that binding expression is guaranteed to have been executed. So the [Edit: Correction, I did mention various null shorting constructs, but I did not mention the conditional expression, |
@tatumizer yeah, renaming variables in nested accessors would be not so easy to read, however, I think that would be used by experienced Dart developers in very specific scenarios so I wouldn't spend energy on it |
Reminder to the thread that these are supposed to help readability by removing unneeded lines. If the effect ends up being that programmers condense multiple lines of declarations into one |
I wouldn't mind seeing |
Postfix @ seems also very nice.
But maybe we could be giving too much attention to renaming and I am not sure if it should be the case.
Quick looking some of our code, renaming would be used very sparingly.
Out of curiosity Could the dart team run some tests looking for possible non renaming needing when setting local vars to properties?
… Em 5 de dez. de 2021, à(s) 19:24, Levi Lesches ***@***.***> escreveu:
I wouldn't mind seeing @ in a postfix position, assuming there were reasonable rules around parenthesis.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications on the go with GitHub Mobile for iOS or Android.
|
The thing is that those kind of if-declaration works but they are very bothersome to work with. // I am checking prop
if (obj.prop != null) {
}
// at this moment I think "I have to declare prop to use it"
// with the @ binding expression I have to type one letter and I can already move on with it:
if (obj.@prop != null) {
use(prop);
}
// I basically didn't have to change anything in my if clause at all.
// now with most other syntax I have to basically remake the if entirely:
if ((final prop = obj.prop) != null) {
use(prop);
} IMHO I would go for |
your own suggestion is much better to read and type than the alternatives: if (properties["firstName"]@name != null) use(name); again: // I am here
if (properties["firstName"] != null)
// I think "I have to declare a variable pointing to this index so I can use it"
// easily add 5 continuous chars to the if without changing its structure at all:
if (properties["firstName"]@name != null) use(name);
// rebuild the whole if from the start to declare the same name variable:
if ((final name = properties["firstName"]) != null) use(name);
really? Array accessors and other type of promotions are greater in proportion than properties promotion? |
Yeah, we are all collecting the possibilities from quite some time but I think I am sold rn if no other argument against the postfix I aways argued that a postfix solution is better to readability and also easier to build the logic in our brains: However, then came the And now you suggested a postfix
|
Was going to comment, having |
@tatumizer I'm not sure if this was implicit in your comment, but @lrhn has a proposal for that syntax here. I have a fairly long comment in that thread with my objections to both of these proposals (though restricting the scope to a single statement as @eernstg proposes above addresses some but not all of my concerns). |
The scopes are the heart of these two proposals. We have discussed them internally, and I think my position is well summarized here and here . If you remove the expression level scoping, then you're basically left with a potential alternative syntax for the |
I guess nothing will be as readable as a full, regular declaration or at least something very verbose/bothersome. The balance here is to find a syntax that is easy to write, easy to read (when you know the feature) and not bothersome/verbose when you need to use. We are in a major refactoring for NNBD and when we have to declare a variable for null check or type promotion, it has chance of breaking our current logic focus and we have to read some of the code again to get back to the level we were. Be it |
@tatumizer wrote:
The With a selector of the form My original proposal was more restricted: It did not allow for the bindings of binding expressions to occur at every kind of selector, but with The selector So the colon is only used with a selector of the form In any case, the Of course, any mechanism can be used to write line noise, and void main() {
// Basic, straightforward approach. Using a block to avoid polluting
// the rest of the function body with variables that are only used here.
{
var r = expressionThatYieldsTheReceiver;
var v1 = r.method1(42);
var v2 = r.method2<int>(v1);
r.getter3[v2].method3();
}
// Same thing using binding expressions, only allowing identifier selectors.
expressionThatYieldsTheReceiver
..@v1:method1(42)
..@v2:method2<int>(v1)
..getter3[v2].method3();
// Same thing using binding expressions, allowing all selectors.
expressionThatYieldsTheReceiver
..method1@v1(42)
..method2<int>@v2(v1)
..getter3[v2].method3();
} The variant that allows arbitrary selectors does have more expressive power, but I think it is considerably less readable. So I tend to prefer the (original) variant that binds the new variable to the result of the entire selector chain up to the next |
@Levi-Lesches wrote:
I'm not quite sure about that, I think it plays a significant role that binding expressions allow us to snapshot an intermediate result in a computation. We would read that kind of code by looking at the expression as a whole, and then check out which intermediate results are being snapshotted. class Link {
final int value;
Link? next;
Link(this.value, [this.next]);
@override
String toString() => '$value -> ${next?.toString() ?? 'nil'}';
}
void main() {
final Link? link = Link(1, Link(2, Link(3)));
// ... stuff ...
// Swap successors, straightforward approach.
if (link != null) {
var next1 = link.next;
if (next1 != null) {
var next2 = next1.next;
if (next2 != null) {
link.next = next2;
next1.next = next2.next;
next2.next = next1;
}
}
}
// Same thing using binding expressions.
if (link?.@next1:next?.@next2:next != null) {
link.next = next2;
next1.next = next2.next;
next2.next = next1;
}
} We would need to ensure that the flow analysis recognizes that the getter invocation |
@tatumizer wrote:
That's a very interesting idea! ... an important consequence, as I see it, is that this allows us to read the code as "compute this expression, then store the result in this new variable". The anonymous form was also mentioned several times ( The example from the previous comment would then be like this: void main() { ...
if (link?.next@next1?.next@next2 != null) {
link.next = next2;
next1.next = next2.next;
next2.next = next1;
}
} Later, @tatumizer wrote:
I guess this was aimed at the prefix form. I don't see why that syntax couldn't allow for the basic form I did restrict the expression to be a if (var p: properties["firstName"] != null) use(p); // or ..
if (@p: properties["firstName"] != null) use(p); We could of course very will have the prefix form for expressions in general (that is, it can only occur at the top level of the expression, and the expression would then presumably be a fully general But even though the postfix form sort of hides the name of the new variable pretty far to the right, I think it works quite well, visually: if (properties["firstName"]@p != null) use(p); |
Also compare to #1216 (inline prefix operators). It suggests allowing prefix operators inline in selector lists, binding to the next "selector", so If this really was an instance of #1216, we would also allow prefix the proposal has (IMO) a problem with delimiting the effect. Take: extension <R extends num, T> on R Function(T) {
operator -() => (arg) => -(this(arg));
} either can be correct. This proposal has the same issue: How far ahead does the name bind? The example: s.var:substring(7).toUpperCase(); suggests that What about Postfix variable binding would be better, then it would be a selector, and it always refers to everything before. Syntax would be a problem. Maybe The example |
We could use if (obj.prop# != null) use(prop);
if (s.foo(10)#baz is Foo) use(baz);
void main() { ...
if (link?.next#next1?.next#next2 != null) {
link.next = next2;
next1.next = next2.next;
next2.next = next1;
}
}
|
Postfix @ ProposalHere is an adjustment of the proposal to use postfix [Edits: Jan 4 2022: Examples and MotivationBinding expressions introduce a new kind of selector of the form A good way to read The binding expression may omit the name of the variable in the case where it occurs immediately after an identifier This means that new variables can be introduced very concisely, and they can reuse an existing name, e.g., A binding expression can be used to "snapshot" the value of a subexpression of a given expression: void main() {
int i = 42;
// '42 has 6 bits':
print('$i has ${i.bitLength@} bit${bitLength != 1? 's':''}');
print('$i has ${i.bitLength@len} bit${len != 1? 's':''}');
} The construct void main() {
var s = "Hello, world!";
var s2 = s.substring(7)@sub.toUpperCase() + sub;
print(s2); // 'WORLD!world!'.
} Binding expressions can bind the value of a complete expression to a new variable. In some cases this works simply by using class C {
int? i = 42;
void f1() {
if (i@ != null) { // Snapshot instance variable `i`, create local `i`, scoped to the `if`.
Expect.isTrue(i.isEven); // Local `i` is promoted by test.
this.i = null; // Assignment to instance variable; `i = null` is an error.
Expect.isTrue(i.isEven); // The local variable still has the value 42.
} else {
// Local `i` is in scope, with declared type `int?` from initialization.
Expect.isTrue(i?.isEven);
}
}
} The binding expression A binding expression always introduces a final local variable. The main reason for this is that it is highly error prone to have a local variable whose name is the same as an instance variable, which serves as a proxy for the instance variable because it has the same value (at least initially), and then assignment to the local variable is interpreted to be an assignment to the instance variable. A binding expression is intended to be a "small" syntactic construct. In particular, parentheses must be used whenever the given expression uses operators ( In return for this high precedence, we get the effect that expressions like The conflict between two things named This implies several things: It is an error to create a new variable using a binding expression if the resulting name clashes with a declaration in the current scope (so This proposal includes the proposal that every composite statement should introduce a scope containing just that statement is assumed in this proposal, and it is extended to wrap every GrammarThe grammar is updated as follows:
Static AnalysisEvery form of binding expression This proposal includes the following change to the scoping rules: Each statement S is immediately enclosed in a new scope if S is derived from one of the following: A local variable declaration D is treated such that variables introduced by a binding expression in D are in scope in that local variable declaration, and not outside D. This cannot be specified as a syntactic desugaring step, but it can be specified in a similar manner as the scoping for local variable introduced by an initializing formal parameter. The main rationale for making the variable final in all cases is that it is highly error prone to save the value of an existing variable If a variable A binding expression of the form A binding expression of the form The It would be a compile-time error to look it up in the current scope (because that's a reference to the variable itself before it's defining expression ends), and it is likely to be useful to look it up in the enclosing scope because the intended variable/getter would often be a non-local variable. A compile-time error occurs if
Null shorting does not cause any particular difficulties. For example, a binding expression of the form Cascades are desugared into expressions using only In all these cases, the expression that provides the declared type of the newly introduced variable is known as the initializing expression for the variable. In expressions with control flow (conditional expressions, logical operators like Dynamic SemanticsA local variable |
@eernstg would it be able to use it like this? String myFunc() => 'hello there';
myMap[someIndex]@foo = myFunc();
print(foo.length); This is nice as it avoids this scenario: final foo = myMap[someIndex] = myFunc();
print(foo.length); // error, foo can be null |
With the newest scope rules it would actually not work: ... // Declarations as needed.
String myFunc() => 'hello there';
void main() {
myMap[someIndex]@foo = myFunc();
print(foo.length); // Error, unknown name `foo`.
} The problem is that The reason for adopting such rigid scoping rules is that it will easily get too hairy to read the code if we can introduce variables in the middle of an expression statement, and then have them in scope in the following statements, which would potentially be many lines of code. The only case where these variables are in scope in many statements is when they are introduced by a structural expression of a composite statement, that is: The condition of an So you'll have to declare ``dart String myFunc() => 'hello there'; void main() {
However, we might prefer the form Actually, I think |
oh, it was my bad. Really had the thought that But testing on Dartpad it worked =] |
That's because an assignment has the type of the assigned value, not the type of the assignment target. So with |
@eernstg I know we talked about it before, however, being able to declare stuff in the scope would be very helpful in some cases. take this code example: someList.map((sub) => '''
foo(${sub.name.camelCase});
${someMethod(sub.name.camelCase)}
''').join('\n')} I don't want the code to twice call the getter someList.map((sub) {
final fn = sub.name.camelCase;
return '''
foo($fn);
${someMethod(sn)}
''').join('\n');
} Rewriting with someList.map((sub) => '''
foo(${sub.name.camelCase@sn});
${someMethod(sn)}
''').join('\n')} We usually feel this need when source generating and few other places. |
maybe this could be two different features? if-binding-expressionsallow var declaration within a block of condition, ie: if (foo.prop@ != null) {
// prop exists inside if
} inline-binding-expressionsallow var declaration within a parent of the expression it is being called, ie someList.map((sub) => '''
foo(${sub.name.camelCase@sn}); // sn exists within `map` and below this line
${someMethod(sn)}
''').join('\n')} |
@jodinathan wrote:
The given example would actually just work, because the innermost enclosing statement is the invocations of methods on someList.map((sub) => '''
foo(${sub.name.camelCase@sn});
${someMethod(sn)}
''').join('\n')} |
Binding expressions are expressions that introduce a local variable
v
with a name which may be taken from the expression itself or it may be specified explicitly. The variable is introduced into the enclosing scope which is limited to the nearest enclosing statement (e.g., an enclosingif
statement or loop, or an enclosing expression statement). The variable is accessible but not usable before the binding expression, and it is promoted based on the treatment of the binding expression.NB: A variant of this proposal using postfix
@
is available in this comment below.The inspiration for this mechanism is #1201 'if-variables', where the conciseness of introducing a variable with an existing name was promoted, and #1191, 'binding type cast and type check', where the ability to introduce a new variable was associated with a general expression.
[Edit Oct 15 2020: Now assuming the proposal from @lrhn that each composite statement introduces a new scope. Nov 24 2021: Mention proposal about using
@
rather thanvar
. Nov 25 2021: Make the scope restriction a bit more tight, limiting it to the enclosing expression statement if that's the nearest enclosing statement, and similarly for other statements that include an expression, e.g.,<returnStatement>
.]Examples and Motivation
In general, a binding expression can be recognized by having
var
and:
.It may or may not introduce a new name explicitly:
var x: ...
introduces the namex
, andvar:...
does not introduce a name explicitly. When a binding expression does not introduce a name explicitly, a name is obtained from the rest of the binding expression. This means that names can be introduces very concisely, and they can be "well-known" in the context because it is already a name which is being used for some purpose.A binding expression can be used to "snapshot" the value of a subexpression of a given expression:
The construct
var:bitLength
works as a selector (such that the getterbitLength
is invoked), and it also introduces a local variable namedbitLength
whose value is the result returned by that getter invocation. The constructvar length: bitlength
gives the new variable the namelength
, and otherwise works the same. This works for method invocations as well:In case of name clashes, it is possible to produce names based on templates where
$
plays the role as the "default" name:Apart from selectors and cascade sections, binding expressions can also bind the value of a complete expression to a new variable:
The binding expression
var:i
introduces a new local variable namedi
into the scope of theif
statement (theif
statement is considered to be enclosed by a new scope, and the variable goes into that scope). It is an error to refer to that local variable before the binding expression, so if we havefoo(var v1:e1, var v2:e2)
,e2
can refer tov1
, bute1
cannot refer tov2
.A binding expression always introduces a final local variable. The main reason for this is that it is highly error prone to have a local variable whose name is the same as an instance variable, which serves as a proxy for the instance variable because it has the same value (at least initially), and then assignment to the local variable is interpreted to be an assignment to the instance variable.
A binding expression is intended to be a "small" syntactic construct. In particular, parentheses must be used whenever the given expression uses operators (
var x: (a + b)
) or other constructs with low precedence. It is basically intended to snapshot the value of an expression of the form<primary> <selector>*
, that is, a receiver with a chain of member invocations, or even smaller things like a single identifier.In return for this high precedence, we get the effect that expressions like
var:i is T
orvar:i as T
parses such that the binding expression isvar:i
, which is a useful construct because it introduces a variablei
and possibly promotes it toT
.The conflict between two things named
i
is handled by a special lookup rule: For the constructvar:i
, the fresh variablei
is introduced into the current scope, and the initialization of that variable is done using thei
which is looked up in the enclosing scope. In particular, ifi
is an instance, static, or global variable,var:i
will snapshot its value and provide access to that value via the newly introduced local variable with the same name.This implies several things: It is an error to create a new variable using a binding expression if the resulting name clashes with a declaration in the current scope. (The template based naming that makes
$1
expand tonext1
and such helps creating similar, non-clashing names). Also, in the typical case where a binding expression introduces a namei
for a local variable which is also the name of an instance variable, every access to the instance variable in that scope must usethis.i
. This may serve as a hint to readers: If a function usesthis.i = 42
then it may be because the namei
is introduced by a binding expression.Update Oct 15: @lrhn's proposal that every composite statement should introduce a scope containing just that statement is assumed in this proposal, and it is extended to wrap every
<expressionStatement>
,<returnStatement>
, and a few others, in a new scope as well. This implies that when a binding expression introduces a variable and it is added to the current scope, it will be scoped to the enclosing statement S, which may include nested statements if S is a composite statement like a loop. If a binding expression occurs in a composite statement then it will introduce a variable which is available in that whole composite statement (e.g., also the else branch of anif
statement), but only there.Grammar
The grammar is updated as follows:
The overall rule is that a binding expression can be recognized by having
var
and:
, which makes it different from a local variable declaration (withvar
and=
), thus helping both readers and parsers to disambiguate. Apart from the fact that both=
and:
are used to provide values for a variable in Dart (e.g., for variable initializers and named parameters), the rationale for using:
is that it makesvar:x
introduce a variable namedx
and obtains its value using thex
that we would get if this new variable had not been created. Various other syntaxes seem less suggestive.Static Analysis
Every form of binding expression
e
introduces a final local variable into the current scope ofe
. Below we just specify the name and type of the variable, and the scope and finality is implied.This proposal includes the following change to the scoping rules: Each statement S is immediately enclosed in a new scope if S is derived from one of the following:
<forStatement>
,<ifStatement>
,<whileStatement>
,<doStatement>
,<switchStatement>
,<expressionStatement>
,<returnStatement>
,<assertStatement>
,<yieldStatement>
,<yieldEachStatement>
. For instanceprint('Hello!');
is treated as{ print('Hello'); }
andif (b) S
is treated as{ if (b) S }
.A local variable declaration D is treated such that variables introduced by a binding expression in D are in scope in that local variable declaration, and not outside D. This cannot be specified as a syntactic desugaring step, but it can be specified in a similar manner as the scoping for local variable introduced by an initializing formal parameter.
The main rationale for making the variable final in all cases is that it is highly error prone to save the value of an existing variable
x
(for instance, an instance variable) in a local variable whose name is alsox
, and then later assign a new value tox
under the assumption that it is an update to that other variable. In this case the assignment must usethis.x = e
because the local variablex
is in scope and is final.If a variable
x
is introduced by a binding expressione
then it is a compile-time error to refer tox
in the current scope ofe
at any point which is textually before the end ofe
.A binding expression of the form
var x: e1
introduces a variable namedx
with the static type ofe1
as its declared type.A binding expression of the form
var:e1
is a compile-time error unlesse1
is an identifier.A binding expression of the form
var:x
wherex
is an identifier introduces a variable namedx
with the static type ofx
in the enclosing scope as the declared type.Note that
x
is looked up in the enclosing scope, not the current scope. It would be a compile-time error to look it up in the current scope (because that's a reference to the variable itself before it's defining expression ends), and it is likely to be useful to look it up in the enclosing scope because the intended variable/getter would often be a non-local variable.A binding expression of the form
e1?.var x:m<types>(arguments)
where?
and<types>
may be omitted introduces a variable namedx
whose declared type is the static type ofe1.m<types>(arguments)
when this selector does not participate in null shorting, and the static type ofe1?.m<types>(arguments)
when it participantes in null shorting.A binding expression of the form
e1?.var:m<types>(arguments)
where?
and<types>
may be omitted introduces a variable namedm
whose declared type is the static type ofe1.m<types>(arguments)
when this selector does not participate in null shorting, and the static type ofe1?.m<types>(arguments)
when it participates in null shorting.If the previous two cases are not applicable, a binding expression of the form
e1?.var x:m
where?
may be omitted introduces a variable namedx
whose declared type is the static type ofe1.m
when this selector does not participate in null shorting, and the static type ofe1?.m
when it participates in null shorting; and a binding expression of the forme1.var:m
introduces a variable namedm
, with the same static types as the previous variant.A binding expression of the form
e?..var x:m<types>(arguments)
where?
and<types>
may be omitted introduces a variable namedx
whose declared type is the static type ofe0.m<types>(arguments)
when this cascade section does not participate in null shorting, and the static type ofe0?.m<types>(arguments)
when it participates in null shorting, wheree0
is the receiver of the cascade.A binding expression of the form
e?..var:m<types>(arguments)
where?
and<types>
may be omitted introduces a variable namedm
whose declared type is the same as in the corresponding situation in the previous case.If the previous two cases are not applicable, a binding expression of the form
e?..var x:m
where?
may be omitted introduces a variable namedx
whose declared type is the static type ofe0.m
when this cascade section does not participate in null shorting, and the static type ofe0?.m
when it participates in null shorting, wheree0
is the receiver of the cascade. Similarly, a binding expression of the forme?..var:m
where?
may be omitted introduces a variable namedm
with the same declared type as the corresponding case.In all these cases, the expression that provides the declared type of the newly introduced variable is known as the initializing expression for the variable.
In all these cases, and in expressions with control flow (conditional expressions, logical operators like
&&
and||
, and so on), it is a compile-time error to evaluate a variable v which has been introduced by a binding expression e unless e is guaranteed to have been evaluated at the location where the evaluation of v occurs.A special mechanism is provided for the case where a selector has a name that clashes with an existing name in the current scope: If a
$
occurs in the specified name then the name of the newly introduced variable replaces$
by the name that would be used if no name had been specified, capitalizing it unless$
is the first character.For example,
a.var $1:next.var $2:next
introduces a variable namednext1
and another variable namednext2
;a.var first$:bar.var second$:bar
introduces variablesfirstBar
andsecondBar
; andvar x$x: 'Hello'
is an error.If a variable
x
is introduced by a binding expressione
then lete0[[e]]
be a notation for the enclosing expressione0
with a "hole" that containse
. Promotion is then applied tox
as if it had occurred in an expression of the forme0[[x]]
.For example,
var x: e is T
promotesx
to typeT
iffx is T
would have promotedx
.Dynamic Semantics
A local variable
x
introduced into a scope by a binding expressione
is initialized to the value of the initializing expression ofx
at the time wheree
is evaluated. If the initializing expression ofx
is not evaluated (due to null shorting),x
is initialized to null.Discussion
About Generated Names
The mechanism that generates a name from a template may be considered arbitrary: It depends on
$
, it uses capitalization (which is only fit for camelCasedNaming), and it is unique in Dart in that it creates a name based on a textual transformation. It was included because it seems likely that the use of binding expressions will create name clashes where there is a genuine need for creating several similar names.The syntax is quite bulky:
a.b.var firstName: b.var secondName: b
makes it difficult to see the core expressiona.b.b.b
. It may be helpful to format this kind of construct with plenty of whitespace, such that the introduction of new variables is emphasized:Alternative Syntax: Use
@
and@name:
In this comment @jodinathan proposed using
@
rather thanvar
as the syntactic element that initiates the variable declaration:One (admittedly subjective) benefit is that the visually disruptive space after
var
is avoided, and another one is that@
can be used before any<selector>
, e.g.,myList@firstElement[0]
would correspond tovar firstElement = myList[0]
.One (similarly subjective) drawback is that there is no hint in the syntax itself about the fact that a variable is being declared. For instance,
a.@b
would not be seen as a construct that declares a variable namedb
by any developer who hasn't been told explicitly that this is exactly what that@
does. However, if it's used frequently enough then it probably doesn't matter much whether or not we can guess what it means the very first time we see it, no explanations given.The text was updated successfully, but these errors were encountered: