-
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
Control Flow Collections #78
Comments
Two questions:
Seems a bit arbitrary and confusing to newcomers that the first like would be allowed and the second one would require conditional operator var x = [if cond expr else expr].first;
var y = if cond expr else expr; |
My answers to those two questions:
We could use |
@lrhn said it well. The other practical reason for not doing switch was that I didn't see any places in the corpus I examined where it seemed like it would be valuable. Allowing |
While we are not designing our language from scratch we are trying to attract more new users to the language - and they are learning the language from scratch. Put yourself in their shoes and ask yourself if this makes sense to you: var x = [if cond expr else expr].first; // ok
var y = if cond expr else expr; // not ok
var z = cond ? expr : expr; // ok
var a = [cond ? expr : expr] // ok, but we suggest this can be converted to if Cause it does not make sense to me. It makes this feature non-orthogonal - where it could easily be made orthogonal without any significant implications AFAIK. If we make it so non-orthogonal - then we can at least make our tools produce meaningful errors, because currently it is not handled very well (Note that DartPad's analyzer happily parses this code - while CFE reports compilation errors) Regarding the switch: this feature is requested quite often and modern language actually do have it, including Java (see http://openjdk.java.net/jeps/325 - which provides very good rationale to have it). Again - adding |
I think the main implication of making |
Would not users similarly expect to use blocks when When I see The error that you would need to implement for I think the only unpleasant and hard to explain corner case here is the interpretation of |
I actually believe that users will be able to distinguish collection comprehension I had the same worries initially, but I actually got to the point where it felt natural. It's arguable that we now have "similar syntax for different meanings", but what we have is actually When used as a a statement, Apart from that, they actually work exactly the same, performing iteration or choice at run-time. Since all the contexts have the notion of repetition and of doing/adding nothing, or doing/adding more than one thing, we can use both iteration and the For expressions, that does not apply. An expression has one result, and it always has a result if it terminates normally. That's why neither repetition nor one-or-none conditions apply to expressions, and why we should not try to extend So, I do think the proposed syntax is consistent with the existing language, and allowing (That does not explain why we can't have |
I am somewhat worried about that (the proposal mentions this as a concern), but I think it's less of a worry because users do understand they are inside the context of a list and that's a pretty explicitly "not statement" place. Something like this feels more ambiguous to me: var foo = if (condition) {
...
} else {
...
} Yes, technically the initializer is strictly an expression so it's definitely not a statement context, but it does look a lot like one.
The syntax for switch is already bad: weird label-looking things, mandatory break, fall-through, etc. I strongly believe we shouldn't make switch more complex. It's primary value in Dart is that it works identical to switch in other languages which eases porting and learning. Anything we add to switch to make it "better" worsens those attributes If we want to make a "better switch" (and I do), I think we should add a separate new pattern-matching syntax that removes all of the warts of switch, allows destructuring, works on user-defined types, etc. If we do that, then I think it's reasonable to consider making it an expression and/or allowing it as a control flow element in a collection. More pragmatically: switch just isn't used that often and I couldn't find any examples in the corpus I looked at where allowing switch inside a collection literal would be useful. |
It seems that outside of the context of constructing a collection literal, the die has been cast for Now as far as far as the specification under question is concerned, is function application allowed within the context of an |
A function application is allowed for list literals, because a function application is an expression, and a list element is an expression. In a map literal, the entry is a key:value pair, which is not a normal expression, nor is it an expressible value. You literally has to write "expression-colon-expression" to be a map entry. (You can perhaps write |
Yes, any expression is allowed there, including a function call.
That's right. You could spread a map into it: var a = {1: 2, 3: 4};
var b = {
5: 6,
if (condition) ...a
}; |
Hi @munificent, this looks very nice. I don't think we should allow general if and for expressions. It's so non-obvious what they mean that it's better not to introduce them at all and there are pretty simple workarounds for where you'd use them. (What is the value of an if with no else and a false condition? What if it's a non-nullable type? Runtime error? What is the value of a for loop? The last value? The rest are evaluated for their side effects?) The runtime semantics needs one more pass through it. Here are some things I spotted to clarify:
I'll sketch up what this would look like as a Dart-to-Dart (actually Kernel) transformation. I'd like to see if we could come up with an operational and static semantics that says it behaves as if ... to explicitly bless such an implementation technique. Notice that for elements gives you let for local variables through an encoding (without closures) which is kind of new. |
Thanks for the feedback!
The proposal says:
Is that clear enough? By "the element's body", it means "the for element's body". I'll tweak the text to clarify that.
They may not be evaluated. If, for example, the condition in an var list = [
if (false) throw "!"
];
print(list); // [] Is that what you mean, or am I missing something?
Ah, you're right. For the spread proposal, I added a separate section to cover the const semantics. I need to do something similar here. I'll try to get that in pretty soon.
That sounds great. It is my intent that this feature is effectively just syntax sugar, so if it's not possible to do that, that would be useful data.
Yes. Importantly, it means it's transparent to print([for (var i in [
[for (var j in [
[for (var k in [
3
])]
])]
])]); |
For loop scoping is either clear enough but not correct, or it is correct but it's not clear enough. I think Dart programmers will expect [for (int i = 0; i < 5; ++i) () => i].map((f) => f()).toList() to be When I wrote that elements are not always added to the collection, I was just wrong. I had scrolled past the base cases. |
The proposal does state:
I'll tweak the text above that to be a little clearer. |
This resonates with me. I don't understand the restrictions of the proposal.
I think users will expect this as well when if and else appear in collections. It seems like the rule for collections is now: When you use This rule seems a bit odd, in the sense that I can't think of any other languages with such a rule that allows control structs as expressions. But it may make sense in the Dart language. If so, this rule could generalise to Widget build(BuildContext context) {
return if (loggedIn)
LogoutButton()
else
LoginButton()
};
} What is the problem with "everything is an expression"? Kotlin allows if...else and try...catch in an expression context, and I've never seen anyone complain: |
I'm actually of the opinion that having your control structures being
expressions instead is statements has few downsides, and seems to be
particularly useful for the domain of creating UIs, as it allows for a more
declarative style. It also allows for more const variables, which should
allow for better performance optimizations on the margin.
I think the only downside of control expressions is that you have to make a
decision about how to treat block ending control expressions in a function
that does not specify it's return value, but that is only an issue if you
go full bore and make all blocks expressions.
It is definitely more of a design decision and reflects more about what
kind of language we want Dart to be. While it was born as a reasonable,
dynamic OO language, it has transformed in such ways that have improved
it's ergonomics for its primary use case of building rich, interactive UIs
with Streams, isolates, and most recently a strong, safe type system. If
we find that control expressions are for more expressiveness in this narrow
case of building collection literals, it may be true that it makes sense to
make such semantics uniform throughout the language.
That would be following the principle of least surprise, at least in my
opinion.
…On Tue, Dec 11, 2018, 03:09 Kasper Peulen ***@***.*** wrote:
Users may be unhappy that the syntax doesn't go far enough. This feature
may lead them to expect, say, while to work inside a collection. They may
expect to be able to put an entire block of statements as the body of an
if. They may want to use if outside of a collection but in other expression
contexts.
In other words, this may be a "garden path" feature that encourages a
whole set of expectations, some of which are met and the rest of which are
confounded.
This resonates with me. I don't understand the restrictions of the
proposal.
I think the main implication of making if an expression is that users will
likely expect to be able to use blocks as the then and else statements.
We'd either have to make that a surprising error (surprising since the
style guide requires you to use blocks in almost all cases for if
statements) or we'd have to allow blocks as expressions too, which I think
does open the whole "everything is an expression" can of worms.
I think users will expect this as well when if and else appear in
collections. It seems like the rule for collections is now:
*When you use if as an expression braces should be omitted and only one
expression per branch is allowed.*
This rule seems a bit odd, in the sense that I can't think of any other
languages which such a rule that allows control structs as expressions.
But it may make sense in the Dart language. If so, this rule could
generalise to if expressions:
Widget build(BuildContext context) {
return if (loggedIn)
LogoutButton()
else
LoginButton()
};
}
What is the problem with "everything is an expression". Kotlin allows
if...else and try...catch in expression context, and I've never seen anyone
complain:
https://kotlinlang.org/docs/reference/idioms.html#if-expression
—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
<#78 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/ABURTxnQzwOpNwCc_mi81kfo7WIQTSFgks5u35JigaJpZM4YMKkE>
.
|
@gamebox I think what you said is what I meant to say. |
I doubt if the examples you gave actually shows that it is more readable than the spread syntax. I do think that using var command = [
engineDartPath,
frontendServer,
for (var root in fileSystemRoots) '--filesystem-root=$root',
for (var entryPointsJson in entryPointsJsonFiles)
if (fileExists("$entryPointsJson.json")) entryPointsJson,
mainPath
]; With spread syntax: var command = [
engineDartPath,
frontendServer,
...fileSystemRoots.map((root) => '--filesystem-root=$root'),
...entryPointsJsonFiles.where((entryPointsJson) => fileExists("$entryPointsJson.json"))
mainPath
]; Also the other example becomes with the spread syntax: Widget build(BuildContext context) {
final themeData = Theme.of(context);
return MergeSemantics(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
...lines.sublist(0, lines.length - 1)).map((line) => Text(line)),
Text(lines.last, style: themeData.textTheme.caption)
]
)
),
if (icon != null) SizedBox(
width: 72.0,
child: IconButton(
icon: Icon(icon),
color: themeData.primaryColor,
onPressed: onPressed
)
)
]
)
),
);
} It feels to me that Let's look at this problem from a different angle. We can now do the following in Dart: var integers = () sync* {
for (var i = 1; i < 5; i++) yield i;
}().toList(); And with the spread syntax you can do: var integers = [...() sync* {
for (var i = 1; i < 5; i++) yield i;
}()]; This of course looks very clunky, in Javscript there is a proposal to have do generator expressions, that would look like: var integers = [...do* {
for (var i = 1; i < 5; i++) yield i;
}]; I have earlier proposed for ecmascript to make it even shorter: var integers = [...for (var i = 1; i < 5; i++) yield i]; In general, if you use The var integers = [for (var i = 1; i < 5; i++) i]; This is short and readable, but it feels less in line with the rest of the language (and style guide). Also because it is so restricted you can do the things you would like to do with a for loop (because you can't do it with map/where without making it less readable). For example something like: var solutionBatch = for (var solution in solutions) {
var numberData = numberDatas.firstWhere(
(numberData) => numberData.oldNumber == solution.number,
orElse: () => null);
if (numberData != null) {
yield Solution(id: solution.id, number: numberData.newNumber);
}
} Last note about Map<String, String> generator() sync* {
for (var demo in kAllGalleryDemos) {
if (demo.documentationUrl != null) {
yield demo.routeName: demo.documentationUrl;
}
}
} tldr, my changes to the proposal would be:
|
I think your mental model of the proposal is that [if (false) 123]; What is the value of the [for (var i = 1; i <= 3; i++) i]; What is the value of this The var value = ...[1, 2, 3]; What spread and the You are correct that if you have spread, then generators give you everything you need. Instead of: var list = [
if (condition) a else b,
for (var i in stuff) i * 2
]; You can always do: var list = [
...() sync* {
if (condition) yield a; else yield b;
}(),
...() sync* {
for (var i in stuff) yield i * 2;
}
]; The three main problems with that are:
I did consider something like implicit generator blocks. They do have the nice property that any statement is allowed, so the "garden path" problem is solved. But they also force you to explicitly Orthogonal to this is supporting Doing that now would be very hard. One key reason is map literals. Consider: var what = if (condition) {} else {} Are those Unfortunately, in Dart, we have a couple of places in the grammar where the exact same text means two different things in an expression and statement context. If we make everything an expression, then those contexts merge and we end up with an ambiguous grammar. Teasing that apart after we've reached 2.0 would be very difficult. |
I see.
I definitely think that allowing I doubt that the proposed The
I really like it as well in other languages, also (Except if Dart would adopt something like
Ah I see. JS has something similar with arrow functions and map literals. For example what does the followiong code mean? const a = () => {}; Here const c = () => ({}); |
Yeah, you always can, but it can be particularly cumbersome in some places. The typical bad cases where when you have some conditional logic inside the looping. To translate that to higher-order methods requires
If you want to stuff some imperative code in a context where an expression is expected, you can always use an immediately-invoked lambda. In practice, it's usually better (more readable, maintainable) to hoist that out to a separate named function.
I have some concerns around that as well. I think definite assignment analysis is another viable approach. Even when you have if as expression, it doesn't gracefully handle cases where you are declaring and initializing multiple variables: var a;
var b;
if (condition) {
a = ...
b = ...
} else {
a = ...
b = ...
} Definite assignment analysis can handle that. You can do it with var [a, b] = if (condition) {
[..., ...]
} else {
[..., ...]
} But I'm not sure if that would feel natural in a language like Dart.
Ah, that's right. Yes, we could do something similar where we require you to parenthesize to indicate that you want a map literal. |
Can you show me examples of those two cases? For the examples in the proposal I could not help thinking that the spread operator combined with map and where would be an as readable option (and faster writable with method completion). |
Sure, here's a contrived one: var list = [
for (var i in items)
for (var j = 0; j < i; j++)
if (j.isEven) j
]; |
You could do |
@tatumizer did your read the whole proposal? Most of the points you mentioned are discussed there and there are other aspects. |
That's correct. This is one of my concerns with the syntax. Using Though, in your example, the code wouldn't be syntactically valid as a block without a semicolon after Overall, the users I've shown the syntax too so far don't seem too bothered by it, so this isn't keeping me up at night. In many cases, if you try to write a block and get a set instead, the resulting code will have a type error because the set doesn't match the list's expected type. We could potentially lint or warn if you try to use a set literal after an But the current plan is to just try the current proposal and see how much of a problem this actually is in practice before we try to "fix" it. |
The "void means nothing" approach is interesting, but it can't stand by itself. You need a type system that understands This "voidable" type starts to look suspiciously like a nullable type. It means If Dart had a proper empty So, I guess I'm saying that what is being proposed here could have been an alternative to nullable types, but I very much doubt we will have both. |
@tatumizer, I agree that The special discipline applied to expressions whose static type is So
This is not quite precise enough (e.g., if that had been true then it would then have been an error to have
Actually, that is allowed. The main reason is that we wanted to support the situation where an argument of a function is being ignored, and the call site is allowed to know about it. For example: X seq<X>(void _, X x) => x;
In order to allow things like With respect to the notion of 'lateinit' variables, I really think Dart needs a mechanism that is supported at run time, that is, we should be able to allow for cases where the static analysis does not establish a guarantee that the variable has been initialized, and then we'll have a dynamic error if it is not initialized when it is first evaluated. With non-null types, this works perfectly for cases like Of course, this property must be predictable for developers, so we could require something like However, that doesn't have much to do with the type I think the problem is less severe for variables with no initializer whose type is nullable, because the variable would be initialized to
(I'll skip that topic today ;-)
Every object has type However, the upcoming null-aware spread operator actually does this using List<int> xs = null;
if (something) xs = [3];
var ys = [1, 2, ...?xs, 4, 5]; With this, In summary, I can see the temptation to make the type void do all these things, but I don't think it will work for Dart, and it's certainly not very compatible with the existing interpretation of that type which says that it can be any object, but it must be discarded. |
Just a quick drive by comment: it looks to me like what folks are reaching for here is a generalization from a single valued expression to a 0/1/many valued expression. What you're looking for is not really |
@tatumizer wrote:
I don't think that will be easy. We started off from Dart 1 where But (Of course, we could have made So:
Interestingly, that could match up with the notion of var map = {
"foo": x ?? _,
"bar": x?.y?.z ?? _ // TRY DOING IT WITH IF :)
} ... the rule would be that "the bomb" is written as I think the typing would match up better, though, if we were to generalize foo([int i = 42]) => i;
main() {
print(foo()); // Prints '42', argument omitted syntactically.
print(foo("the bomb")); // Prints '42', argument omitted semantically.
} I don't know whether it will work out. ;-) |
@tatumizer wrote:
I'm afraid it is very unlikely that we will change |
Done, thanks for reminding me. :) Leaf's comment is spot on. People are looking at this proposal as having to do with making This proposal and spread narrowly define a couple of "generator elements" ( Extending that to allow these elements in arbitrary expressions is interesting but much less clearer semantically. I don't think we could expect users to infer what, say, this means: var k = ["a", "b", "c"];
var v = [1, 2, 3, 4];
var map = {...k: ...v}; I could see us expanding both the set of generator elements over time and the set of places where they are allowed somewhat. But I don't think it will make sense to collapse elements and expressions into a single category. Dart isn't Icon. |
@tatumizer wrote:
(Oops, didn't see this, I was traveling at the time.) I think this extra "I'm not here" object Z wouldn't need to have many properties. Various parts of the code need to recognize that the given object is Z, and then do something else than usual, like not adding an element to a list (that is: certain language elements need to be specified to do so, but arbitrary user code could just be written to follow the same conventions, as long as Z appears in some context where it has no special semantics, presumably including a built-in It (presumably) just needs one special property: It should be allowed to occur everywhere, such that we can abstract over "I'm not here"-ness. But isn't that a perfect match for the null object, using nullable types to indicate that some value "may not be there"? This would mean that we are statically tracking whether absence is permitted (nullable/non-null types are built for that already), and we have the ability to write code when a value is tested (using The missing bit, as far as I can see, would be that we'd want language support for a few extra cases: We already have null-aware method invocation etc., so there are lots of ways to explicitly allow absence and do nothing when it occurs. But we could add the ability for a composite literal whose element (or key or value) type is non-null to accept a nullable typed expression and interpret the null object as "skip", and similarly for named parameters: void foo({int x = 42}) => print(x);
main() {
foo(); // Prints '42', as usual.
foo(x?: null); // Ditto. Proposed by Lasse at some point, I think.
foo(x: someNullableExpression); // Error.
<int>[1, 2, ?null]; // Yields <int>[1, 2].
// etc ...
} So apart from the fact that you'd have to announce that there is a potential for receiving the null object in a situation where the context type is a non-null type (thus flagging the possible "I'm not here" semantics explicitly), I think this is very nearly an approach that we have already had on the table several times. With that, I'm tempted to say that we don't need a Z which is different from the null object. It does confine the "I'm not here"-ness to nullable typed expressions in a context where a non-null instance is expected (except that we added some |
Cool, thanks! |
Rationale: Implementation of control-flow collections is done through the notion of block expressions. This is a first implementation to get this working. It takes a few shortcuts (like disabling OSR inside block expressions for now) that may be refined later. dart-lang/language#78 dart-lang/language#47 Change-Id: I966bf10942075052fcfd9bac00298a179efc551b Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/94441 Reviewed-by: Alexander Markov <alexmarkov@google.com> Reviewed-by: Martin Kustermann <kustermann@google.com>
Closing; this is launching in Dart 2.3 |
In order to handle conditionally omitting elements from list literals (#62, #70), I propose that we allow
if
andfor
inside list and map literals.Feature specification draft.
This issue is for discussing that proposal.
Edit: The proposal has been accepted as is being implemented. The implementation work is tracked in #165.
The text was updated successfully, but these errors were encountered: