-
Notifications
You must be signed in to change notification settings - Fork 214
Introduce let
construct for local destructuring
#2197
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
Comments
let
constructor for local destructuringlet
construct for local destructuring
So long as you're already choosing a variable name, why not separate the declaration into its own line (which would also encourage more meaningful variable names)? IMO it would help readers be able to quickly scan the scope and usage of the variable. var z = x + y + (let w = calculate(x, y) in w * w) + q;
// vs
final magnitude = calculate(x, y);
var z = x + y + (magnitude*magnitude) + q;
Map<int, X> toMap<X>(Iterable<X> iterable) => {let x = 0 in for (var y in iterable) x++: y};
// vs
Map<int, X> toMap<X>(Iterable<X> iterable) {
var index = 0;
return {
for (var y in iterable) index++: y
};
} |
People, for better or worse, like their one-line The Also, not all local variables in a collection literal can necessarily be hoisted: { for (var i in something) let x = expensiveComputation(i) in x.name: x } |
I can definitely see the appeal, and if Dart were an expression-based language, this would be a no-brainer. But in a fairly statement-oriented language that derives heavily from the C/Java/etc. syntactic tradition, I gotta say it looks and feels pretty weird to me. A big part of the problem for me is that it only has a single expression for the body and that doesn't really give you a lot of mileage for the variable you just bound. I have to assume that if we give people I strongly suspect that if we provide expression-level variables, they'll (reasonably!) ask for expression-level rest-of-the-whole-language. I don't want to be in the position that we're in with const expressions where each tiny piece we add just increases the desire to add the rest. I think we should either not do expression-level variables, or we should anticipate making most statements usable inside expressions. If it's the latter, how about instead of a special expression form for variable declarations, we consider the ECMAScript do expression proposal? That provides a nicely delineated way to embed an entire series of arbitrary statements in an expression context, with a single trailing expression to return the result. Your example becomes: class C {
(double, double) pair;
int get first => do { var (first, _) = pair; first };
} It's a little more verbose, but it extends to multiple variables and any other statement you could want. Also, it answers all of the questions around whether the variable(s) are final, typed, late, etc. You get all the existing local variable statement declaration semantics. Done. Personally, I'm not super enthused about this. I think code is easier to read if you don't have huge nested expressions containing imperative code in them. But if expression-local variables is important to many users, I think something like |
I don't think this is any better than: class C {
(double, double) pair;
int get first {
var (first, _) = pair;
return first;
}
} In a code review I would suggest replacing the former with the later. The only case that I think may benefit from this is when using if inside collections, which is quite common in Flutter Widget build(BuildContext context) {
return Column(
children: [
a,
b,
if (c)
do {
// some more complex logic to get a widget d
d;
},
],
);
} While today we have to either do the complex logic before the list or use a auto-calling function: Widget build(BuildContext context) {
// some more complex logic to get a widget d
return Column(
children: [
a,
b,
if (c)
d,
],
);
} Widget build(BuildContext context) {
return Column(
children: [
a,
b,
if (c)
() {
// some more complex logic to get a widget d
return d;
}(),
],
);
} |
Indeed, as would I. Part of the reason I'm not super enthused about expression-level variable declarations is that in many cases, you really are better off just hoisting things out and writing some statements. We're an imperative language. Statements are fine.
+1. When I was working on UI-as-code and trying to make these build expressions more, uh, expressive, I tried a number of different approaches to allow arbitrary statements and control flow in there and never got to anything I liked. The nice thing about collection-if and for is that the body is a single element which makes the common case where you just have a single value to emit a lot more terse. Extending that with do expressions might be a nice way to opt in to statements when you need something more powerful while still allowing the common case to remain terse as it is now. |
Could another operator be defined instead? Example syntax:
var x = let (first:, last:) = list in "$first, ..., $last";
var z = x + y + (let w = calculate(x, y) in w * w) + q; Would become: var x = (first:, last:) = list # "$first, ..., $last"; // The last expression is always returned
var z = x + y + (calculate(x, y) # it * it) + q; // `it` is the return value of calculate(...) And Map<int, X> toMap<X>(Iterable<X> iterable) => {let x = 0 in for (var y in iterable) x++: y}; Would become: Map<int, X> toMap<X>(Iterable<X> iterable) => {var x = 0 # for (var y in iterable) x++: y}; And var message = 'hello world';
[message].map((value) => value.replaceFirst('', '_')).map(print); Would become: var message = 'hello world';
message # it.replaceFirst('', '_') # print(it); If the |
I don't understand where you're going here. You feel that adding
This is not a very convincing argument to me. The overwhelming majority of Dart code is Flutter code, and the Flutter code is primarily based around... expressions. I'm not sure that Dart is so heavily statement based as all of that. Many of the language changes we have made over the past N years have been explicitly around enriching the expression language. |
The more I think about this exchange, the more puzzled I get. I also don't think the first suggestion is better than the second... but that's a critique of the counter-proposal from @munificent , not this issue. The original example was: class C {
(double, double) pair;
int get first => let (first, _) = pair in first;
} which is, in my opinion, better than either of the above. Do you disagree? Do you think the statement form is actively superior to the expression form? Would you suggest replacing the |
I do. With Flutter UI code, I fairly see users run into code like: return Column(
children: [
someExpensiveComputation().someProperty,
someExpensiveComputation().anotherThing
],
); And they don't want to perform that computation twice, but UI-as-code doesn't help. Though I suppose that you could reorganize code like this to not need an actual sequence operator, like: return Column(
children: [
...let c = someExpensiveComputation() in [c.someProperty, c.anotherThing]
],
); So maybe I'm wrong an a sequence operator wouldn't come up that often.
Is that... something we'd like users to write? Coming from an object-oriented C-ish background, that's pretty alien looking.
My point was just that if we're going to go there, let's pick an expression syntax that expands gracefully to handle it, which I think
Yes, but I think users still associate certain kinds of behavior with statements and don't like seeing it embedded inside expressions. I get pushback on the fact that the formatter allows single-line ifs because users don't like seeing With variable declarations, users have to infer not just the execution behavior but the scope of the variables, and with something like
Well, yeah. We probably should have made it completely expression-oriented from the get-go and we're trying to slowly deal with that limitation without making the language too weird in the process. It's a hard balancing act. I don't want to end up in the uncanny valley where the language looks half expression-based and half statement-based in a way that alienates both sets of user preferences, like we did with the old type system. Maybe this issue thread just needs better motivating examples, but I don't see any code in here where I think, "Yes, this code using |
Yeah, you're kind of making my point here. Though the better way to write that code is: return Column(
children: let c = someExpensiveComputation() in [c.someProperty, c.anotherThing],
); which sure LGTM.
It looks ok to me, coming from an ML background. :) But really, for Dart, I would suggest allowing most statements as elements in a let body. Then you get: let
expr1;
expr2;
in expr3 which is entirely reasonable.
I don't understand why you bring up control flow here. Are you anticipating my suggestion (above) to add statements? Otherwise this adds absolutely no control flow whatsoever not already present in Dart.
I highly question that. I think people are pretty familiar with
I mean, you started your comment with an example of exactly that, no? |
There is actually no new functionality here. You can get the same effect, with worse scoping, using existing language functionality and a pre-declared uninitialized variable Then T seq<T>(void _, T value) => value;
int x; // Uninitialized
...seq(x = expr1, expr2)... and Doesn't work with control flow elements, you need In fact, you can do return Column(
children: [
let c = someExpensiveComputation() in ...[c.someProperty, c.anotherThing]
],
); (notice the About allowing arbitrary statements in the I like it, but might as well just do The big difference from existing control flow blocks is that the declarations inside So, yes, I'd be fine with |
My point is that in a C-tradition language, there are certain things users generally associate more with statements than with expressions. I think that's mostly control flow (though With variables in particular, I think there is a fairly strong association with statements because blocks (which are a statement-only concept) are the delimiters for local scopes. (I understand that for elements go against all of this, but, then, we were pretty careful to ensure that users did find them fairly intuitive before we shipped them.) Again, I'm not saying this means we can't do
Really? Are there other languages in Dart's general area that have something like
I don't actually particularly like the example I wrote. I would rather have users write: var c = someExpensiveComputation();
return Column(
children: [
c.someProperty,
c.anotherThing
],
); I could maybe be on board with a return Column(
children: do {
var c = someExpensiveComputation();
[
c.someProperty,
c.anotherThing
]
},
); That keeps the familiar syntax for declaring a local variable and the intuition that its scope is to the end of the surrounding braces. |
Ignoring async, |
True. Ignoring |
Kotlin uses an extension for I do wonder whether patterns and tuples are going to put more pressure on this though. Tuples give you multiple returns, and without this, the only way to handle the elements of a multiple return function other than as an aggregate will be to bind them at the top level, which is a bit annoying, or use a single element switch expression, which is a bit annoying. Speaking of which, we will actually have
|
Well, as @lrhn noted, we already have To be clear, much of my concern about |
It would be valuable in Flutter for sure: |
Today I seriously considered writing |
Uh oh!
There was an error while loading. Please reload this page.
With pattern destructuring, we treat declarations specially.
You can write
but that doesn't easily let you destructure inside an expression, because declarations are not inside expressions.
So, let's introduce:
(possibly allow a comma-separated list of "pattern
=
expression" instead of requiring you to nestlet
s).The use cases would be
=>
functions:or, more importantly, inside collection literals:
The grammar has no end-token, which means that it'll likely need to have a low precedence, and will need to be parenthesized if used inside more complex expressions, which is very likely what's best for readability too.
The scope of the variable will only be the
in
-expression (or following bindings if we allow more than one:Should the variables be final? Or do we want a mutable variable. The latter seems occasionally useful, like:
You can always not assign to a variable, so being mutable seems like the best default.
People wanting extra safety against accidental mutation can write
let final x = ..., in ...
.Probably means
let
needs to be at least a built-in identifier, and probably a reserved word.The text was updated successfully, but these errors were encountered: