[impl Trait] Should we allow impl Trait after -> in fn types or parentheses sugar?

184e610
Opened by Niko Matsakis at 2021-05-28 14:57:59

RFC 1951 disallowed uses of impl Trait within Fn trait sugar or higher-ranked bounds. For example, the following is disallowed:

fn foo(f: impl Fn(impl SomeTrait) -> impl OtherTrait)
fn bar() -> (impl Fn(impl SomeTrait) -> impl OtherTrait)

This tracking issue exists to discuss -- if we were to allow them -- what semantics they ought to have. Some known concerns around the syntax are:

  • Should the () switch from existential to universal quantification and back?
    • I think the general feeling here is now "no", basically because "too complex".
  • If HRTB were introduced, where would we (e.g.) want impl OtherTrait to be bound?

For consistency, we are disallow fn(impl SomeTrait) -> impl OtherTrait and dyn Fn(impl SomeTrait) -> impl OtherTrait as well. When considering the questions, one should also consider what the meaning would be in those contexts.

  1. Should we allow impl Trait after -> in fn types or parentheses sugar?

    fn(impl SomeTrait) -> impl OtherTrait

    How is impl Trait meant to work in function pointers? Do you mean Fn instead of fn?

    est31 at 2018-05-31 14:12:37

  2. @est31

    How is impl Trait meant to work in function pointers? Do you mean Fn instead of fn?

    I meant fn -- this basically gets at the heart of the question, which is the scoping of impl Trait. In particular, one might expect that

    fn foo(x: fn(impl Trait))
    

    would desugar to

    fn foo<T: Trait>(x: fn(T))
    

    Of course -- as you righly point out -- if the T were going to be scoped to x -- sort of like for<T: Trait> fn(T) -- we couldn't actually compile that with monomorphization (though we could do it with Fn traits, potentially).

    Niko Matsakis at 2018-06-01 17:24:55

  3. @nikomatsakis I see, thanks for the clarification!

    est31 at 2018-06-01 22:11:04

  4. I've done a bit of hunting, and I think that this is the most appropriate place to bring this up.

    Allowing impl Trait in this position would allow us to define async callbacks that borrow the input parameters. This can be done with macros at the moment, but not bare async fns due to the difficulty of expressing the higher-ranked lifetime bounds involving type parameters.

    I would like to be able to write something like:

    impl StructWithCallback {
        // This works, but requires a macro/something to convert from `async fn` to something that has the right signature. 
        fn new(cb: for<'r> fn(&'r mut MyStruct) -> Pin<Box<dyn Future<Output = ()> + 'r>>) -> Self {
            Self {
                callback: Box::new(move |s| cb(s)),
            }
        }
        // I believe that this would also work if we allowed `impl Future` in this position.
        fn from_async_fn(cb: impl for<'r> FnMut(&'r mut MyStruct) -> (impl Future<Output = ()> + 'r)) -> Self {
            Self {
                callback: Box::new(move |s| Box::pin(cb(s))),
            }
        }
    }
    

    See also:

    • context for the above, in playground form: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=fce2ec77cee84e784fc56ad7f523dda8
    • The journey that Goose is taking in this area:
      • https://github.com/tag1consulting/goose/pull/8 - initial spike based on https://users.rust-lang.org/t/how-to-store-async-function-pointer/38343
      • https://github.com/tag1consulting/goose/pull/22 - current iteration, based on https://www.reddit.com/r/rust/comments/goh2be/how_to_store_an_async_future_to_a_function_with_a/
    • This would also be useful in the Gotham project, where we are currently forced to pass the context object in by move, and return a tuple containing the context (which stops us from using ?). I made a spike of this a few months ago, and hit the same issue: https://github.com/alsuren/gotham/pull/3/files#r429578771

    David Laban at 2020-05-23 21:43:14

  5. @alsuren I found a syntax / way that works:

    https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=ef3ca927495338661420b93141c5d0b6

    (similar to how Box itself also works with any functions)

    Would that change your stance that impl Trait is needed?

    Fabian Franz at 2020-05-26 14:44:47

  6. @LionsAd Unfortunately, that makes StructWithCallback<T> generic, which means that you can't store it in a vec or hashmap. https://gist.github.com/rust-play/b89d17f00f58f071555339093d76a0a3

    If you can't store it in a hashmap, then you can't store it in a routing table, so that's a non-starter for the Gotham use-case.

    A possible way to avoid needing impl Trait in this position would be to support writing fn foo(f: impl async Fn(SomeInput) -> SomeOutput). This would do the same lifetime elision and impl Future<Output=SomeOutput> desugaring that async fn does by default, so everything would fall out nicely in the wash.

    David Laban at 2020-05-26 19:22:31

  7. https://gist.github.com/87ab93979d770314e6698a9867d1e7e5 solves this problem using a helper trait.

    It might be worth waiting to see how https://github.com/gotham-rs/gotham/pull/450/files pans out, but it looks like we don't need impl Trait in this position for the use-case I describe above.

    David Laban at 2020-07-01 19:22:45

  8. I have a problem that is related to this issue: https://stackoverflow.com/questions/67458566/how-to-define-a-generic-function-that-takes-a-function-that-converts-a-slice-to. I want to write a generic function that takes a function that iterates a slice in different ways:

    fn foo(make_iter: impl for<'a> Fn(&'a mut [i32]) -> impl Iterator<Item = &'a mut i32>) {
        let mut data = [1, 2, 3, 4];
    
        make_iter(&mut data);
    }
    

    Currently, this is not possible. Is there any way to work around this issue with stable Rust? The following attempt fails:

    fn foo<'a, I: Iterator<Item = &'a mut i32>>(make_iter: impl Fn(&'a mut [i32]) -> I) {
        let mut data = [1, 2, 3, 4];
    
        make_iter(&mut data);
    }
    

    Note that data should be generated inside the foo function.

    I can also use boxed trait object for this, but it requires an extra allocation:

    fn foo(make_iter: impl for<'a> Fn(&'a mut [i32]) -> Box<dyn Iterator<Item = &'a mut i32> + 'a>) {
        let mut data = [1, 2, 3, 4];
    
        make_iter(&mut data);
    }
    

    EFanZh at 2021-05-14 12:01:06

  9. I came up with one solution:

    use std::iter::Rev;
    use std::slice::IterMut;
    
    trait MakeIter<'a> {
        type Iter: Iterator<Item = &'a mut i32>;
    
        fn make_iter(&mut self, slice: &'a mut [i32]) -> Self::Iter;
    }
    
    fn foo(mut make_iter: impl for<'a> MakeIter<'a>) {
        let mut data = [1, 2, 3, 4];
    
        make_iter.make_iter(&mut data);
    }
    
    struct Forward;
    
    impl<'a> MakeIter<'a> for Forward {
        type Iter = IterMut<'a, i32>;
    
        fn make_iter(&mut self, slice: &'a mut [i32]) -> Self::Iter {
            slice.iter_mut()
        }
    }
    
    struct Backward;
    
    impl<'a> MakeIter<'a> for Backward {
        type Iter = Rev<IterMut<'a, i32>>;
    
        fn make_iter(&mut self, slice: &'a mut [i32]) -> Self::Iter {
            slice.iter_mut().rev()
        }
    }
    
    fn main() {
        foo(Forward);
        foo(Backward);
    }
    

    But I am not sure whether it can be simplified.

    EFanZh at 2021-05-28 14:57:59