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

add stream::from_fn #130

Closed
wants to merge 5 commits into from
Closed

Conversation

yoshuawuyts
Copy link
Contributor

Implements stream::poll_fn. Ref #129.

Got one compile error around pin that I can't quite figure out; if someone could take a look to help out it'd be much appreciated. Thanks!

Signed-off-by: Yoshua Wuyts <[email protected]>
Signed-off-by: Yoshua Wuyts <[email protected]>
Signed-off-by: Yoshua Wuyts <[email protected]>
@yoshuawuyts
Copy link
Contributor Author

Also depends on landing #125 first to make the example work`

@yoshuawuyts yoshuawuyts changed the title From fn impl stream::from_fn Aug 30, 2019
@yoshuawuyts yoshuawuyts changed the title impl stream::from_fn add stream::from_fn Aug 30, 2019
@laizy
Copy link
Contributor

laizy commented Aug 30, 2019

this fix the compile error: laizy@5f6e526

@taiki-e
Copy link
Contributor

taiki-e commented Aug 30, 2019

@yoshuawuyts There is a higher-performance version: rust-lang/futures-rs#1842
Free feel to copy :)

Signed-off-by: Yoshua Wuyts <[email protected]>
/// if count < 6 {
/// future::ready(Some(count))
/// } else {
/// future::ready(None)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should make this an async closure (closure of async block) instead

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, probably the equivalent of this cannot be written by async/await at this time. (see rust-lang/futures-rs#1842 (comment))

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(This is one of the unuseful features I mentioned in #129 (comment).)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding is that it can't close over futures, but it can access statics, which may make it more useful?

@ghost
Copy link

ghost commented Sep 4, 2019

I still think stream::from_fn should have a different signature :)

Consider the following types of constructors:

// We already have this that matches `iter::once`.
fn once<T>(t: T) -> impl Stream<T>;

// What I want and matches `iter::from_fn`.
fn from_fn<T>(f: impl FnOnce() -> T) -> impl Stream<T>;

// What this PR introduces, but names it `from_fn`.
fn from_async_fn<T>(f: impl FnOnce() -> impl Future<Output = T>) -> impl Stream<T>;

// We don't have this yet, but perhaps should.
fn from_future<T>(f: impl Future<Output = T>) -> impl Stream<T>;

I find from_future the most powerful constructor because all others can be trivially implemented using it:

fn once<T>(t: T) -> impl Stream<T> {
    from_future(async move { t })
}

fn from_fn<T>(f: impl FnOnce() -> T) -> impl Stream<T> {
    from_future(async move { f() })
}

fn from_async_fn<T>(f: impl FnOnce() -> impl Future<Output = T>) -> impl Stream<T> {
    from_future(async move { f().await })
}

The main hazard of the from_async_fn-style constructor is in that it might have to be changed once the standard library introduces AsyncFnOnce trait. In that case, our constructor would change to:

fn from_async_fn<T>(f: impl AsyncFnOnce() -> T) -> impl Stream<T>;

I feel confident about having once and from_fn because they look like exact copies of iter::once and iter::from_fn.

But I don't feel confident about from_async_fn until the lang team decides what to do about the AsyncFnOnce trait.

Constructor from_future has no precedent in std::iter but I don't think we'd ever change our mind if we added it so that one is fine.

@yoshuawuyts
Copy link
Contributor Author

yoshuawuyts commented Sep 5, 2019

std::iter::from_fn has the following signature:

pub fn from_fn<T, F>(f: F) -> FromFn<F> where
    F: FnMut() -> Option<T>, 

The method I'm proposing here is an async version of that:

pub fn from_fn<T, F>(f: F) -> FromFn<F> where
    F: FnMut() -> impl Future<Output = Option<T>>,

@stjepang this would be equivalent to what you refer to as from_async_fn. In all fairness I don't see much value in adding sync -> async implementations because they could be implemented in an async -> async method with the same cost.

// These streams are equivalent; we probably should only have the async variant
let mut s1 = stream::from_fn(|| 12);
let mut s2 = stream::from_async_fn(async || 12);

You do raise an interesting point about forwards compatibility. Given our signature there wouldn't be a conflict with AsyncFnOnce but with a possible AsyncFnMut. I'm not sure what this would look like, but wouldn't it be fair to assume it might be similar to how async move || {} and || async move {} are equivalent for all intents and purposes?


However concluding; I agree there's no much inherent value to this one method. But I do like the idea of filling in all the blanks so we can point to stdlib to things we're missing (such as async variants of the closures perhaps?) But if we feel uncomfortable with this proposal I suggest we shelve it for now, and focus on things we feel more confident in -- overall this function doesn't seem that important, and we probably shouldn't spend too much time on it.

@ghost
Copy link

ghost commented Sep 8, 2019

The method I'm proposing here is an async version of that:

I guess my disagreement is around that - an async version of iter::from_fn should take a normal closure and not an async closure, just like Stream::map takes a normal closure and not an async closure.

This also got me thinking - an async closure taking no arguments is really not much different from a normal future.

Closures with no arguments are typically used to express laziness, for example:

// The "else value" gets computed only if `my_option` is `None`.
my_option.unwrap_or_else(|| compute_something_expensive());

But futures are already lazy by their nature, so a closure with no arguments returning a future is two layers of laziness. I find it difficult to come up with a situation where async || {} is more useful than async {}.

@yoshuawuyts
Copy link
Contributor Author

yoshuawuyts commented Sep 10, 2019

But futures are already lazy by their nature, so a closure with no arguments returning a future is two layers of laziness.

Hmm, these are good questions. But I think the differentiator is not in whether they're lazy, but how they work. Consider this scenario:

let a = async {};
let b = async || {};

a.await;
a.await; // panic

b().await;
b().await; // ok

The difference is that async {} is at-most-once, while async || {} can be called repeatedly.

The way I'm thinking about from_fn is that it's a placeholder for generators. In std::iterator it yields an "anonymous" iterator from sync calls. The way I view it in std::stream it should return an "anonymous" stream for async calls.

@yoshuawuyts
Copy link
Contributor Author

Let's revisit this at a later point as it's not that useful right now anyway. Thanks!

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

Successfully merging this pull request may close these issues.

3 participants