Author: rnystrom@google.com
Status: Abandoned, the unified proposal was accepted
Allow if and for in collection literals to build collections using
conditionals and repetition.
Note: Because this feature interacts heavily with Set Literals and Spread Collections, which are all being implemented concurrently, we have a unified proposal that covers the behavior of all three. That proposal is now the source of truth. This document is useful for motivation, but may be otherwise out of date.
A key goal of Flutter's API design is that, as much as possible, the textual
layout of the code reflects the nesting structure of the resulting user
interface. If a Button constructor call is nested inside a Padding constructor,
that button is surrounded by that padding on the screen. Ideally, a build()
method for a widget is a single nested expression tree that you can read from
top-to-bottom and outside-in:
Widget build(BuildContext context) {
return Row(
children: [
IconButton(icon: Icon(Icons.menu)),
Expanded(child: title),
IconButton(icon: Icon(Icons.search)),
],
);
}Dart's terse constructor syntax and list literals are enough to achieve that in simple cases like this. But real widgets often get more complex. In particular, widgets often need to conditionally omit or swap out certain child widgets.
Let's say we only want to show that search button on Android. Because there's no graceful way to omit an element from a list literal, we have to hoist that entire list out to the statement level where we can use control flow:
Widget build(BuildContext context) {
var buttons = <Widget>[
IconButton(icon: Icon(Icons.menu)),
Expanded(child: title),
];
if (isAndroid) {
buttons.add(IconButton(icon: Icon(Icons.search)));
}
return Row(
children: buttons,
);
}The code has lost its top-down structure. The reader first sees some list of buttons being built but doesn't know what they're for. Only when they reach the end do they see the outermost widget that contains them.
Also notice how much the code had to change to go from its original form to the modified one. All we wanted to do was omit a single element, but we had to reorganize the entire function.
Clever users have come up with workarounds like:
Widget build(BuildContext context) {
return Row(
children: [
IconButton(icon: Icon(Icons.menu)),
Expanded(child: title),
isAndroid ? IconButton(icon: Icon(Icons.search)) : null,
].where((child) => child != null).toList(),
);
}It's arguably better than the above code, but it's not obvious or terse. Adding spread syntax would let you do:
Widget build(BuildContext context) {
return Row(
children: [
IconButton(icon: Icon(Icons.menu)),
Expanded(child: title),
...isAndroid ? [IconButton(icon: Icon(Icons.search))] : [],
],
);
}Is that better? Maybe. It still doesn't make the intent of the code clear. The user wants to express "if we're on Android, include the search button" and they have to cobble together a few syntaxes to approximate that.
With this proposal, the code is:
Widget build(BuildContext context) {
return Row(
children: [
IconButton(icon: Icon(Icons.menu)),
Expanded(child: title),
if (isAndroid) IconButton(icon: Icon(Icons.search)),
]
);
}Compare this to the original non-conditional form. In order to make one child
widget conditionally omitted, all we had to do was add if (isAndroid) before
an element.
Note that the "body" of the if is not a statement. It's a list
element—an expression whose result is directly inserted into the resulting
list. This keeps the code declarative and expression-oriented. You don't state
how the element is inserted by modifying a list. It's less like "control
flow" and more like the conditional expansion tags in
various template languages.
Of course, else is supported too. Let's say we want to show an "about" button
instead of "search" when we're not on Android. With this proposal, it's:
Widget build(BuildContext context) {
return Row(
children: [
IconButton(icon: Icon(Icons.menu)),
Expanded(child: title),
if (isAndroid)
IconButton(icon: Icon(Icons.search))
else
IconButton(icon: Icon(Icons.about)),
]
);
}Users can and do use the conditional operator (?:) for cases like this today.
It works OK, but isn't very easy on the eyes. And, of course, it falls down in
cases where you don't have an "else" widget that you want to use instead of the
"then" one.
I admit it is a little strange seeing the familiar if keyword in a place
where it's never appeared before. But my hope is that the semantics are fairly
intuitive.
This is less common than conditional control flow, but repetition comes up too. At the statement level, you can loop if you want to execute something a certain number of times or for each of a series of items in an Iterable. In an expression context, it's useful if you want to produce more than one value.
Spread syntax covers some of these use cases, but when you want to do more than
just insert a sequence in place, it forces you to chain a series of higher-order
methods together to express what you want. That can get cumbersome, especially
if you're mixing both repetition and conditional logic. You always can solve
that using some combination of map(), where(), and expand(), but the
result isn't always readable.
So this proposal also lets you use for inside a collection literal. That
turns, for example, this code:
var command = [
engineDartPath,
frontendServer,
];
for (var root in fileSystemRoots) {
command.add('--filesystem-root=$root');
}
for (var entryPointsJson in entryPointsJsonFiles) {
if (fileExists("$entryPointsJson.json")) {
command.add(entryPointsJson);
}
}
command.add(mainPath);Into:
var command = [
engineDartPath,
frontendServer,
for (var root in fileSystemRoots) '--filesystem-root=$root',
for (var entryPointsJson in entryPointsJsonFiles)
if (fileExists("$entryPointsJson.json")) entryPointsJson,
mainPath
];Note the if nested inside the for and consider what that would look like if
using higher-order methods on Iterable instead.
A nice bonus of allowing for is that it gives us something not too far from
the "list comprehension" syntax supported by some other languages. We now
have a nice short syntax for creating a list from a computation:
var integers = [for (var i = 1; i < 5; i++) i]; // [1, 2, 3, 4]
var squares = [for (var n in integers) n * n]; // [1, 4, 9, 16]It may seem surprising, but for also works perfectly well for map literals.
It lets you turn this:
Map<String, WidgetBuilder>.fromIterable(
kAllGalleryDemos,
key: (demo) => '${demo.routeName}',
value: (demo) => demo.buildRoute,
);Into:
return {
for (var demo in kAllGalleryDemos)
'${demo.routeName}': demo.buildRoute,
};You can think of it as a more direct way of expressing what you'd use
Map.fromIterable() for today.
If we're going to support for, we may as well also support its asynchronous
sister await for:
main() async {
var stream = getAStream();
var elements = [await for (var element in stream) element];
}This gives you a concise way to transform each element of a stream and store the result in a list.
As some of the previous examples have shown, if and for can be freely
composed. That enables some interesting patterns and techniques:
[for (var x in hor) for (var y in vert) Point(x, y)]This produces the Cartesian product of all points in the rectangle.
[for (var i in integers) if (i.isEven) i * i]This produces the squares of the even integers.
This proposal can be composed with spread syntax to include multiple elements
based on a single if condition:
Widget build(BuildContext context) {
return Row(
children: [
IconButton(icon: Icon(Icons.menu)),
Expanded(child: title),
if (isAndroid) ...[
IconButton(icon: Icon(Icons.search)),
IconButton(icon: Icon(Icons.refresh)),
IconButton(icon: Icon(Icons.help))
],
]
);
}Again, this works in maps too. Here's an example I found in Flutter:
var routes = Map<String, String>.fromIterable(
kAllGalleryDemos.where((demo) => demo.documentationUrl != null),
key: (dynamic demo) => demo.routeName,
value: (dynamic demo) => demo.documentationUrl,
);This could become:
var routes = {
for (var demo in kAllGalleryDemos)
if (demo.documentationUrl != null)
demo.routeName: demo.documentationUrl
};You can sell basically any language syntax using toy examples. For a better sense of how this would look in reality, here's a less-contrived piece of code taken from Flutter:
// flutter/examples/flutter_gallery/lib/demo/contacts_demo.dart:54
Widget build(BuildContext context) {
final themeData = Theme.of(context);
final columnChildren = lines
.sublist(0, lines.length - 1)
.map((line) => Text(line))
.toList();
columnChildren.add(Text(lines.last, style: themeData.textTheme.caption));
final rowChildren = [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: columnChildren
)
)
];
if (icon != null) {
rowChildren.add(SizedBox(
width: 72.0,
child: IconButton(
icon: Icon(icon),
color: themeData.primaryColor,
onPressed: onPressed
)
));
}
return MergeSemantics(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: rowChildren
)
),
);
}I don't want to belabor the point, but again note how the top-down structure is lost. It's pretty imperative too. In order to visualize the resulting UI, the user doesn't have to just read the code, they have to simulate its execution in their head.
With this proposal, that becomes:
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: [
for (var line in lines .sublist(0, lines.length - 1))
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
)
)
]
)
),
);
}This proposal is mostly about readability, but that isn't the only benefit. By turning imperative, mutating code into declarative expressions inside the collection literal, type inference becomes more effective. Both upwards and downwards inference has more code to chew on.
Consider:
Widget build(BuildContext context) {
var buttons = <Widget>[];
if (isAndroid) {
buttons.add(IconButton(icon: Icon(Icons.search)));
}
buttons.add(IconButton(icon: Icon(Icons.menu)));
return Row(children: buttons);
}Note that explicit type annotation on the list literal. That's needed because we
don't know any of its elements at creation time. By moving that if inside the
list, we can use the elements to infer the list's type:
Widget build(BuildContext context) {
var buttons = [
if (isAndroid) IconButton(icon: Icon(Icons.search)),
IconButton(icon: Icon(Icons.menu))
];
return Row(children: buttons);
}In many cases, this upwards inference infers the type that you want, and being able to move more of the list's contents inside the literal improves that. In this case, though, the inferred type is a little more precise than desired. Fortunately, downwards inference is improved too. If the code is fully refactored to:
Widget build(BuildContext context) {
return Row(children: [
if (isAndroid) IconButton(icon: Icon(Icons.search)),
IconButton(icon: Icon(Icons.menu))
]);
}Now, the fact that Row's children parameter has type List<Widget> causes
that to be the inferred type of the list. We're able to do this because the
entire list creation is now a single expression so it can be moved right into
the constructor call for Row().
We extend the list and set grammars to allow control flow elements in addition to regular elements:
listLiteral:
const? typeArguments? '[' collectionElementList? ']'
;
setLiteral:
const? typeArguments? '{' collectionElementList? '}' ;
collectionElementList:
collectionElement ( ',' collectionElement )* ','?
;
collectionElement:
expression |
'if' '(' expression ')' collectionElement ( 'else' collectionElement )? |
'await'? 'for' '(' forLoopParts ')' collectionElement
;
Instead of expressionList, this uses a new collectionElementList rule since
expressionList is used elsewhere in the grammar like argument lists where
control flow isn't allowed.
Each element in a list or set can be one of a few things:
- A normal expression.
- An
ifelement. - A
forelement.
The body of if and for elements use collectionElement, not expression,
which allows nesting.
The changes for map literals are similar:
mapLiteral:
const? typeArguments? '{' mapLiteralEntryList? '}' ;
mapLiteralEntryList:
mapLiteralEntry ( ',' mapLiteralEntry )* ','?
;
mapLiteralEntry:
expression ':' expression |
'if' '(' expression ')' mapLiteralEntry ( 'else' mapLiteralEntry )? |
'await'? 'for' '(' forLoopParts ')' mapLiteralEntry
;
Note: The final grammar once spread is taken into account will be somewhat different to account for the ambiguity between sets and maps that contain only spreads, but the differences between this proposal and the final grammar should be fairly obvious.
Let the element type of a list literal be the static type of the type argument
used to create the list. So <int>[] has an element type of int. It may be
explicit or filled in by type inference. So [1, 2.0] has an element type of
num.
Let the key type and value type of a map literal be the corresponding
static types of the type arguments for a map literal. So <int, String>{} and
{1: "s"} both have a key type of int and a value type of String.
Let the body elements of an if element be the "then" element and the "else"
element if there is one. Let the body elements of a for element be the
single element it contains.
Both styles of for element may introduce a local variable, as in:
[
for (var i = 1; i < 4; i++) i,
for (var i in [1, 2, 3]) i
]If a for element declares a variable, then a new namespace is created on each
iteration where that variable is defined. The body of the for element is
resolved and evaluated in that namespace. The variable goes out of scope at the
end of the for element's body.
Each iteration of the loop binds a new fresh variable:
var closures = [for (var i = 1; i < 4; i++) () => i];
for (var closure in closures) print(closure());
// Prints "1", "2", "3".The static semantics of collection if and for mostly follow their statement
analogues.
If is a static error when:
-
The collection is a list and the type of any of the body elements may not be assigned to the list's element type.
<int>[if (true) "not int"] // Error.
-
The collection is a map and the key type of any of the body elements may not be assigned to the map's key type.
<int, int>{if (true) "not int": 1} // Error.
-
The collection is a map and the value type of any of the body elements may not be assigned to the map's value type.
<int, int>{if (true) 1: "not int"} // Error.
-
The type of the condition expression in an
ifelement may not be assigned tobool.[if ("not bool") 1] // Error.
-
The type of the iterator expression in a synchronous
for-inelement may not be assigned toIterable<T>for some typeT. Otherwise, the iterable type of the iterator isT.[for (var i in "not iterable") i] // Error.
-
The iterable type of the iterator in a synchronous
for-inelement may not be assigned to thefor-invariable's type.[for (int i in ["not", "int"]) i] // Error.
-
The type of the stream expression in an asynchronous
await for-inelement may not be assigned toStream<T>for some typeT. Otherwise, the stream type of the stream isT.[await for (var i in "not stream") i] // Error.
-
The stream type of the iterator in an asynchronous
await for-inelement may not be assigned to thefor-invariable's type.[await for (int i in Stream.fromIterable(["not", "int"])) i] // Error.
-
awaitis used when the collection literal is not inside an asynchronous function. -
awaitis used before a C-styleforelement.awaitcan only be used withfor-inloops. -
The type of the condition expression (the second clause) in a C-style
forelement may not be assigned tobool.[for (; "not bool";) 1] // Error.
Inference propagates upwards and downwards like you would expect. For the most
part, inference flows "through" the if and for into the body element(s).
-
If a list literal has a downwards inference type of
List<T>for someT, then the downwards inference context type of the body elements isT.Thus:
List<List<String>> i = [ if (true) [], if (false) [] else [], for (var i = 0; i < 1; i++) [] ];
Produces a
List<List<String>>containing three emptyList<String>. -
The upwards inference element type of an
iflist element without anelseis the type of the "then" element. -
The upwards inference element type of an
if-elselist element is the least upper bound of the types of the "then" and "else" elements. -
The upwards inference element type of a
forlist element is the type of the body element. -
If a map literal has a downwards inference type of
Map<K, V>for someKandV, then the downwards inference context type of the keys in the body elements isKand the values isV.Thus:
Map<List<String>, List<int>> i = { if (true) []: [], if (false) []: [] else []: [], for (var i = 0; i < 1; i++) []: [] };
Produces a
Map<List<String>, List<int>>containing three entries. Each key is an emptyList<String>and each value is an emptyList<int>. -
The upwards inference key type of an
ifmap element without anelseis the key type of the "then" element, likewise for the value type. -
The upwards inference key type of an
if-elsemap element is the least upper bound of the key types of the "then" and "else" elements, likewise for the value type. -
The upwards inference key type of a
formap element is the key type of the body element, likewise for the value type.
As with the if statement, the condition expression of an if element induces
type promotion in the "then" element of the if when the condition expression
shows that a variable has some type and promotion isn't otherwise aborted.
A collection literal is now a series of elements (some of which may contain nested subelements) instead of just expressions (for lists and sets) or entries (for maps). A constant collection takes that tree of elements and expands it to a series of values (lists and sets) or entries (maps). The resulting collection contains that series of values/entries, in order.
We have to be careful to ensure that arbitrary computation doesn't happen due to control flow appearing in a constant collection. There are five kinds of elements to consider:
-
An expression element (the base case in lists and sets):
- It is a compile-time error if the expression is not a constant expression.
The expansion is the value of the expression.
-
An entry element (the base case in maps):
-
It is a compile-time error if the key or value expressions are not constant expressions.
-
As is already the case in Dart, it is a compile-time error if the key is an instance of a class that implements the operator
==unless the key is a Boolean, string, integer, literal symbol or the result of invoking a constant constructor of class Symbol. It is a compile-time error if the type arguments of a constant map literal include a type parameter.
The expansion is the entry formed by the key and value expression values.
-
-
A spread element:
See the relevant proposal for how these are handled.
-
An if element:
-
It is a compile-time error if the condition expression is not constant or does not evaluate to
trueorfalse. -
It is a compile-time error if the then and else branches are not potentially const expressions. The "potentially const" is to allow a the unchosen branch to throw an exception. In other words, if elements short-circuit.
-
It is a compile-time error if the condition evaluates to
trueand the then expression is not a constant expression. -
It is a compile-time error if the condition evaluates to
falseand the else expression, if it exists, is not a constant expression.
The expansion is:
-
The then element if the condition expression evaluates to
true. -
The else element if the condition is
falseand there is one. -
Otherwise, the
ifelement expands to nothing.
-
-
A for element:
These are disallowed in constant collections. In order to fit within the restrictions on constants, the set of things you could conceivably do with
foris so limited that we felt the best option was to omit it entirely.
The description here merges maps with lists and sets, but note that, of course, a const list or set may not contain entry elements and a map may not contain expression elements. (The grammar prohibits this anyway.)
Dart allows the const keyword to be omitted in "constant contexts". All of the
expressions inside elements in a constant collection are const contexts,
transitively. This includes the if condition expression, spread expression,
etc.
The new dynamic semantics are a superset of the original behavior. To avoid redundancy and handle nested uses, the semantics are expressed in terms of a separate procedure below:
-
Create a fresh instance
collectionof a class that implementsList<E>.An implementation is, of course, free to optimize by pre-allocating a list of the correct capacity when its size is statically known. Note that when
ifandforcome into play, it's no longer always possible to statically tell the final size of the resulting flattened list. -
For each
elementin the list literal:- Evaluate
elementusing the procedure below.
- Evaluate
-
The result of the literal expression is
collection.
-
Create a fresh instance
collectionof a class that implementsSet<E>. -
For each
elementin the set literal:- Evaluate
elementusing the procedure below.
- Evaluate
-
The result of the literal expression is
collection.
A map literal of the form <K, V>{entry_1 ... entry_n} is evaluated as follows:
-
Allocate a fresh instance
mapof a class that implementsLinkedHashMap<K, V>. -
For each
elementin the map literal:- Evaluate
elementusing the procedure below.
- Evaluate
-
The result of the map literal expression is
map.
This procedure handles elements in both list and map literals because the only difference is how a base expression element or entry element is handled. The control flow parts are the same so are unified here.
-
If
elementis anifelement:-
Evaluate the condition expression to a value
condition. -
Subject
conditionto boolean conversion to a valueresult. -
If
resultistrue:- Evaluate the "then" element using this procedure.
-
Else, if there is an "else" element of the
if:- Evaluate the "else" element using this procedure.
-
-
Else, if
elementis a synchronousfor-inelement:-
Evaluate the iterator expression to a value
sequence. -
Evaluate
sequence.iteratorto a valueiterator. -
Loop:
-
If the boolean conversion of
iterator.moveNext()does not returntrue, exit the loop. -
If the
for-inelement declares a variable, create a freshvariablefor it. Otherwise, use the existingvariableit refers to. -
Evaluate
iterator.currentand bind it tovariable. -
Evaluate the body element using this procedure in the scope of
variable.
-
-
If the
for-inelement declares a variable, discard it.
-
-
Else, if
elementis an asynchronousawait for-inelement:-
Evaluate the stream expression to a value
stream. It is a dynamic error ifstreamis not an instance of a class that implementsStream. -
Create a new
Future,streamDone. -
Evaluate
await streamDone. -
Listen to
stream. On each data eventeventthe stream sends:-
If the
for-inelement declares a variable, create a freshvariablefor it. Otherwise, use the existingvariableit refers to. -
Bind
eventtovariable. -
Evaluate the body element using this procedure in the scope of
variable. If this raises an exception, completestreamDonewith it as an error.
-
-
If the
for-inelement declares a variable, discard it. -
If
streamraises an exception, completestreamDonewith it as an error. Otherwise, when all events in the stream are processed, completestreamDonewithnull.
-
-
Else, if
elementis a C-styleforelement:-
Evaluate the initializer clause of the element, if there is one.
-
Loop:
-
Evaluate the condition expression to a value
condition. If there is no condition expression, usetrue. -
If the boolean conversion of
conditionis nottrue, exit the loop. -
Evaluate the body element using this procedure in the scope of the variable declared by the initializer clause if there is one.
-
If there is an increment clause, execute it.
-
-
-
Else, if
elementis a spread element, see the relevant proposal. -
Else, if
elementis an expression element:-
Evaluate the element's expression to a value
value. -
Call
collection.add(value).
-
-
Else,
elementhas formkeyExpression: valueExpression:-
Evaluate
keyExpressionto a valuekey. -
Evaluate
valueExpressionto a valuevalue. -
Call
map[key] = value.
-
This is a non-breaking change that purely makes existing semantics more easily expressible, so there is no required migration.
It may be possible for tooling to detect some of the existing idioms that are better expressed using this new syntax and give the user the option to automatically change it to the new style. Cases where conditional logic has been hoisted all the way out of a collection may be hard to detect since the resulting code is pretty imperative.
It should be possible to detect cases like:
-
Switching out an element using a conditional operator:
[ before, condition ? first : second, after ]
Fix:
[ before, if (condition) first else second, after ]
-
Omitting an element using a conditional operator and
nullfiltering:[ before, condition ? first : null, after ].where((e) => e != null).toList()
Fix:
[ before, if (condition) first, after ] -
Using
Map.fromIterable():Map.fromIterable(things, key: (e) => someExpression(e), value: (e) => anotherExpression(e) )
Fix:
{ for (var e in things) someExpression(e): anotherExpression(e) }This may fall down if the two closures are complex but I think that's rare in practice.
I think we probably don't want to blanket apply all of these fixes without user intervention. There may be some style preferences or the fix may not always succeed. This is a big enough change where having a human validate it is a good idea.
As always, the immediate next step of a proposal is running it past the language leads and stakeholders.
This feature has some good things going for it:
-
It is mostly syntax sugar. A front end should be able to compile this down to existing Dart semantics (with perhaps some extra support needed for
ifin const collections). It shouldn't significantly impact the runtime or backends, so the implementation cost should be relatively low. -
The semantics are narrow and fairly straightforward. It doesn't interact with the type system in complex ways. It doesn't touch tricky parts of the grammar, calling conventions, or runtime behavior. I think the implementation is fairly low-risk. I don't think we're likely to run into major surprises we didn't anticipate during implementation.
However, the human side of this is less certain. I've tried to make the behavior intuitive by piggy-backing on syntax users already understand. But I worry that:
-
Users will find it confusing to see
iforforinside a collection literal. -
Even after understanding it, users may not like the syntax.
-
Users may be unhappy that the syntax doesn't go far enough. This feature may lead them to expect, say,
whileto work inside a collection. They may expect to be able to put an entire block of statements as the body of anif. They may want to useifoutside 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.
-
Users may want to include multiple elements inside a body and not know how to accomplish that. The spread proposal gives them a mechanism, but it may not be a natural or obvious one.
I don't think we can reasonably resolve these on paper, so before shipping this feature, I think we should do user studies of some of these scenarios and refine the behavior if needed based on the results.
This proposal only covers conditional execution in collections. A natural extension that would be particularly useful for Flutter is to extend it to argument lists:
IconButton(
icon: Icon(Icons.menu),
tooltip: 'Navigation menu',
if (isAndroid) padding: const EdgeInsets.all(20.0),
)Without rest parameters, for isn't useful and if probably isn't
feasible for positional arguments. But even without rest params, it's possible
to support if for named arguments.
We can and should look at doing that as a separate proposal.
The proposal only allows one looping construct, but Dart has three: for,
while, and do-while. What's special about for?
The key reason is that for loops are implicitly terminated. A for-in loop
ends when it reaches the end of the iterator. A C-style for loop ends when the
condition expression returns false, which is in turn based on the increment
expression.
while and do-while loops both have a condition expression that signals
termination, but that's not enough. For that to work, the body of those loops
must have some explicit side-effect that eventually causes the expression to
return false.
But, in this proposal, the body of a loop is an element whose primary role is declarative—it emits a value that gets added to the resulting collection. There's no room there for an imperative, side-effecting operation.
In order to make a while loop usable, you'd need some kind of block structure so
you can contain side-effectful statements (including possibly break). But you
also need a way to emit values, which is the primary purpose. It's hard to come
up with a syntax that supports side effects that doesn't also make the main use
case—emitting values—more verbose and less declarative.
In other words, for loops are declarative enough to work well in an expression
context, but while and do-while loops are not.
Also, when examining a corpus for collection literals, I found a number of cases
where for loops would be useful, but none where I felt the other kinds would
be.
There's also an argument that if what you're doing is so imperative that you
want a while loop, then you should hoist that out into the statement level.
The readability benefits of embedding control flow inside a collection literal
is that it keeps more of your code declarative and expression-based. If your
code is actually imperative, then the most familiar, readable way to express
that is using actual statements.
You can always move that imperative code into a separate function which returns an Iterable, and then use spread syntax to insert the results of that into your collection.