Repository navigation
Bikeshedding the 'gen' keyword & syntax #123731
Description
#117078 lists "unresolved" questions:
- Whether we should use some keyword other than gen.
- Whether we should reserve gen as a full keyword or as a contextual one.
@yoshuawuyts For the FCP I started above, the
genkeyword andgen { ...}blocks andasync gen { ... }blocks are in-scope, whilegen fnis out of scope.
So we need a keyword for "gen blocks"; gen fn might get added in the future.
Conflicts
This conflicts with rand::Rng::gen (see rust-random/rand#1435). There are quite a few hits within the rand crate. The rand_distr crate has 3 uses within documentation, 2 within tests and 14 in algorithms.
This isn't a lot of data, but hints at quite a few real uses of Rng::gen outside rand, not counting other possible uses of gen.
From the RFC
I haven't read all comments on the RFC, but several indicate that gen fn may not be a good thing.
IMO this function syntax, while analogous to how async functions work, looks very surprising and non-obvious:
gen fn odd_dup(values: impl Iterator<Item = u32>) -> u32Without inspecting the opposite end of the line, the function looks like it returns a single
u32. I knowasync fnworks the same way, but at least when you see anasync fn foo() -> u32you know it will eventually return a singleu32.Not to mention there's good arguments why
async fnmight have also been a mistake.Because the "simple" case of an owned iterator doesn't require complicated type signatures unlike some Futures generated by async fn, wouldn't this be a good time to adopt this function expression syntax suggested many times in the past (including the blog post above)?
fn foo() -> impl Iterator<Item = u32> = gen { ... }
(NOT A CONTRIBUTION)
I think there are several good arguments for replacing
->with something likeyieldoryields. I'm pretty much just summarizing here:* Educational: it helps users understand that this yields many times, instead of returning once. It ties the declaration syntax to the different return keyword inside the body. * Forward compatibility: I personally don't think general purpose coroutines should be declared with the same keyword as generators-as-Iterators, but this does leave space for that possibility in the syntax by allowing the syntax to support a separate `->` type declaration.The only possible difficulty you need to consider is that you want to be forward compatibility with the ability to declare the type yielded by a
gen { }block. This may lean towardyields; not sure ifgen yield i32could be supported by the parser or if its ambiguous.
I was thinking potentially
fn f() gen i32for 'generates'
There is also discussion on usage of generator instead and discussion on gen as a contextual keyword.
Since I'm not fully read-up on the status of this RFC and its implementation, I'll just leave this here with the question of whether gen will become a keyword.
Activity
jieyouxu commentedon Apr 10, 2024
jieyouxu commentedon Apr 10, 2024
Relevant Zulip thread regarding reserving the
genkeyword: https://rust-lang.zulipchat.com/#narrow/stream/268952-edition/topic/.60gen.60.20keywordPatchMixolydic commentedon Apr 10, 2024
As a user, I'm not too concerned about what the keyword is, as long as it's fairly succinct and suggests either generators or iterators1.
generseems a little weird, but would probably avoid most name collisions.generatorseems a little too long, at least for Rust.I'd be highly in favour of providing some sort of
gen fnsyntax to make it easier to use generators, both as iterators and as restricted coroutines, but I'm not entirely sure what that'd look like. In isolation,gen fn name(...) yield YieldTy { ... }seems fine to me, but that might lead to incongruencies if the language team ever stabilizes coroutines with return types other than()2.Footnotes
Coroutines would also work, but might conflict with syntax space for general (semi)coroutines. ↩
Ignoring resume arguments, the obvious choices for general coroutine syntax would be
co fn name(...) yield YieldTy -> RetTy, which is internally inconsistent, orco fn name(...) yield YieldTy return RetTy, which is inconsistent with normalfns. ↩Yokinman commentedon Apr 11, 2024
I don't think
async fnwas a mistake. The consistency among keywords that affect a function's syntax is definitely useful, at least documentation-wise (const,async,unsafe, etc.). In my opinion,genaffecting the entire function's control flow should keep consistent with keywords likeasync.It feels natural to read
-> Tas, "this produces T", but it is different from the usual meaning. I think this syntax looks confusing whenreturnends the function without a value.It would also make it possible to confuse functions returning single types versus iterator types, just in terms of skimming function return types in documentation or source code. I would prefer anything else like:
gen fn() -> yield i32- Could retain-> Tas "produces T". e.g.coroutine fn() -> yield i32, return i32.gen fn() yield i32- Uses an existing keyword. Slightly awkward to read.gen fn() yields i32- Requires a new keyword. Reads very nicely.gen fn() ^ i32- Upwards arrow (yields to the caller, may resume later).gen fn() >> i32- Two arrows (produces multiple values).gen fn() => i32- Is->meaningfully distinct from=>? I suppose->is usually for types and=>is usually for values.I can't wait for generator functions, I love generator functions.
Your friend,
Yokin
oli-obk commentedon Apr 11, 2024
One thing that I think is important is that we pick the same keyword for coroutines and generators, as they are very similar. Coroutines take arguments on every resume, even the first one, so they need a closure-style syntax (e.g.
coro |x| {...}orgen |x| { ... }). So ifgenis considered too breaking, we can instead useco {}orcoro {}blocks, which both have significantly less edition breakage.I personally am not convinced that the extra language surface of
gen fnare worth it, and would like to see whetherfn foo() -> impl Iterator<Item = i32>is too annoying in practice and use that as information on whether to addgen fn. It's also not clear to me whethergen fnis sugar forfn() -> impl Iteratororfn() -> impl PinnedIterator(doesn't exist yet, but we likely want it), and if we stabilize it asimpl Iteratorwe can't take it back. This is in contrast togen {}blocks, which can just implement more traits in the future without it being a breaking change.ds84182 commentedon Apr 11, 2024
Eh.
gen fn |x| {...}¯\_(ツ)_/¯use
fnto disambiguate, or even require it.Slight off topic:
fn(x) => exprandfn(x) {}are just 2 to 4 extra characters and share similarity with match. Too late to change the syntax.rpjohnst commentedon Apr 13, 2024
I don't think this is something we should commit to at this point. Both generators and async functions are specializations of the more general underlying control flow mechanism, exposing different subsets of that behavior- neither expose the "resume argument" to the user and both fix the yield and return types in particular ways.
Coroutines should remain free to change arbitrarily, so long as
awaitandyieldcan desugar to them, until we're happy with them as a user-facing feature (if ever).oli-obk commentedon Apr 14, 2024
Yea, that makes sense. Especially since I was reminded that gen closures could be a different thing, the equivalent to async closures
oli-obk commentedon Apr 14, 2024
Throwing some options out there:
gen, break lots of cratesgener(personal opinion: it's not clear what it means, tho we'll learn)impl Iterator {}, kinda long but very extensible to builtin syntax for other traits (impl Generator,impl Try), conflicts with inherent implsiter/iterator, breaks too much codesomething!, make it a builtin macro, so local definitions and imports override itgenerator, wordy, but explicit. We don't need to stick with the "keywords must be short" rule and it likely has no problematic breakage#[gen] {}, a bit odd, but very extensiblegenbecoming a contextual keywordyields {}, may be too confusing withyieldmove gen {}could signal the difference tostatic gen {}which would not create iterator impls, but only some so-far-non-existentPinnedIteratortrait). Not really necessary (we can just infer which traits to implement forgenblocks, though this is another nail in the coffin ofgen fn). Also it's very confusing asmove genis different fromgen move(move all variables instead of letting the generator be movable) and implies the existence ofmove gen move.tmandry commentedon Apr 16, 2024
Right now I'm -1 on any syntax that uses
-> Tto mean "yields typeT"; I think usingyieldoryieldsis much better. I'm also okay with deferring the question and using-> impl Iterator<Item = T>for now; I think that's how the RFC ended up.If we reserve
yieldsit could be reused for the above purpose. I guess explicit type annotation for generator blocks would look like this then:yields T { ... }Seems no worse than
genin that regard; maybe slightly clearer.oli-obk commentedon Apr 17, 2024
An insight on the
randedition breakage:Gen::genis never implemented by users. So it would be fine to just deprecate it on all editions and add ageneratemethod instead. This is a minor version bump change only.Considering we have two weeks left until the first 2024 edition deadline, I would say we go with
genas T-lang has already decided on the RFC and resolve thegenkeyword bikeshed. I am getting the feeling that the general mood is forgenas a keyword, we're just unhappy to different degrees with the side effects that decision gives us.yoshuawuyts commentedon Apr 17, 2024
Apologies to @oli-obk - it was just yesterday that I said I wouldn't post in this thread and would prefer to write a blog instead to explore the implications of the
yieldskeyword. But I'm finding myself short on time this week, so here's my longer-form thinking on the choice of keywords for generator blocks and closures.Gen keyword
With generator blocks and closures there is a distinction between the logical return type and the logical yield type. While
Iteratorcannot express any return type other than(), we have to anticipate that at least syntactically generator blocks and closures will want to support distinguishing between the type yielded and the type returned. Using the baregen {}syntax currently being proposed this would result in the following notations for generator blocks and closures.We're not yet making a decision on generator functions, though they are available on unstable. But it's worth pointing out that much of the same rationale I've listed here applies to those too, but unlike generator blocks and closures, generator functions must always spell out their yield type - they cannot just rely on inference the way blocks and closures do.
If we were to use
-> Tto mean "yields typeT", that would make it impossible for generators notation to express a different return type, which as an implication means that generalized coroutines would need to use a different keyword with different semantics. This directly means that we'd need to be comfortable having e.g. both agen || -> T {}andcoro || -> T {}where-> Tmeans different things. I believe that would be bad.Yields keyword
On the RFC PR I floated the idea of using
yieldsas the notation for bare generator blocks and closures. I wasn't entirely sure of that idea at the time, but I believe it has merit spelling out the example in full, so let's do that here:As you can see here, the fact that we're supporting both generator blocks and generator functions ends up leading to syntactic ambiguity. If we were to adopt
|| yields {}as the notation, it would be unclear whether this was a function returning an generator, or a generator function. These are distinct concepts, and I believe it's worth keeping these separate.To resolve this ambiguity we'd need to put the
yieldskeyword before the||argument list, which seems a little strange. In effect we're declaring a function output in a position where normally we'd describe function inputs. If we were to consider the syntax for generator functions, it's unlikely we'd want to write:fn yields u32 () {}or similar. We almost certainly would prefer any form ofyieldsto come after the()argument list.Gen + Yields
One way to still reap the benefits of a
yieldsnotation without the parsing ambiguity would be for generator blocks and closures to always requiregento signal it's a generator, but permityieldsas an explicit notation after the argument list to explicitly declare the yield type.While a little more verbose than the bare
yieldsexample, I believe this makes up for it by being unambiguous. It does introduce a small wart in the sense that generator functions don't require being annotated, so there may be a loss of parity between the two notations.Having gone through this exercise, I believe that of the three options this option carries the least worst tradeoffs.
Conclusion
I think
gen {}as a keyword here is fine, as long as we eventually pair it up with some form ofyieldsnotation to be able to annotate the yield type. I mostly wanted to make sure it was spelled out whyyieldsas a keyword is necessary, and why introducing it without an additional prefix marker for closures and blocks would lead to ambiguity in the parser.Basically that's a long way to say that I find myself agreeing with what @tmandry expressed earlier.
Yokinman commentedon Apr 17, 2024
I could be wrong, but I don't think the syntax can be
gen || 🔹 Y -> Rsince that would be ambiguous with function types.On the other hand, the syntax
gen || -> R 🔹 Yis probably unambiguous.I don't really like this second one though, cause in my head it reads "returns the type
Rwhich yields the typeY" like some kind of syntax sugar for function pointers (fn() yields i32meaningfn() -> impl Iterator<Item=i32>). It's also arguably backwards, reading the yield type after the return type.I think the syntax
gen || -> 🔹 Y 🔸 Rwould solve this, likegen || -> 🔹 Y, return Rorgen || -> 🔹 Y; R.The first feels a bit more readable, refactorable, extensible. The second flows better as a "mini function body" kind of syntax. Similar to
fn() -> i32 { 1 }, you would have the correspondingfn() -> yield i32; i32 { yield 1; 2 }.tmandry commentedon Apr 18, 2024
This is the only part of your comment I disagree with @yoshuawuyts. I think
gen fncarries its weight in the same wayasync fndoes, by signaling a transformation to the function signature and body. Specifically, it signals that the return type of the function is something likeimpl Iterator<Item = u32>rather than the written type, and none of the code inside the body is run until iteration begins.There's also the question of consistency with async closures, blocks, and functions.
For completeness though, if we wanted the "minimal syntactic noise" option I guess we could use
genonly to introduce generator blocks, and only theyieldskeyword for everything else. I don't actually think this is a good idea, for all of the reasons above.@Yokinman We can resolve such ambiguities, which I would expect to be rare, by requiring parentheses to disambiguate.
In fact, we may want to use
yieldswithGenFnand perhaps other traits, which would make thegen || -> R 🔹 Ysyntax you raised equally ambiguous.Admittedly, all of this discussion is getting outside the scope of the original issue. I don't think we have to reserve
yieldstoday since as long as it always followsgen/fn/||, it works as a contextual keyword.oli-obk commentedon Apr 19, 2024
until we know what traits it's supposed to implement (e.g. hypothetical
PinnedIteratorif there are borrows across yields), we probably shouldn't go down this route. And even then, we basically always have to make it returnPinnedIterator, otherwise a seemingly small body change can be a breaking change, because suddenly anIteratorimpl disappears.yoshuawuyts commentedon Apr 19, 2024
@tmandry Oh I absolutely agree about the question about consistency. I see that as the main thing we'd be trading off. I also agree that we need some identifier in the function signature to signal: "Hey this function here, this is a generator function". We could certainly allow people to write the following:
I was thinking that if we disallowed that first case (
gen fnwithoutyields), the presence ofyieldswould be enough to signal that we're dealing with a generator function. At which point, at least from a parser perspective, also having agenkeyword present would not be required. Not sure if you were seeing something different here?oli-obk commentedon Apr 29, 2024
as per #123731 (comment) I'm gonna close this as resolved now. We're going with
genhttps://github.com/rust-lang/rust/issues/123731#issuecomment-2053953399