-
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
Generator expressions / Iterable literals and Stream literals #1633
Comments
Interesting idea. The grammar won't work, It's also somewhat restrictive that you can only use collection syntax. |
Fair enough, I suppose it would have to be defined to multiply by a
Sure, but there are also generator functions which are less restrictive. This wouldn't be much different from how python does it, where you have generator functions using function syntax and more restricted generator expressions using comprehension syntax.
Allowing statements inside a collection would be great, I agree with you there. If you allowed Personally I think allowing the [ 1, 2, yield 3 ] Would this be allowed? And if it is allowed then what is the resulting value? |
We're basically considering syntactic sugar for We could use abbreviated function literals, #265, to get the somewhat more concise form Iterable<List<int>> pythagoreanSynchronous1 = sync* {
for (int a = 0; a < 100; a++)
for (int b = a; b < 100; b++)
for (int c = b; c < 100; c++)
if (a * a + b * b == c * c) yield [a, b, c];
}();
// Compared to proposal here:
Iterable<List<int>> pythagoreanSynchronous2 = sync* [
for (int a = 0; a < 100; a++)
for (int b = a; b < 100; b++)
for (int c = b; c < 100; c++)
if (a * a + b * b == c * c) [a, b, c],
]; Not that different! ;-) And the nice thing is that this is just another case where a more general mechanism (abbreviated function literals) provides a bit of conciseness. It is indeed a breaking change to make It should not be a problem to parse those terms as function literals (Dart.g with updates as indicated in #265 handles the |
@eernstg I use the curly_braces_in_flow_control_structures lint rule in production code. I'm sure many others do as well since the pedantic package uses this lint rule. So I think a more realistic comparison using abbreviated function literals would be: Iterable<List<int>> pythagoreanSynchronous1 = sync* {
for (int a = 0; a < 100; a++) {
for (int b = a; b < 100; b++) {
for (int c = b; c < 100; c++) {
if (a * a + b * b == c * c) {
yield [a, b, c];
}
}
}
}
}();
Iterable<List<int>> pythagoreanSynchronous2 = sync* [
for (int a = 0; a < 100; a++)
for (int b = a; b < 100; b++)
for (int c = b; c < 100; c++)
if (a * a + b * b == c * c) [a, b, c],
]; The code diverges a bit further when using spreads: Iterable<int> oscillator1 = sync* {
for (;;) {
for (final item in [-1, 1]) {
yield item;
}
}
}();
Iterable<int> oscillator2 = sync* [for (;;) ...[-1, 1]]; One of the benefits of the snytax I have proposed is that it will be really easy to switch between generating a list versus generating an iterable, since they both use collection syntax. Someone might start out writing their code with a list, but then later want to switch to an iterable due to memory concerns. Also perhaps having a collection literal syntax for iterables would be useful in other areas. I know that pattern matching is being considered for the language #546, but I don't think the current proposal allows you to match against iterables. Languages like haskell and scala allow for pattern matching against lazy lists. |
That's true. But note that the generator function code doesn't have to use the extra loop: Iterable<int> oscillator1 = sync* {
for (;;) yield* [-1, 1];
}();
// Which should actually be written as the following, unless there is a need to mutate it:
Iterable<int> oscillator1a() sync* {
for (;;) yield* [-1, 1];
} Also, of course, the fact that the function allows statements rather than elements (or other expression-ish terms) allows the function bodies to be more expressive than the collection literals. If we allow statements in collection literals (including some new, hypothetical iterable literals) then we'd presumably need some markers like
That is indeed an interesting idea. If there is a special syntax for iterable literals then it should certainly be aligned with a similar syntax for patterns matching iterables. |
@lrhn suggested the same thing.
But, I still don't really understand how that would work. Particularly with As a side note. One of the issues I have with trying to express this using existing language features is it just isn't obvious. Yes, the existing syntax is not that different from the proposed syntax, but the existing solution requires you to make clever use of a somewhat obscure language feature. If you look at the dart language tour, the section on generators makes no mention of the fact that generators can be expressed as anonymous functions. Similarly, the section on anonymous functions makes no mention of the fact that anonymous functions can be expressed as generators. I suspect plenty of dart developers are unaware of anonymous generator functions. And then you have to think to use an immediately invoked anonymous generator function. Conceptually there is a lot more going on compared to defining a collection with collection syntax. ...As another side note. It also occurred to me that these expressions might be able to support recursive definitions. For example: Iterable<int> oscillator = sync* [-1, 1, ...oscillator];
// Could desugar to something like this:
Iterable<int> oscillator = () {
Iterable<int> oscillator() sync* {
yield -1;
yield 1;
yield* oscillator();
}
return oscillator();
}(); |
I came across this today and thought it was interesting.
I have no experience writing rust programs (YouTube somehow recommended the video to me), so I might not fully understand the proposal as it relates to rust. But, if I understand correctly, the proposal includes both generator functions and generator expressions for both iterables and streams. There is a short example of what a generator expression might look like in rust: let iter = async move iterator {
for await x in stream {
yield x;
}
};
for await x in iter {
println!("{:?}", x);
} |
I have wanted a syntax for iterable literals like this in the past. I think it fits well with enhanced collection literals and would let us write more code to look declarative. For example I'll sometimes rewrite a One disparity against collection literals is that I think we'd need to disallow |
Yeah, iterable literals would allow a declarative approach to writing iterables. Sure the syntax between an immediately invoked anonymous generator function and an iterable literal syntax would not be that much different, but in my experience you think about the problem differently when writing imperative vs declarative code.
I agree. Personally I never use map or filter anymore since collection-for and collection-if were added to the language.
Good point, I hadn't considered that. |
For what it's worth, when I was working on the UI as code stuff, I spent a lot of time trying to come up with a more general way of embedding arbitrary statements inside collections and using When I was working on the control flow collection stuff, I did spend some time thinking about a syntax for iterable literals too. One option is to follow Python's generator expressions and use parentheses: var iterable = (for (var i = 0; i < 1000000; i++) i); But I didn't want to step on future tuple syntax: var wat = (1, 2, 3); // Lazy iterable of three elements, or tuple? I ended up not pitching anything. My intuition is that lazy iterable are rare enough that they probably don't warrant dedicated syntax (especially given the opportunity cost of using that syntax on something more common). My feeling is that the |
I'm curious why you think that - I see lazy iterables used frequently. I do occasionally find it limiting that some iterable chains need to use hacks like
Just for comparison I tried migrating some real code in both styles. Original: final canRunWithSoundNullSafety = await _allMigratedToNullSafety(
packageGraph,
reader,
orderedBuilders
.map((e) => e.import)
.followedBy(postProcessBuilderDefinitions.map((e) => e.import))); IIFE: final canRunWithSoundNullSafety =
await _allMigratedToNullSafety(packageGraph, reader, () sync* {
for (var b in orderedBuilders) {
yield b.import;
}
for (var b in postProcessBuilderDefinitions) {
yield b.import;
}
}()); Proposed generator: final canRunWithSoundNullSafety =
await _allMigratedToNullSafety(packageGraph, reader, sync* [
for (var b in orderedBuilders) b.import,
for (var b in postProcessBuilderDefinitions) b.import
]); I find the second migration more readable than either the IIFE or the original. If we assume that we stopped following the lint requiring braces for flow control, the IIFE version does look nicer, but now it violates our recommendations and still doesn't look as nice as the generator IMO. final canRunWithSoundNullSafety =
await _allMigratedToNullSafety(packageGraph, reader, () sync* {
for (var b in orderedBuilders) yield b.import;
for (var b in postProcessBuilderDefinitions) yield b.import;
}()); |
For what it's worth, you could also do: final canRunWithSoundNullSafety =
await _allMigratedToNullSafety(packageGraph, reader, () sync* {
yield* orderedBuilders.map((b) => b.import);
yield* postProcessBuilderDefinitions.map((b) => b.import);
}()); I agree having dedicated syntax looks nicer (though I'm not sold on It's more a question of the opportunity cost of adding that dedicated syntax to the language when we could potentially use that grammar for other more common patterns, or spend that language design time on more valuable features. |
I don't particularly care if Just to throw out a crazy idea. Is there any chance that static metaprogramming #1482 could be applied to collection literals? Imagine if you could have a macro which takes a collection literal, and transforms it into code which constructs some other type of collection. Users could then write macros to construct an |
I don't think this is a crazy idea. :) C# has something called collection initializers which essentially let you use collection-literal-like syntax as an initializer for your own class. I have occasionally pondered something similar for Dart. Given how crowded Dart's expression grammar is, it would be really difficult to add without colliding with some other syntax, but I like the idea in general. |
Incidentally - in python it isn't the parentheses that create the tuple, it's the comma: >>> x = 1, 2, 3
>>> x
(1, 2, 3)
>>> type(x)
<class 'tuple'> |
@PaulRudin It would still be an issue if you are proposing the following syntax: 1, 2, 3 // tuple
(1, 2, 3) // lazy iterable It's a problem since you can add parenthesis around any arbitrary expression. This isn't an issue for python because python does not have iterable literals at all, only comprehensions that generate iterables. # python
1, 2, 3 # tuple
(1, 2, 3) # also tuple
(item + 1 for item in range(3)) # generator comprehension (lazy iterable) |
In dart we have literal expressions for lists, sets, and maps. I propose that we also allow for expressions that generate iterables and streams.
Take for example this code:
If we wanted to instead generate an
Iterable<List<int>>
we could do that with a generator function using thesync*
keyword. We can even usesync*
on lambda expressions which looks like the following:I propose that we could generate an
Iterable
by placing thesync*
keyword before a list literal, which would be syntactic sugar for the above:Similarly using the
async*
keyword would instead generate a stream:Of course when generating streams, you would likely want to use await within the context of the expression, so you should be able to do the following as well:
Which would be syntactic sugar for:
Should support spreads as well:
Which is equivalent to:
Raw values like:
Are equivalent to:
Generator expressions already exist in the python programming language (https://www.python.org/dev/peps/pep-0289/).
I feel this would make working with lazy collections in dart even easier.
The text was updated successfully, but these errors were encountered: