Skip to content
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

Are value-consuming generators useful? #101

Open
akrzemi1 opened this issue Sep 2, 2023 · 7 comments
Open

Are value-consuming generators useful? #101

akrzemi1 opened this issue Sep 2, 2023 · 7 comments

Comments

@akrzemi1
Copy link
Member

akrzemi1 commented Sep 2, 2023

The two-argument variant of async::generator, one that is able to consume values from the resumer, seems to have no practical use case. At least none has been provided in the docs or examples. I personally question its utility because of the shape of the interface. The coroutine sees it this way:

auto value_from_resumer = co_yield value_from_coroutine;

Where a natural reading is that value_from_resumer is in response to the yielded value_from_coroutine. But the resumer sees it this way:

auto next_value_from_coroutine = co_await some_genertor(value_from_resumer);

Here the resumer has to prepare and inject the value before it asks the coroutine to generate its next value. This is in contradiction to the previous expectation.

I presented my intuition. It may be wrong. But the documentation does not offer any other. I believe that the value consuming interface of async::generator should be either removed, or justified in the docs.

@klemens-morgenstern
Copy link
Collaborator

Well, the generator can be made lazy now, so your intuition is probably correct for eager generators, that would do a look ahead. There's probably some niche use-case, but I would agree that it's more likely that pushing values would be used for lazy ones.
But given that this is a runtime setting now, I don't see how that'll tarnish the interface.

@akrzemi1
Copy link
Member Author

So, it looks to me that at least the recommended practice should be that if you use a push-value generator you always put co_await async::this_coro::initial in the coroutine.

@akrzemi1
Copy link
Member Author

But even the lazy push-value generators: do they have a use case?
There is an example in the docs:
https://klemens.dev/async/tutorial.html#generator_with_push_value

It does not compile for at least two reasons (unused variable val and return in a coroutine). It should be lazy, I think.

Is there a full example anywhere? I have a feeling that a code that does not use a coroutine would be simpler. In the use case you simply request new things to be serialized. There is nothing being generated.

@klemens-morgenstern
Copy link
Collaborator

The example is missing a ser.reset(&val) call, will fix that.

The generator can make complex state management easier, because you can put things like the serializer into the generator frame. You need more complexity than that however for that to pay off (which makes bad examples) so I understand your skepticism. I would however also add, that you or I thinking something is simpler doesn't mean that users will agree. Based on my knowledge of other languages I am pretty confident that there are plenty of use cases.

Regarding the name: that's just the common name for the same thing in other languages (python, js).

To the "should always be lazy point": well that's how python does it. But there you have to await the generator once without a push value. I can't do that because C++ isn't dynamic, so that would be a bad interface. Therefor I think making it a runtime decision and giving the eager case valid (although weird) semantics is a proper fit for a C++ library.

@akrzemi1
Copy link
Member Author

By "recommended practice" I mean putting a note in docs, "if you are using a push-value generator, you probably want to perform the inirial suspense."

@akrzemi1
Copy link
Member Author

The example is missing a ser.reset(&val) call, will fix that.

That wouldn't be enough. It still has a plain return in the coroutine. I would recommend adding a full compiling program to folder examples.

The generator can make complex state management easier, because you can put things like the serializer into the generator frame. You need more complexity than that however for that to pay off (which makes bad examples) so I understand your skepticism. I would however also add, that you or I thinking something is simpler doesn't mean that users will agree. Based on my knowledge of other languages I am pretty confident that there are plenty of use cases.

I agree with this argumentation for generators that do not consume values. My criticism is for the ones that consume values.

I challenge the example not because of the "size of the maintained state" alone, but because of the combination: "size of the maintained state" and "nothing is generated".

Regarding the name: that's just the common name for the same thing in other languages (python, js).

I think that maybe the name is fine but the example is not exemplary of the capabilities of value-consuming generators. I realize that I just dump more work on you (find a better example), but I must admit, I am lacking imagination or intuition in this domain.

@akrzemi1
Copy link
Member Author

One more observation. Given that:

  • you have to specialize generator_base for type void,
  • synopsis in documentaiton lists conditional members (operators () and co_await)
  • that you seem to recomend different "lazines" defaults for the two cases: eager for Push==void and lazy for value-consuming generators,

The design of this library would be simpler if these two were separate coroutine types:

  1. One that simply generate values.
  2. The other that generates and consumes values.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants